diff options
860 files changed, 26057 insertions, 10270 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index b1c091bfa946..a60ced5835ea 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -65,13 +65,13 @@ aconfig_declarations_group { "android.sdk.flags-aconfig-java", "android.security.flags-aconfig-java", "android.server.app.flags-aconfig-java", + "android.service.appprediction.flags-aconfig-java", "android.service.autofill.flags-aconfig-java", "android.service.chooser.flags-aconfig-java", "android.service.compat.flags-aconfig-java", "android.service.controls.flags-aconfig-java", "android.service.dreams.flags-aconfig-java", "android.service.notification.flags-aconfig-java", - "android.service.appprediction.flags-aconfig-java", "android.service.quickaccesswallet.flags-aconfig-java", "android.service.voice.flags-aconfig-java", "android.speech.flags-aconfig-java", @@ -523,7 +523,10 @@ aconfig_declarations { package: "android.companion.virtualdevice.flags", container: "system", exportable: true, - srcs: ["core/java/android/companion/virtual/flags/*.aconfig"], + srcs: [ + "core/java/android/companion/virtual/flags/flags.aconfig", + "core/java/android/companion/virtual/flags/launched_flags.aconfig", + ], } java_aconfig_library { @@ -548,7 +551,7 @@ aconfig_declarations { name: "android.companion.virtual.flags-aconfig", package: "android.companion.virtual.flags", container: "system", - srcs: ["core/java/android/companion/virtual/*.aconfig"], + srcs: ["core/java/android/companion/virtual/flags/deprecated_flags_do_not_edit.aconfig"], } // InputMethod @@ -828,8 +831,8 @@ java_aconfig_library { min_sdk_version: "30", apex_available: [ "//apex_available:platform", - "com.android.permission", "com.android.nfcservices", + "com.android.permission", ], } diff --git a/apct-tests/perftests/aconfig/src/android/os/flagging/AconfigPackagePerfTest.java b/apct-tests/perftests/aconfig/src/android/os/flagging/AconfigPackagePerfTest.java index df6e3c836256..e790874ebc61 100644 --- a/apct-tests/perftests/aconfig/src/android/os/flagging/AconfigPackagePerfTest.java +++ b/apct-tests/perftests/aconfig/src/android/os/flagging/AconfigPackagePerfTest.java @@ -43,7 +43,7 @@ public class AconfigPackagePerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameterized.Parameters(name = "isPlatform={0}") + @Parameterized.Parameters(name = "isPlatform_{0}") public static Collection<Object[]> data() { return Arrays.asList(new Object[][] {{false}, {true}}); } @@ -60,10 +60,9 @@ public class AconfigPackagePerfTest { } } - @Parameterized.Parameter(0) - // if this variable is true, then the test query flags from system/product/vendor // if this variable is false, then the test query flags from updatable partitions + @Parameterized.Parameter(0) public boolean mIsPlatform; @Test diff --git a/apct-tests/perftests/core/src/android/app/OverlayManagerPerfTest.java b/apct-tests/perftests/core/src/android/app/OverlayManagerPerfTest.java index a12121fd13f7..5d39ccc882a8 100644 --- a/apct-tests/perftests/core/src/android/app/OverlayManagerPerfTest.java +++ b/apct-tests/perftests/core/src/android/app/OverlayManagerPerfTest.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertTrue; import android.content.Context; import android.content.om.OverlayManager; -import android.os.UserHandle; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.perftests.utils.TestPackageInstaller; @@ -127,7 +126,7 @@ public class OverlayManagerPerfTest { private void assertSetEnabled(boolean enabled, Context context, Stream<String> packagesStream) { final var overlayPackages = packagesStream.toList(); overlayPackages.forEach( - name -> sOverlayManager.setEnabled(name, enabled, UserHandle.SYSTEM)); + name -> sOverlayManager.setEnabled(name, enabled, context.getUser())); // Wait for the overlay changes to propagate final var endTime = System.nanoTime() + TimeUnit.SECONDS.toNanos(20); @@ -174,7 +173,7 @@ public class OverlayManagerPerfTest { // Disable the overlay and remove the idmap for the next iteration of the test state.pauseTiming(); assertSetEnabled(false, sContext, packageName); - sOverlayManager.invalidateCachesForOverlay(packageName, UserHandle.SYSTEM); + sOverlayManager.invalidateCachesForOverlay(packageName, sContext.getUser()); state.resumeTiming(); } } @@ -189,7 +188,7 @@ public class OverlayManagerPerfTest { // Disable the overlay and remove the idmap for the next iteration of the test state.pauseTiming(); assertSetEnabled(false, sContext, packageName); - sOverlayManager.invalidateCachesForOverlay(packageName, UserHandle.SYSTEM); + sOverlayManager.invalidateCachesForOverlay(packageName, sContext.getUser()); state.resumeTiming(); } } diff --git a/apct-tests/perftests/core/src/android/content/pm/SystemFeaturesPerfTest.java b/apct-tests/perftests/core/src/android/content/pm/SystemFeaturesPerfTest.java new file mode 100644 index 000000000000..43f545318124 --- /dev/null +++ b/apct-tests/perftests/core/src/android/content/pm/SystemFeaturesPerfTest.java @@ -0,0 +1,96 @@ +/* + * 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.content.pm; + +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; + +import androidx.test.filters.LargeTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.pm.RoSystemFeatures; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; + + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class SystemFeaturesPerfTest { + // As each query is relatively cheap, add an inner iteration loop to reduce execution noise. + private static final int NUM_ITERATIONS = 10; + + @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + @Test + public void hasSystemFeature_PackageManager() { + final PackageManager pm = + InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager(); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + for (int i = 0; i < NUM_ITERATIONS; ++i) { + pm.hasSystemFeature(PackageManager.FEATURE_WATCH); + pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); + pm.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS); + pm.hasSystemFeature(PackageManager.FEATURE_AUTOFILL); + pm.hasSystemFeature("com.android.custom.feature.1"); + pm.hasSystemFeature("foo"); + pm.hasSystemFeature(""); + } + } + } + + @Test + public void hasSystemFeature_SystemFeaturesCache() { + final PackageManager pm = + InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager(); + final SystemFeaturesCache cache = + new SystemFeaturesCache(Arrays.asList(pm.getSystemAvailableFeatures())); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + for (int i = 0; i < NUM_ITERATIONS; ++i) { + cache.maybeHasFeature(PackageManager.FEATURE_WATCH, 0); + cache.maybeHasFeature(PackageManager.FEATURE_LEANBACK, 0); + cache.maybeHasFeature(PackageManager.FEATURE_IPSEC_TUNNELS, 0); + cache.maybeHasFeature(PackageManager.FEATURE_AUTOFILL, 0); + cache.maybeHasFeature("com.android.custom.feature.1", 0); + cache.maybeHasFeature("foo", 0); + cache.maybeHasFeature("", 0); + } + } + } + + @Test + public void hasSystemFeature_RoSystemFeatures() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + for (int i = 0; i < NUM_ITERATIONS; ++i) { + RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0); + RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_LEANBACK, 0); + RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_IPSEC_TUNNELS, 0); + RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_AUTOFILL, 0); + RoSystemFeatures.maybeHasFeature("com.android.custom.feature.1", 0); + RoSystemFeatures.maybeHasFeature("foo", 0); + RoSystemFeatures.maybeHasFeature("", 0); + } + } + } +} diff --git a/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java b/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java index 2d2cf1c80e1e..b04d08f6795f 100644 --- a/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java +++ b/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java @@ -34,11 +34,20 @@ import android.view.WindowManagerGlobal; import org.junit.Rule; import org.junit.Test; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + /** Measure the performance of warm launch activity in the same task. */ public class InTaskTransitionTest extends WindowManagerPerfTestBase implements RemoteCallback.OnResultListener { private static final long TIMEOUT_MS = 5000; + private static final String LOG_SEPARATOR = "LOG_SEPARATOR"; @Rule public final PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter(); @@ -62,6 +71,7 @@ public class InTaskTransitionTest extends WindowManagerPerfTestBase final ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState(); long measuredTimeNs = 0; + long firstStartTime = 0; boolean readerStarted = false; while (state.keepRunning(measuredTimeNs)) { @@ -70,6 +80,10 @@ public class InTaskTransitionTest extends WindowManagerPerfTestBase readerStarted = true; } final long startTime = SystemClock.elapsedRealtimeNanos(); + if (readerStarted && firstStartTime == 0) { + firstStartTime = startTime; + executeShellCommand("log -t " + LOG_SEPARATOR + " " + firstStartTime); + } activity.startActivity(next); synchronized (mMetricsReader) { try { @@ -89,6 +103,7 @@ public class InTaskTransitionTest extends WindowManagerPerfTestBase state.addExtraResult("windowsDrawnDelayMs", metrics.mWindowsDrawnDelayMs); } } + addExtraTransitionInfo(firstStartTime, state); } @Override @@ -99,6 +114,46 @@ public class InTaskTransitionTest extends WindowManagerPerfTestBase } } + private void addExtraTransitionInfo(long startTime, ManualBenchmarkState state) { + final ProcessBuilder pb = new ProcessBuilder("sh"); + final String startLine = String.valueOf(startTime); + final String commitTimeStr = " commit="; + boolean foundStartLine = false; + try { + final Process process = pb.start(); + final InputStream in = process.getInputStream(); + final PrintWriter out = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(process.getOutputStream())), true /* autoFlush */); + out.println("logcat -v brief -d *:S WindowManager:V " + LOG_SEPARATOR + ":I" + + " | grep -e 'Finish Transition' -e " + LOG_SEPARATOR); + out.println("exit"); + + String line; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + while ((line = reader.readLine()) != null) { + if (!foundStartLine) { + if (line.contains(startLine)) { + foundStartLine = true; + } + continue; + } + final int strPos = line.indexOf(commitTimeStr); + if (strPos < 0) { + continue; + } + final int endPos = line.indexOf("ms", strPos); + if (endPos > strPos) { + final int commitDelayMs = Math.round(Float.parseFloat( + line.substring(strPos + commitTimeStr.length(), endPos))); + state.addExtraResult("commitDelayMs", commitDelayMs); + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + /** The test activity runs on a different process to trigger metrics logs. */ public static class TestActivity extends Activity implements Runnable { static final String CALLBACK = "callback"; diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig index 8b1a40c7f833..a0dfd1906938 100644 --- a/apex/jobscheduler/framework/aconfig/job.aconfig +++ b/apex/jobscheduler/framework/aconfig/job.aconfig @@ -17,14 +17,6 @@ flag { } flag { - name: "backup_jobs_exemption" - is_exported: true - namespace: "backstage_power" - description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content." - bug: "318731461" -} - -flag { name: "handle_abandoned_jobs" namespace: "backstage_power" description: "Detect, report and take action on jobs that maybe abandoned by the app without calling jobFinished." diff --git a/boot/preloaded-classes b/boot/preloaded-classes index b83bd4e4d401..9926aef91ee1 100644 --- a/boot/preloaded-classes +++ b/boot/preloaded-classes @@ -6470,6 +6470,7 @@ android.os.connectivity.WifiActivityEnergyInfo android.os.connectivity.WifiBatteryStats$1 android.os.connectivity.WifiBatteryStats android.os.flagging.AconfigPackage +android.os.flagging.PlatformAconfigPackage android.os.health.HealthKeys$Constant android.os.health.HealthKeys$Constants android.os.health.HealthKeys$SortedIntArray diff --git a/config/preloaded-classes b/config/preloaded-classes index e53c78f65877..bdd95f8e67ae 100644 --- a/config/preloaded-classes +++ b/config/preloaded-classes @@ -6474,6 +6474,7 @@ android.os.connectivity.WifiActivityEnergyInfo android.os.connectivity.WifiBatteryStats$1 android.os.connectivity.WifiBatteryStats android.os.flagging.AconfigPackage +android.os.flagging.PlatformAconfigPackage android.os.health.HealthKeys$Constant android.os.health.HealthKeys$Constants android.os.health.HealthKeys$SortedIntArray diff --git a/config/preloaded-classes-denylist b/config/preloaded-classes-denylist index e3e929cb00d9..a6a1d1680b7b 100644 --- a/config/preloaded-classes-denylist +++ b/config/preloaded-classes-denylist @@ -1,5 +1,4 @@ android.content.AsyncTaskLoader$LoadTask -android.media.MediaCodecInfo$CodecCapabilities$FeatureList android.net.ConnectivityThread$Singleton android.os.FileObserver android.os.NullVibrator diff --git a/core/api/current.txt b/core/api/current.txt index c4109392d6bd..6707c15de682 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -8905,7 +8905,7 @@ package android.app.appfunctions { @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>); + method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.content.pm.SigningInfo, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>); field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index f82aecbd6d44..93f311969c1e 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -581,7 +581,7 @@ package android.accessibilityservice { package android.accounts { public class AccountManager { - method @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.COPY_ACCOUNTS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public android.accounts.AccountManagerFuture<java.lang.Boolean> copyAccountToUser(@NonNull android.accounts.Account, @NonNull android.os.UserHandle, @NonNull android.os.UserHandle, @Nullable android.accounts.AccountManagerCallback<java.lang.Boolean>, @Nullable android.os.Handler); + method @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.COPY_ACCOUNTS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public android.accounts.AccountManagerFuture<java.lang.Boolean> copyAccountToUser(@NonNull android.accounts.Account, @NonNull android.os.UserHandle, @NonNull android.os.UserHandle, @Nullable android.os.Handler, @Nullable android.accounts.AccountManagerCallback<java.lang.Boolean>); method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public android.accounts.AccountManagerFuture<android.os.Bundle> finishSessionAsUser(android.os.Bundle, android.app.Activity, android.os.UserHandle, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler); } @@ -1290,7 +1290,6 @@ package android.app { public class WallpaperManager { method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public void clearWallpaper(int, int); - method @FlaggedApi("android.app.customization_packs_apis") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public android.util.SparseArray<android.graphics.Rect> getBitmapCrops(int); method @FlaggedApi("android.app.customization_packs_apis") public static int getOrientation(@NonNull android.graphics.Point); method @FloatRange(from=0.0f, to=1.0f) @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) public float getWallpaperDimAmount(); method @FlaggedApi("android.app.customization_packs_apis") @Nullable public android.os.ParcelFileDescriptor getWallpaperFile(int, boolean); @@ -8095,16 +8094,16 @@ package android.media.soundtrigger { method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void deleteModel(java.util.UUID); method public int getDetectionServiceOperationsTimeout(); method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.media.soundtrigger.SoundTriggerManager.Model getModel(java.util.UUID); - method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getModelState(@NonNull java.util.UUID); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public int getModelState(@NonNull java.util.UUID); method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModuleProperties getModuleProperties(); method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getParameter(@NonNull java.util.UUID, int); - method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public boolean isRecognitionActive(@NonNull java.util.UUID); - method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public boolean isRecognitionActive(@NonNull java.util.UUID); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel); method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModelParamRange queryParameter(@Nullable java.util.UUID, int); method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int setParameter(@Nullable java.util.UUID, int, int); - method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int startRecognition(@NonNull java.util.UUID, @Nullable android.os.Bundle, @NonNull android.content.ComponentName, @NonNull android.hardware.soundtrigger.SoundTrigger.RecognitionConfig); - method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int stopRecognition(@NonNull java.util.UUID); - method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int unloadSoundModel(@NonNull java.util.UUID); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public int startRecognition(@NonNull java.util.UUID, @Nullable android.os.Bundle, @NonNull android.content.ComponentName, @NonNull android.hardware.soundtrigger.SoundTrigger.RecognitionConfig); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public int stopRecognition(@NonNull java.util.UUID); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public int unloadSoundModel(@NonNull java.util.UUID); method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void updateModel(android.media.soundtrigger.SoundTriggerManager.Model); } @@ -15045,6 +15044,7 @@ package android.telephony { method public int getCellularIdentifier(); method public int getNasProtocolMessage(); method @NonNull public String getPlmn(); + method @FlaggedApi("com.android.internal.telephony.flags.vendor_specific_cellular_identifier_disclosure_indications") public boolean isBenign(); method public boolean isEmergency(); method public void writeToParcel(@NonNull android.os.Parcel, int); field public static final int CELLULAR_IDENTIFIER_IMEI = 2; // 0x2 @@ -15062,6 +15062,8 @@ package android.telephony { field public static final int NAS_PROTOCOL_MESSAGE_IMSI_DETACH_INDICATION = 11; // 0xb field public static final int NAS_PROTOCOL_MESSAGE_LOCATION_UPDATE_REQUEST = 5; // 0x5 field public static final int NAS_PROTOCOL_MESSAGE_REGISTRATION_REQUEST = 7; // 0x7 + field @FlaggedApi("com.android.internal.telephony.flags.vendor_specific_cellular_identifier_disclosure_indications") public static final int NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_FALSE = 12; // 0xc + field @FlaggedApi("com.android.internal.telephony.flags.vendor_specific_cellular_identifier_disclosure_indications") public static final int NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_TRUE = 13; // 0xd field public static final int NAS_PROTOCOL_MESSAGE_TRACKING_AREA_UPDATE_REQUEST = 4; // 0x4 field public static final int NAS_PROTOCOL_MESSAGE_UNKNOWN = 0; // 0x0 } @@ -18568,13 +18570,13 @@ package android.telephony.satellite { @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public final class SatelliteManager { method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void addAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); - method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getAttachRestrictionReasonsForCarrier(int); method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int[] getSatelliteDisallowedReasons(); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatellitePlmnsForCarrier(int); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); - method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @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 int registerForCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback); method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForCommunicationAccessStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCommunicationAccessStateCallback); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index cd2cc07b8cc3..a352d9d2ea06 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -536,6 +536,7 @@ package android.app { method @Nullable public android.graphics.Bitmap getBitmap(); method @Nullable public android.graphics.Bitmap getBitmapAsUser(int, boolean, int); method @FlaggedApi("com.android.window.flags.multi_crop") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public java.util.List<android.graphics.Rect> getBitmapCrops(@NonNull java.util.List<android.graphics.Point>, int, boolean); + method @FlaggedApi("android.app.customization_packs_apis") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public android.util.SparseArray<android.graphics.Rect> getBitmapCrops(int); method @FlaggedApi("com.android.window.flags.multi_crop") @NonNull public java.util.List<android.graphics.Rect> getBitmapCrops(@NonNull android.graphics.Point, @NonNull java.util.List<android.graphics.Point>, @Nullable java.util.Map<android.graphics.Point,android.graphics.Rect>); method public boolean isLockscreenLiveWallpaperEnabled(); method @Nullable public android.graphics.Rect peekBitmapDimensions(); @@ -3414,6 +3415,10 @@ package android.telecom { method public void onBindClient(@Nullable android.content.Intent); } + public class TelecomManager { + method @FlaggedApi("com.android.server.telecom.flags.voip_call_monitor_refactor") public boolean hasForegroundServiceDelegation(@Nullable android.telecom.PhoneAccountHandle); + } + } package android.telephony { @@ -4537,7 +4542,6 @@ package android.window { field public final int displayId; field public final boolean isDuplicateTouchToWallpaper; field public final boolean isFocusable; - field public final boolean isPreventSplitting; field public final boolean isTouchable; field public final boolean isTrustedOverlay; field public final boolean isVisible; diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java index 72450999993d..ddc1ae29f6df 100644 --- a/core/java/android/accounts/AccountManager.java +++ b/core/java/android/accounts/AccountManager.java @@ -30,7 +30,6 @@ import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.Size; -import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.UserHandleAware; @@ -2019,23 +2018,22 @@ public class AccountManager { * @param account the account to copy * @param fromUser the user to copy the account from * @param toUser the target user - * @param callback Callback to invoke when the request completes, - * null for no callback * @param handler {@link Handler} identifying the callback thread, * null for the main thread + * @param callback Callback to invoke when the request completes, + * null for no callback * @return An {@link AccountManagerFuture} which resolves to a Boolean indicated whether it * succeeded. * @hide */ - @SuppressLint("SamShouldBeLast") @NonNull @SystemApi @RequiresPermission(anyOf = {COPY_ACCOUNTS, INTERACT_ACROSS_USERS_FULL}) @FlaggedApi(FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED) public AccountManagerFuture<Boolean> copyAccountToUser( @NonNull final Account account, @NonNull final UserHandle fromUser, - @NonNull final UserHandle toUser, @Nullable AccountManagerCallback<Boolean> callback, - @Nullable Handler handler) { + @NonNull final UserHandle toUser, @Nullable Handler handler, + @Nullable AccountManagerCallback<Boolean> callback) { if (account == null) throw new IllegalArgumentException("account is null"); if (toUser == null || fromUser == null) { throw new IllegalArgumentException("fromUser and toUser cannot be null"); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index b198811416cd..4782205e3b21 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1027,9 +1027,6 @@ public class Activity extends ContextThemeWrapper /** The autofill client controller. Always access via {@link #getAutofillClientController()}. */ private AutofillClientController mAutofillClientController; - /** @hide */ - boolean mEnterAnimationComplete; - private boolean mIsInMultiWindowMode; /** @hide */ boolean mIsInPictureInPictureMode; @@ -2898,7 +2895,6 @@ public class Activity extends ContextThemeWrapper mCalled = true; getAutofillClientController().onActivityStopped(mIntent, mChangingConfigurations); - mEnterAnimationComplete = false; notifyVoiceInteractionManagerServiceActivityEvent( VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_STOP); @@ -8594,8 +8590,6 @@ public class Activity extends ContextThemeWrapper * @hide */ public void dispatchEnterAnimationComplete() { - mEnterAnimationComplete = true; - mInstrumentation.onEnterAnimationComplete(); onEnterAnimationComplete(); if (getWindow() != null && getWindow().getDecorView() != null) { View decorView = getWindow().getDecorView(); diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 999db18a1229..6151b8e2ef0a 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -142,6 +142,15 @@ public abstract class ActivityManagerInternal { String processName, String abiOverride, int uid, Runnable crashHandler); /** + * Called when a user is being deleted. This can happen during normal device usage + * or just at startup, when partially removed users are purged. Any state persisted by the + * ActivityManager should be purged now. + * + * @param userId The user being cleaned up. + */ + public abstract void onUserRemoving(@UserIdInt int userId); + + /** * Called when a user has been deleted. This can happen during normal device usage * or just at startup, when partially removed users are purged. Any state persisted by the * ActivityManager should be purged now. diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 7eacaac29d4b..b611acf79bc3 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -59,7 +59,6 @@ import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; -import android.view.SurfaceControl; import android.view.ViewConfiguration; import android.view.Window; import android.view.WindowManagerGlobal; @@ -137,7 +136,6 @@ public class Instrumentation { private PerformanceCollector mPerformanceCollector; private Bundle mPerfMetrics = new Bundle(); private UiAutomation mUiAutomation; - private final Object mAnimationCompleteLock = new Object(); @RavenwoodKeep public Instrumentation() { @@ -455,31 +453,6 @@ public class Instrumentation { idler.waitForIdle(); } - private void waitForEnterAnimationComplete(Activity activity) { - synchronized (mAnimationCompleteLock) { - long timeout = 5000; - try { - // We need to check that this specified Activity completed the animation, not just - // any Activity. If it was another Activity, then decrease the timeout by how long - // it's already waited and wait for the thread to wakeup again. - while (timeout > 0 && !activity.mEnterAnimationComplete) { - long startTime = System.currentTimeMillis(); - mAnimationCompleteLock.wait(timeout); - long totalTime = System.currentTimeMillis() - startTime; - timeout -= totalTime; - } - } catch (InterruptedException e) { - } - } - } - - /** @hide */ - public void onEnterAnimationComplete() { - synchronized (mAnimationCompleteLock) { - mAnimationCompleteLock.notifyAll(); - } - } - /** * Execute a call on the application's main thread, blocking until it is * complete. Useful for doing things that are not thread-safe, such as @@ -640,13 +613,14 @@ public class Instrumentation { activity = aw.activity; } - // Do not call this method within mSync, lest it could block the main thread. - waitForEnterAnimationComplete(activity); - - // Apply an empty transaction to ensure SF has a chance to update before - // the Activity is ready (b/138263890). - try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) { - t.apply(true); + // Typically, callers expect that the launched activity can receive input events after this + // method returns, so wait until a stable state, i.e. animation is finished and input info + // is updated. + try { + WindowManagerGlobal.getWindowManagerService() + .syncInputTransactions(true /* waitForAnimations */); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); } return activity; } diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index 67f7bee4028e..b5ac4e78c7ad 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -70,7 +70,6 @@ import com.android.internal.widget.VerifyCredentialResponse; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.Charset; -import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -1064,7 +1063,7 @@ public class KeyguardManager { Log.e(TAG, "Save lock exception", e); success = false; } finally { - Arrays.fill(password, (byte) 0); + LockPatternUtils.zeroize(password); } return success; } diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index 3d85ea6a1fca..ffd235f91e09 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -1129,6 +1129,10 @@ public final class LoadedApk { @UnsupportedAppUsage public ClassLoader getClassLoader() { + ClassLoader ret = mClassLoader; + if (ret != null) { + return ret; + } synchronized (mLock) { if (mClassLoader == null) { createOrUpdateClassLoaderLocked(null /*addedPaths*/); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 24594ab41100..c2ce7d511681 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -774,8 +774,9 @@ public class Notification implements Parcelable /** * Bit to be bitwise-ored into the {@link #flags} field that should be - * set by the system if this notification is a promoted ongoing notification, either via a - * user setting or allowlist. + * set by the system if this notification is a promoted ongoing notification, both because it + * {@link #hasPromotableCharacteristics()} and the user has not disabled the feature for this + * app. * * Applications cannot set this flag directly, but the posting app and * {@link android.service.notification.NotificationListenerService} can read it. @@ -1967,6 +1968,13 @@ public class Notification implements Parcelable @SystemApi public static final int SEMANTIC_ACTION_CONVERSATION_IS_PHISHING = 12; + /** + * {@link #extras} key to a boolean defining if this action requires special visual + * treatment. + * @hide + */ + public static final String EXTRA_IS_MAGIC = "android.extra.IS_MAGIC"; + private final Bundle mExtras; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private Icon mIcon; @@ -5855,7 +5863,9 @@ public class Notification implements Parcelable return null; } final int size = mContext.getResources().getDimensionPixelSize( - R.dimen.notification_badge_size); + Flags.notificationsRedesignTemplates() + ? R.dimen.notification_2025_badge_size + : R.dimen.notification_badge_size); Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); badge.setBounds(0, 0, size, size); @@ -5981,6 +5991,15 @@ public class Notification implements Parcelable } setHeaderlessVerticalMargins(contentView, p, hasSecondLine); + // Update margins to leave space for the top line (but not for headerless views like + // HUNS, which use a different layout that already accounts for that). + if (Flags.notificationsRedesignTemplates() && !p.mHeaderless) { + int margin = getContentMarginTop(mContext, + R.dimen.notification_2025_content_margin_top); + contentView.setViewLayoutMargin(R.id.notification_main_column, + RemoteViews.MARGIN_TOP, margin, TypedValue.COMPLEX_UNIT_PX); + } + return contentView; } @@ -6204,7 +6223,7 @@ public class Notification implements Parcelable int textColor = Colors.flattenAlpha(getPrimaryTextColor(p), pillColor); contentView.setInt(R.id.expand_button, "setDefaultTextColor", textColor); contentView.setInt(R.id.expand_button, "setDefaultPillColor", pillColor); - // Use different highlighted colors for e.g. unopened groups + // Use different highlighted colors for conversations' unread count if (p.mHighlightExpander) { pillColor = Colors.flattenAlpha( getColors(p).getTertiaryFixedDimAccentColor(), bgColor); @@ -6453,16 +6472,6 @@ public class Notification implements Parcelable big.setColorStateList(R.id.snooze_button, "setImageTintList", actionColor); big.setColorStateList(R.id.bubble_button, "setImageTintList", actionColor); - // Update margins to leave space for the top line (but not for HUNs, which use a - // different layout that already accounts for that). - if (Flags.notificationsRedesignTemplates() - && p.mViewType != StandardTemplateParams.VIEW_TYPE_HEADS_UP) { - int margin = getContentMarginTop(mContext, - R.dimen.notification_2025_content_margin_top); - big.setViewLayoutMargin(R.id.notification_main_column, RemoteViews.MARGIN_TOP, - margin, TypedValue.COMPLEX_UNIT_PX); - } - boolean validRemoteInput = false; // In the UI, contextual actions appear separately from the standard actions, so we @@ -6804,8 +6813,6 @@ public class Notification implements Parcelable public RemoteViews makeNotificationGroupHeader() { return makeNotificationHeader(mParams.reset() .viewType(StandardTemplateParams.VIEW_TYPE_GROUP_HEADER) - // Highlight group expander until the group is first opened - .highlightExpander(Flags.notificationsRedesignTemplates()) .fillTextsFrom(this)); } @@ -6981,14 +6988,12 @@ public class Notification implements Parcelable * @param useRegularSubtext uses the normal subtext set if there is one available. Otherwise * a new subtext is created consisting of the content of the * notification. - * @param highlightExpander whether the expander should use the highlighted colors * @hide */ - public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext, - boolean highlightExpander) { + public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext) { StandardTemplateParams p = mParams.reset() .viewType(StandardTemplateParams.VIEW_TYPE_MINIMIZED) - .highlightExpander(highlightExpander) + .highlightExpander(false) .fillTextsFrom(this); if (!useRegularSubtext || TextUtils.isEmpty(p.mSubText)) { p.summaryText(createSummaryText()); diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 08bd854525ec..aede8aa70ede 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -17,6 +17,7 @@ package android.app; import static android.Manifest.permission.POST_NOTIFICATIONS; +import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.service.notification.Flags.notificationClassification; @@ -50,6 +51,7 @@ import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.os.IpcDataCache; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; @@ -71,6 +73,8 @@ import android.util.LruCache; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.InstantSource; @@ -1202,12 +1206,20 @@ public class NotificationManager { * package (see {@link Context#createPackageContext(String, int)}).</p> */ public NotificationChannel getNotificationChannel(String channelId) { - INotificationManager service = service(); - try { - return service.getNotificationChannel(mContext.getOpPackageName(), - mContext.getUserId(), mContext.getPackageName(), channelId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return getChannelFromList(channelId, + mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId()))); + } else { + INotificationManager service = service(); + try { + return service.getNotificationChannel(mContext.getOpPackageName(), + mContext.getUserId(), mContext.getPackageName(), channelId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @@ -1222,13 +1234,21 @@ public class NotificationManager { */ public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId, @NonNull String conversationId) { - INotificationManager service = service(); - try { - return service.getConversationNotificationChannel(mContext.getOpPackageName(), - mContext.getUserId(), mContext.getPackageName(), channelId, true, - conversationId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return getConversationChannelFromList(channelId, conversationId, + mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId()))); + } else { + INotificationManager service = service(); + try { + return service.getConversationNotificationChannel(mContext.getOpPackageName(), + mContext.getUserId(), mContext.getPackageName(), channelId, true, + conversationId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @@ -1241,15 +1261,62 @@ public class NotificationManager { * {@link Context#createPackageContext(String, int)}).</p> */ public List<NotificationChannel> getNotificationChannels() { - INotificationManager service = service(); - try { - return service.getNotificationChannels(mContext.getOpPackageName(), - mContext.getPackageName(), mContext.getUserId()).getList(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId())); + } else { + INotificationManager service = service(); + try { + return service.getNotificationChannels(mContext.getOpPackageName(), + mContext.getPackageName(), mContext.getUserId()).getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } + // channel list assumed to be associated with the appropriate package & user id already. + private static NotificationChannel getChannelFromList(String channelId, + List<NotificationChannel> channels) { + if (channels == null) { + return null; + } + if (channelId == null) { + channelId = DEFAULT_CHANNEL_ID; + } + for (NotificationChannel channel : channels) { + if (channelId.equals(channel.getId())) { + return channel; + } + } + return null; + } + + private static NotificationChannel getConversationChannelFromList(String channelId, + String conversationId, List<NotificationChannel> channels) { + if (channels == null) { + return null; + } + if (channelId == null) { + channelId = DEFAULT_CHANNEL_ID; + } + if (conversationId == null) { + return getChannelFromList(channelId, channels); + } + NotificationChannel parent = null; + for (NotificationChannel channel : channels) { + if (conversationId.equals(channel.getConversationId()) + && channelId.equals(channel.getParentChannelId())) { + return channel; + } else if (channelId.equals(channel.getId())) { + parent = channel; + } + } + return parent; + } + /** * Deletes the given notification channel. * @@ -1328,6 +1395,71 @@ public class NotificationManager { } } + private static final String NOTIFICATION_CHANNEL_CACHE_API = "getNotificationChannel"; + private static final String NOTIFICATION_CHANNEL_LIST_CACHE_NAME = "getNotificationChannels"; + private static final int NOTIFICATION_CHANNEL_CACHE_SIZE = 10; + + private final IpcDataCache.QueryHandler<NotificationChannelQuery, List<NotificationChannel>> + mNotificationChannelListQueryHandler = new IpcDataCache.QueryHandler<>() { + @Override + public List<NotificationChannel> apply(NotificationChannelQuery query) { + INotificationManager service = service(); + try { + return service.getNotificationChannels(query.callingPkg, + query.targetPkg, query.userId).getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public boolean shouldBypassCache(@NonNull NotificationChannelQuery query) { + // Other locations should also not be querying the cache in the first place if + // the flag is not enabled, but this is an extra precaution. + if (!Flags.nmBinderPerfCacheChannels()) { + Log.wtf(TAG, + "shouldBypassCache called when nm_binder_perf_cache_channels off"); + return true; + } + return false; + } + }; + + private final IpcDataCache<NotificationChannelQuery, List<NotificationChannel>> + mNotificationChannelListCache = + new IpcDataCache<>(NOTIFICATION_CHANNEL_CACHE_SIZE, IpcDataCache.MODULE_SYSTEM, + NOTIFICATION_CHANNEL_CACHE_API, NOTIFICATION_CHANNEL_LIST_CACHE_NAME, + mNotificationChannelListQueryHandler); + + private record NotificationChannelQuery( + String callingPkg, + String targetPkg, + int userId) {} + + /** + * @hide + */ + public static void invalidateNotificationChannelCache() { + if (Flags.nmBinderPerfCacheChannels()) { + IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, + NOTIFICATION_CHANNEL_CACHE_API); + } else { + // if we are here, we have failed to flag something + Log.wtf(TAG, "invalidateNotificationChannelCache called without flag"); + } + } + + /** + * For testing only: running tests with a cache requires marking the cache's property for + * testing, as test APIs otherwise cannot invalidate the cache. This must be called after + * calling PropertyInvalidatedCache.setTestMode(true). + * @hide + */ + @VisibleForTesting + public void setChannelCacheToTestMode() { + mNotificationChannelListCache.testPropertyName(); + } + /** * @hide */ diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index 2e6f3e1c7f0a..57549847f05d 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -753,7 +753,7 @@ public class UiModeManager { * <p> * The mode can be one of: * <ul> - * <li><em>{@link #MODE_NIGHT_NO}<em> sets the device into + * <li><em>{@link #MODE_NIGHT_NO}</em> sets the device into * {@code notnight} mode</li> * <li><em>{@link #MODE_NIGHT_YES}</em> sets the device into * {@code night} mode</li> @@ -889,7 +889,7 @@ public class UiModeManager { * <p> * The mode can be one of: * <ul> - * <li><em>{@link #MODE_NIGHT_NO}<em> sets the device into + * <li><em>{@link #MODE_NIGHT_NO}</em> sets the device into * {@code notnight} mode</li> * <li><em>{@link #MODE_NIGHT_YES}</em> sets the device into * {@code night} mode</li> diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 360376da618c..73ecc7199686 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -1690,7 +1690,7 @@ public class WallpaperManager { * @hide */ @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS) - @SystemApi + @TestApi @RequiresPermission(READ_WALLPAPER_INTERNAL) @NonNull public SparseArray<Rect> getBitmapCrops(@SetWallpaperFlags int which) { diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index a2fddb045179..c50452157d74 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -4354,20 +4354,24 @@ public class DevicePolicyManager { } /** - * Indicates that app functions are not controlled by policy. + * Indicates that {@link android.app.appfunctions.AppFunctionManager} is not controlled by + * policy. * * <p>If no admin set this policy, it means appfunctions are enabled. */ @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) public static final int APP_FUNCTIONS_NOT_CONTROLLED_BY_POLICY = 0; - /** Indicates that app functions are controlled and disabled by a policy. */ + /** Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and + * disabled by policy, i.e. no apps in the current user are allowed to expose app functions. + */ @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) public static final int APP_FUNCTIONS_DISABLED = 1; /** - * Indicates that app functions are controlled and disabled by a policy for cross profile - * interactions only. + * Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and + * disabled by a policy for cross profile interactions only, i.e. app functions exposed by apps + * in the current user can only be invoked within the same user. * * <p>This is different from {@link #APP_FUNCTIONS_DISABLED} in that it only disables cross * profile interactions (even if the caller has permissions required to interact across users). @@ -4388,7 +4392,9 @@ public class DevicePolicyManager { public @interface AppFunctionsPolicy {} /** - * Sets the app functions policy which controls app functions operations on the device. + * Sets the {@link android.app.appfunctions.AppFunctionManager} policy which controls app + * functions operations on the device. An app function is a piece of functionality that apps + * expose to the system for cross-app orchestration. * * <p>This function can only be called by a device owner, a profile owner or holders of the * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_FUNCTIONS}. @@ -4414,7 +4420,7 @@ public class DevicePolicyManager { } /** - * Returns the current app functions policy. + * Returns the current {@link android.app.appfunctions.AppFunctionManager} policy. * * <p>The returned policy will be the current resolved policy rather than the policy set by the * calling admin. diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java index d86f1d841d33..8e48b4e56570 100644 --- a/core/java/android/app/appfunctions/AppFunctionService.java +++ b/core/java/android/app/appfunctions/AppFunctionService.java @@ -28,6 +28,7 @@ import android.annotation.SdkConstant; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.content.pm.SigningInfo; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; @@ -78,10 +79,10 @@ public abstract class AppFunctionService extends Service { void perform( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, + @NonNull SigningInfo callingPackageSigningInfo, @NonNull CancellationSignal cancellationSignal, @NonNull - OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> - callback); + OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback); } /** @hide */ @@ -93,6 +94,7 @@ public abstract class AppFunctionService extends Service { public void executeAppFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, + @NonNull SigningInfo callingPackageSigningInfo, @NonNull ICancellationCallback cancellationCallback, @NonNull IExecuteAppFunctionCallback callback) { if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE) @@ -105,6 +107,7 @@ public abstract class AppFunctionService extends Service { onExecuteFunction.perform( request, callingPackage, + callingPackageSigningInfo, buildCancellationSignal(cancellationCallback), new OutcomeReceiver<>() { @Override @@ -154,15 +157,17 @@ public abstract class AppFunctionService extends Service { /** * Called by the system to execute a specific app function. * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. + * <p>This method is the entry point for handling all app function requests in an app. When the + * system needs your AppFunctionService to perform a function, it will invoke this method. * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * <p>Each function you've registered is identified by a unique identifier. This identifier + * doesn't need to be globally unique, but it must be unique within your app. For example, a + * function to order food could be identified as "orderFood". In most cases, this identifier is + * automatically generated by the AppFunctions SDK. + * + * <p>You can determine which function to execute by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. This allows your service to route the + * incoming request to the appropriate logic for handling the specific function. * * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker * thread and dispatch the result with the given callback. You should always report back the @@ -173,6 +178,8 @@ public abstract class AppFunctionService extends Service { * * @param request The function execution request. * @param callingPackage The package name of the app that is requesting the execution. + * @param callingPackageSigningInfo The signing information of the app that is requesting the + * execution. * @param cancellationSignal A signal to cancel the execution. * @param callback A callback to report back the result or error. */ @@ -180,10 +187,9 @@ public abstract class AppFunctionService extends Service { public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, + @NonNull SigningInfo callingPackageSigningInfo, @NonNull CancellationSignal cancellationSignal, - @NonNull - OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> - callback); + @NonNull OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback); /** * Returns result codes from throwable. diff --git a/core/java/android/app/appfunctions/IAppFunctionService.aidl b/core/java/android/app/appfunctions/IAppFunctionService.aidl index bf935d2a102b..78bcb7f66eb1 100644 --- a/core/java/android/app/appfunctions/IAppFunctionService.aidl +++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl @@ -35,12 +35,15 @@ oneway interface IAppFunctionService { * * @param request the function execution request. * @param callingPackage The package name of the app that is requesting the execution. + * @param callingPackageSigningInfo The signing information of the app that is requesting the + * execution. * @param cancellationCallback a callback to send back the cancellation transport. * @param callback a callback to report back the result. */ void executeAppFunction( in ExecuteAppFunctionRequest request, in String callingPackage, + in android.content.pm.SigningInfo callingPackageSigningInfo, in ICancellationCallback cancellationCallback, in IExecuteAppFunctionCallback callback ); diff --git a/core/java/android/app/backup/BackupManagerInternal.java b/core/java/android/app/backup/BackupManagerInternal.java new file mode 100644 index 000000000000..ceb5ae01f177 --- /dev/null +++ b/core/java/android/app/backup/BackupManagerInternal.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 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.backup; + +import android.annotation.UserIdInt; +import android.os.IBinder; + +/** + * Local system service interface for {@link com.android.server.backup.BackupManagerService}. + * + * @hide Only for use within the system server. + */ +public interface BackupManagerInternal { + + /** + * Notifies the Backup Manager Service that an agent has become available. This + * method is only invoked by the Activity Manager. + */ + void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent); + + /** + * Notify the Backup Manager Service that an agent has unexpectedly gone away. + * This method is only invoked by the Activity Manager. + */ + void agentDisconnectedForUser(String packageName, @UserIdInt int userId); +} diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index 041c2a7c09f4..5d01d72c35a0 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -93,38 +93,6 @@ interface IBackupManager { IBackupObserver observer); /** - * Notifies the Backup Manager Service that an agent has become available. This - * method is only invoked by the Activity Manager. - * - * If {@code userId} is different from the calling user id, then the caller must hold the - * android.permission.INTERACT_ACROSS_USERS_FULL permission. - * - * @param userId User id for which an agent has become available. - */ - void agentConnectedForUser(int userId, String packageName, IBinder agent); - - /** - * {@link android.app.backup.IBackupManager.agentConnected} for the calling user id. - */ - void agentConnected(String packageName, IBinder agent); - - /** - * Notify the Backup Manager Service that an agent has unexpectedly gone away. - * This method is only invoked by the Activity Manager. - * - * If {@code userId} is different from the calling user id, then the caller must hold the - * android.permission.INTERACT_ACROSS_USERS_FULL permission. - * - * @param userId User id for which an agent has unexpectedly gone away. - */ - void agentDisconnectedForUser(int userId, String packageName); - - /** - * {@link android.app.backup.IBackupManager.agentDisconnected} for the calling user id. - */ - void agentDisconnected(String packageName); - - /** * Notify the Backup Manager Service that an application being installed will * need a data-restore pass. This method is only invoked by the Package Manager. * diff --git a/core/java/android/app/time/TimeZoneCapabilities.java b/core/java/android/app/time/TimeZoneCapabilities.java index 4dee15988795..929f66079a3d 100644 --- a/core/java/android/app/time/TimeZoneCapabilities.java +++ b/core/java/android/app/time/TimeZoneCapabilities.java @@ -55,7 +55,8 @@ public final class TimeZoneCapabilities implements Parcelable { * The user the capabilities are for. This is used for object equality and debugging but there * is no accessor. */ - @NonNull private final UserHandle mUserHandle; + @NonNull + private final UserHandle mUserHandle; private final @CapabilityState int mConfigureAutoDetectionEnabledCapability; /** @@ -69,6 +70,7 @@ public final class TimeZoneCapabilities implements Parcelable { private final @CapabilityState int mConfigureGeoDetectionEnabledCapability; private final @CapabilityState int mSetManualTimeZoneCapability; + private final @CapabilityState int mConfigureNotificationsEnabledCapability; private TimeZoneCapabilities(@NonNull Builder builder) { this.mUserHandle = Objects.requireNonNull(builder.mUserHandle); @@ -78,6 +80,8 @@ public final class TimeZoneCapabilities implements Parcelable { this.mConfigureGeoDetectionEnabledCapability = builder.mConfigureGeoDetectionEnabledCapability; this.mSetManualTimeZoneCapability = builder.mSetManualTimeZoneCapability; + this.mConfigureNotificationsEnabledCapability = + builder.mConfigureNotificationsEnabledCapability; } @NonNull @@ -88,6 +92,7 @@ public final class TimeZoneCapabilities implements Parcelable { .setUseLocationEnabled(in.readBoolean()) .setConfigureGeoDetectionEnabledCapability(in.readInt()) .setSetManualTimeZoneCapability(in.readInt()) + .setConfigureNotificationsEnabledCapability(in.readInt()) .build(); } @@ -98,6 +103,7 @@ public final class TimeZoneCapabilities implements Parcelable { dest.writeBoolean(mUseLocationEnabled); dest.writeInt(mConfigureGeoDetectionEnabledCapability); dest.writeInt(mSetManualTimeZoneCapability); + dest.writeInt(mConfigureNotificationsEnabledCapability); } /** @@ -117,8 +123,8 @@ public final class TimeZoneCapabilities implements Parcelable { * * Not part of the SDK API because it is intended for use by SettingsUI, which can display * text about needing it to be on for location-based time zone detection. - * @hide * + * @hide */ public boolean isUseLocationEnabled() { return mUseLocationEnabled; @@ -148,6 +154,18 @@ public final class TimeZoneCapabilities implements Parcelable { } /** + * Returns the capability state associated with the user's ability to modify the time zone + * notification setting. The setting can be updated via {@link + * TimeManager#updateTimeZoneConfiguration(TimeZoneConfiguration)}. + * + * @hide + */ + @CapabilityState + public int getConfigureNotificationsEnabledCapability() { + return mConfigureNotificationsEnabledCapability; + } + + /** * Tries to create a new {@link TimeZoneConfiguration} from the {@code config} and the set of * {@code requestedChanges}, if {@code this} capabilities allow. The new configuration is * returned. If the capabilities do not permit one or more of the requested changes then {@code @@ -174,6 +192,12 @@ public final class TimeZoneCapabilities implements Parcelable { newConfigBuilder.setGeoDetectionEnabled(requestedChanges.isGeoDetectionEnabled()); } + if (requestedChanges.hasIsNotificationsEnabled()) { + if (this.getConfigureNotificationsEnabledCapability() < CAPABILITY_NOT_APPLICABLE) { + return null; + } + newConfigBuilder.setNotificationsEnabled(requestedChanges.areNotificationsEnabled()); + } return newConfigBuilder.build(); } @@ -197,13 +221,16 @@ public final class TimeZoneCapabilities implements Parcelable { && mUseLocationEnabled == that.mUseLocationEnabled && mConfigureGeoDetectionEnabledCapability == that.mConfigureGeoDetectionEnabledCapability - && mSetManualTimeZoneCapability == that.mSetManualTimeZoneCapability; + && mSetManualTimeZoneCapability == that.mSetManualTimeZoneCapability + && mConfigureNotificationsEnabledCapability + == that.mConfigureNotificationsEnabledCapability; } @Override public int hashCode() { return Objects.hash(mUserHandle, mConfigureAutoDetectionEnabledCapability, - mConfigureGeoDetectionEnabledCapability, mSetManualTimeZoneCapability); + mConfigureGeoDetectionEnabledCapability, mSetManualTimeZoneCapability, + mConfigureNotificationsEnabledCapability); } @Override @@ -216,6 +243,8 @@ public final class TimeZoneCapabilities implements Parcelable { + ", mConfigureGeoDetectionEnabledCapability=" + mConfigureGeoDetectionEnabledCapability + ", mSetManualTimeZoneCapability=" + mSetManualTimeZoneCapability + + ", mConfigureNotificationsEnabledCapability=" + + mConfigureNotificationsEnabledCapability + '}'; } @@ -226,11 +255,13 @@ public final class TimeZoneCapabilities implements Parcelable { */ public static class Builder { - @NonNull private UserHandle mUserHandle; + @NonNull + private UserHandle mUserHandle; private @CapabilityState int mConfigureAutoDetectionEnabledCapability; private Boolean mUseLocationEnabled; private @CapabilityState int mConfigureGeoDetectionEnabledCapability; private @CapabilityState int mSetManualTimeZoneCapability; + private @CapabilityState int mConfigureNotificationsEnabledCapability; public Builder(@NonNull UserHandle userHandle) { mUserHandle = Objects.requireNonNull(userHandle); @@ -240,12 +271,14 @@ public final class TimeZoneCapabilities implements Parcelable { Objects.requireNonNull(capabilitiesToCopy); mUserHandle = capabilitiesToCopy.mUserHandle; mConfigureAutoDetectionEnabledCapability = - capabilitiesToCopy.mConfigureAutoDetectionEnabledCapability; + capabilitiesToCopy.mConfigureAutoDetectionEnabledCapability; mUseLocationEnabled = capabilitiesToCopy.mUseLocationEnabled; mConfigureGeoDetectionEnabledCapability = - capabilitiesToCopy.mConfigureGeoDetectionEnabledCapability; + capabilitiesToCopy.mConfigureGeoDetectionEnabledCapability; mSetManualTimeZoneCapability = - capabilitiesToCopy.mSetManualTimeZoneCapability; + capabilitiesToCopy.mSetManualTimeZoneCapability; + mConfigureNotificationsEnabledCapability = + capabilitiesToCopy.mConfigureNotificationsEnabledCapability; } /** Sets the value for the "configure automatic time zone detection enabled" capability. */ @@ -274,6 +307,14 @@ public final class TimeZoneCapabilities implements Parcelable { return this; } + /** + * Sets the value for the "configure time notifications enabled" capability. + */ + public Builder setConfigureNotificationsEnabledCapability(@CapabilityState int value) { + this.mConfigureNotificationsEnabledCapability = value; + return this; + } + /** Returns the {@link TimeZoneCapabilities}. */ @NonNull public TimeZoneCapabilities build() { @@ -283,7 +324,9 @@ public final class TimeZoneCapabilities implements Parcelable { verifyCapabilitySet(mConfigureGeoDetectionEnabledCapability, "configureGeoDetectionEnabledCapability"); verifyCapabilitySet(mSetManualTimeZoneCapability, - "mSetManualTimeZoneCapability"); + "setManualTimeZoneCapability"); + verifyCapabilitySet(mConfigureNotificationsEnabledCapability, + "configureNotificationsEnabledCapability"); return new TimeZoneCapabilities(this); } diff --git a/core/java/android/app/time/TimeZoneConfiguration.java b/core/java/android/app/time/TimeZoneConfiguration.java index 7403c129f4dc..68c090f6dde3 100644 --- a/core/java/android/app/time/TimeZoneConfiguration.java +++ b/core/java/android/app/time/TimeZoneConfiguration.java @@ -62,7 +62,8 @@ public final class TimeZoneConfiguration implements Parcelable { * * @hide */ - @StringDef({ SETTING_AUTO_DETECTION_ENABLED, SETTING_GEO_DETECTION_ENABLED }) + @StringDef({SETTING_AUTO_DETECTION_ENABLED, SETTING_GEO_DETECTION_ENABLED, + SETTING_NOTIFICATIONS_ENABLED}) @Retention(RetentionPolicy.SOURCE) @interface Setting {} @@ -74,6 +75,10 @@ public final class TimeZoneConfiguration implements Parcelable { @Setting private static final String SETTING_GEO_DETECTION_ENABLED = "geoDetectionEnabled"; + /** See {@link TimeZoneConfiguration#areNotificationsEnabled()} for details. */ + @Setting + private static final String SETTING_NOTIFICATIONS_ENABLED = "notificationsEnabled"; + @NonNull private final Bundle mBundle; private TimeZoneConfiguration(Builder builder) { @@ -98,7 +103,8 @@ public final class TimeZoneConfiguration implements Parcelable { */ public boolean isComplete() { return hasIsAutoDetectionEnabled() - && hasIsGeoDetectionEnabled(); + && hasIsGeoDetectionEnabled() + && hasIsNotificationsEnabled(); } /** @@ -128,8 +134,7 @@ public final class TimeZoneConfiguration implements Parcelable { /** * Returns the value of the {@link #SETTING_GEO_DETECTION_ENABLED} setting. This * controls whether the device can use geolocation to determine time zone. This value may only - * be used by Android under some circumstances. For example, it is not used when - * {@link #isGeoDetectionEnabled()} is {@code false}. + * be used by Android under some circumstances. * * <p>See {@link TimeZoneCapabilities#getConfigureGeoDetectionEnabledCapability()} for how to * tell if the setting is meaningful for the current user at this time. @@ -150,6 +155,32 @@ public final class TimeZoneConfiguration implements Parcelable { return mBundle.containsKey(SETTING_GEO_DETECTION_ENABLED); } + /** + * Returns the value of the {@link #SETTING_NOTIFICATIONS_ENABLED} setting. This controls + * whether the device can send time and time zone related notifications. This value may only + * be used by Android under some circumstances. + * + * <p>See {@link TimeZoneCapabilities#getConfigureNotificationsEnabledCapability()} ()} for how + * to tell if the setting is meaningful for the current user at this time. + * + * @throws IllegalStateException if the setting is not present + * + * @hide + */ + public boolean areNotificationsEnabled() { + enforceSettingPresent(SETTING_NOTIFICATIONS_ENABLED); + return mBundle.getBoolean(SETTING_NOTIFICATIONS_ENABLED); + } + + /** + * Returns {@code true} if the {@link #areNotificationsEnabled()} setting is present. + * + * @hide + */ + public boolean hasIsNotificationsEnabled() { + return mBundle.containsKey(SETTING_NOTIFICATIONS_ENABLED); + } + @Override public int describeContents() { return 0; @@ -244,6 +275,17 @@ public final class TimeZoneConfiguration implements Parcelable { return this; } + /** + * Sets the state of the {@link #SETTING_NOTIFICATIONS_ENABLED} setting. * + * + * @hide + */ + @NonNull + public Builder setNotificationsEnabled(boolean enabled) { + this.mBundle.putBoolean(SETTING_NOTIFICATIONS_ENABLED, enabled); + return this; + } + /** Returns the {@link TimeZoneConfiguration}. */ @NonNull public TimeZoneConfiguration build() { diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java index 316d129bd6b9..971d402569c0 100644 --- a/core/java/android/companion/CompanionDeviceService.java +++ b/core/java/android/companion/CompanionDeviceService.java @@ -62,10 +62,11 @@ import java.util.concurrent.Executor; * * <p> * If the companion application has requested observing device presence (see - * {@link CompanionDeviceManager#startObservingDevicePresence(String)}) the system will - * <a href="https://developer.android.com/guide/components/bound-services"> bind the service</a> - * when it detects the device nearby (for BLE devices) or when the device is connected - * (for Bluetooth devices). + * {@link CompanionDeviceManager#stopObservingDevicePresence(ObservingDevicePresenceRequest)}) + * the system will <a href="https://developer.android.com/guide/components/bound-services"> + * bind the service</a> when one of the {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified. * * <p> * The system binding {@link CompanionDeviceService} elevates the priority of the process that @@ -102,15 +103,25 @@ public abstract class CompanionDeviceService extends Service { /** * An intent action for a service to be bound whenever this app's companion device(s) - * are nearby. + * are nearby or self-managed device(s) report app appeared. * - * <p>The app will be kept alive for as long as the device is nearby or companion app reports - * appeared. - * If the app is not running at the time device gets connected, the app will be woken up.</p> + * <p>The app will be kept bound by the system when one of the + * {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified. * - * <p>Shortly after the device goes out of range or the companion app reports disappeared, - * the service will be unbound, and the app will be eligible for cleanup, unless any other - * user-visible components are running.</p> + * If the app is not running when one of the + * {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified, the app will be + * kept bound by the system.</p> + * + * <p>Shortly, the service will be unbound if both + * {@link DevicePresenceEvent#EVENT_BLE_DISAPPEARED} and + * {@link DevicePresenceEvent#EVENT_BT_DISCONNECTED} are notified, or + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_DISAPPEARED} event is notified. + * The app will be eligible for cleanup, unless any other user-visible components are + * running.</p> * * If running in background is not essential for the devices that this app can manage, * app should avoid declaring this service.</p> diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java index 42c74414ecd9..311e24ba6254 100644 --- a/core/java/android/companion/virtual/VirtualDeviceInternal.java +++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java @@ -83,7 +83,6 @@ import java.util.function.IntConsumer; public class VirtualDeviceInternal { private final Context mContext; - private final IVirtualDeviceManager mService; private final IVirtualDevice mVirtualDevice; private final Object mActivityListenersLock = new Object(); @GuardedBy("mActivityListenersLock") @@ -206,7 +205,6 @@ public class VirtualDeviceInternal { Context context, int associationId, VirtualDeviceParams params) throws RemoteException { - mService = service; mContext = context.getApplicationContext(); mVirtualDevice = service.createVirtualDevice( new Binder(), @@ -217,11 +215,7 @@ public class VirtualDeviceInternal { mSoundEffectListener); } - VirtualDeviceInternal( - IVirtualDeviceManager service, - Context context, - IVirtualDevice virtualDevice) { - mService = service; + VirtualDeviceInternal(Context context, IVirtualDevice virtualDevice) { mContext = context.getApplicationContext(); mVirtualDevice = virtualDevice; try { diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index ed2fd99c55c5..73ea9f0462d5 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -577,9 +577,8 @@ public final class VirtualDeviceManager { } /** @hide */ - public VirtualDevice(IVirtualDeviceManager service, Context context, - IVirtualDevice virtualDevice) { - mVirtualDeviceInternal = new VirtualDeviceInternal(service, context, virtualDevice); + public VirtualDevice(Context context, IVirtualDevice virtualDevice) { + mVirtualDeviceInternal = new VirtualDeviceInternal(context, virtualDevice); } /** diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags/deprecated_flags_do_not_edit.aconfig index 46da4a3d99bc..eae50624539e 100644 --- a/core/java/android/companion/virtual/flags.aconfig +++ b/core/java/android/companion/virtual/flags/deprecated_flags_do_not_edit.aconfig @@ -1,24 +1,18 @@ -# Do not add new flags to this file. +# Do not modify this file. # -# Due to "virtual" keyword in the package name flags -# added to this file cannot be accessed from C++ -# code. +# Due to "virtual" keyword in the package name flags added to this file cannot +# be accessed from C++ code. # # Use frameworks/base/core/java/android/companion/virtual/flags/flags.aconfig -# instead. +# instead for new flags. +# +# All of the remaining flags here have been used for API flagging and are +# therefore exported and should not be deleted. package: "android.companion.virtual.flags" container: "system" flag { - name: "enable_native_vdm" - namespace: "virtual_devices" - description: "Enable native VDM service" - bug: "303535376" - is_fixed_read_only: true -} - -flag { name: "dynamic_policy" is_exported: true namespace: "virtual_devices" diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index de01280f293f..84af84072f1b 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -1,17 +1,11 @@ +# VirtualDeviceManager flags # -# Copyright (C) 2023 The Android Open Source Project +# This file contains flags guarding features that are in development. # -# 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. +# Once a flag is launched or abandoned and there are no more references to it in +# the codebase, it should be either: +# - deleted, or +# - moved to launched_flags.aconfig if it was launched and used for API flagging. package: "android.companion.virtualdevice.flags" container: "system" diff --git a/core/java/android/companion/virtual/flags/launched_flags.aconfig b/core/java/android/companion/virtual/flags/launched_flags.aconfig new file mode 100644 index 000000000000..ee896319bb72 --- /dev/null +++ b/core/java/android/companion/virtual/flags/launched_flags.aconfig @@ -0,0 +1,6 @@ +# This file contains the launched VirtualDeviceManager flags from the +# "android.companion.virtualdevice.flags" package that cannot be deleted because +# they have been used for API flagging. + +package: "android.companion.virtualdevice.flags" +container: "system" diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 3d75423edfa9..350048df3112 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -12394,35 +12394,57 @@ public class Intent implements Parcelable, Cloneable { * @hide */ public void collectExtraIntentKeys() { + collectExtraIntentKeys(false); + } + + /** + * Collects keys in the extra bundle whose value are intents. + * With these keys collected on the client side, the system server would only unparcel values + * of these keys and create IntentCreatorToken for them. + * This method could also be called from the system server side as a catch all safty net in case + * these keys are not collected on the client side. In that case, call it with forceUnparcel set + * to true since everything is parceled on the system server side. + * + * @param forceUnparcel if it is true, unparcel everything to determine if an object is an + * intent. Otherwise, do not unparcel anything. + * @hide + */ + public void collectExtraIntentKeys(boolean forceUnparcel) { if (preventIntentRedirect()) { - collectNestedIntentKeysRecur(new ArraySet<>()); + collectNestedIntentKeysRecur(new ArraySet<>(), forceUnparcel); } } - private void collectNestedIntentKeysRecur(Set<Intent> visited) { + private void collectNestedIntentKeysRecur(Set<Intent> visited, boolean forceUnparcel) { addExtendedFlags(EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED); - if (mExtras != null && !mExtras.isEmpty()) { + if (mExtras != null && (forceUnparcel || !mExtras.isParcelled()) && !mExtras.isEmpty()) { for (String key : mExtras.keySet()) { Object value; try { - value = mExtras.get(key); + // Do not unparcel any Parcelable objects. It may cause issues for app who would + // change class loader before it reads a parceled value. b/382633789. + // It is okay to not collect a parceled intent since it would have been + // coming from another process and collected by its containing intent already + // in that process. + if (forceUnparcel || !mExtras.isValueParceled(key)) { + value = mExtras.get(key); + } else { + value = null; + } } catch (BadParcelableException e) { - // This could happen when the key points to a LazyValue whose class cannot be - // found by the classLoader - A nested object more than 1 level deeper who is - // of type of a custom class could trigger this situation. In such case, we - // ignore it since it is not an intent. However, it could be a custom type that - // extends from Intent. If such an object is retrieved later in another - // component, then trying to launch such a custom class object will fail unless - // removeLaunchSecurityProtection() is called before it is launched. + // This may still happen if the keys are collected on the system server side, in + // which case, we will try to unparcel everything. If this happens, simply + // ignore it since it is not an intent anyway. value = null; } if (value instanceof Intent intent) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0), + forceUnparcel); } else if (value instanceof Parcelable[] parcelables) { - handleParcelableArray(parcelables, key, visited); + handleParcelableArray(parcelables, key, visited, forceUnparcel); } else if (value instanceof ArrayList<?> parcelables) { - handleParcelableList(parcelables, key, visited); + handleParcelableList(parcelables, key, visited, forceUnparcel); } } } @@ -12432,13 +12454,15 @@ public class Intent implements Parcelable, Cloneable { Intent intent = mClipData.getItemAt(i).mIntent; if (intent != null && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i), + forceUnparcel); } } } } - private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key) { + private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key, + boolean forceUnparcel) { if (mCreatorTokenInfo == null) { mCreatorTokenInfo = new CreatorTokenInfo(); } @@ -12448,24 +12472,28 @@ public class Intent implements Parcelable, Cloneable { mCreatorTokenInfo.mNestedIntentKeys.add(key); if (!visited.contains(intent)) { visited.add(intent); - intent.collectNestedIntentKeysRecur(visited); + intent.collectNestedIntentKeysRecur(visited, forceUnparcel); } } - private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited) { + private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited, + boolean forceUnparcel) { for (int i = 0; i < parcelables.length; i++) { if (parcelables[i] instanceof Intent intent && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i), + forceUnparcel); } } } - private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited) { + private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited, + boolean forceUnparcel) { for (int i = 0; i < parcelables.size(); i++) { if (parcelables.get(i) instanceof Intent intent && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i), + forceUnparcel); } } } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index c16582f19c9b..8c7e93a834b7 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -4649,6 +4649,7 @@ public abstract class PackageManager { * the Android Keystore backed by an isolated execution environment. The version indicates * which features are implemented in the isolated execution environment: * <ul> + * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record. * <li>300: Ability to include a second IMEI in the ID attestation record, see * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}. * <li>200: Hardware support for Curve 25519 (including both Ed25519 signature generation and @@ -4682,6 +4683,7 @@ public abstract class PackageManager { * StrongBox</a>. If this feature has a version, the version number indicates which features are * implemented in StrongBox: * <ul> + * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record. * <li>300: Ability to include a second IMEI in the ID attestation record, see * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}. * <li>200: No new features for StrongBox (the Android Keystore environment backed by an diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index 0333942b7f3e..9d11710a2cad 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -17,6 +17,7 @@ package android.content.pm; import android.Manifest; +import android.annotation.NonNull; import android.compat.annotation.UnsupportedAppUsage; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -30,6 +31,7 @@ import android.os.Environment; import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; +import android.util.ArrayMap; import android.util.AtomicFile; import android.util.AttributeSet; import android.util.IntArray; @@ -45,11 +47,11 @@ import com.android.internal.util.ArrayUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; -import libcore.io.IoUtils; - import com.google.android.collect.Lists; import com.google.android.collect.Maps; +import libcore.io.IoUtils; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -94,6 +96,9 @@ public abstract class RegisteredServicesCache<V> { @GuardedBy("mServicesLock") private final SparseArray<UserServices<V>> mUserServices = new SparseArray<UserServices<V>>(2); + @GuardedBy("mServicesLock") + private final ArrayMap<String, ServiceInfo<V>> mServiceInfoCaches = new ArrayMap<>(); + private static class UserServices<V> { @GuardedBy("mServicesLock") final Map<V, Integer> persistentServices = Maps.newHashMap(); @@ -323,13 +328,16 @@ public abstract class RegisteredServicesCache<V> { public final ComponentName componentName; @UnsupportedAppUsage public final int uid; + public final long lastUpdateTime; /** @hide */ - public ServiceInfo(V type, ComponentInfo componentInfo, ComponentName componentName) { + public ServiceInfo(V type, ComponentInfo componentInfo, ComponentName componentName, + long lastUpdateTime) { this.type = type; this.componentInfo = componentInfo; this.componentName = componentName; this.uid = (componentInfo != null) ? componentInfo.applicationInfo.uid : -1; + this.lastUpdateTime = lastUpdateTime; } @Override @@ -490,7 +498,7 @@ public abstract class RegisteredServicesCache<V> { final List<ResolveInfo> resolveInfos = queryIntentServices(userId); for (ResolveInfo resolveInfo : resolveInfos) { try { - ServiceInfo<V> info = parseServiceInfo(resolveInfo); + ServiceInfo<V> info = parseServiceInfo(resolveInfo, userId); if (info == null) { Log.w(TAG, "Unable to load service info " + resolveInfo.toString()); continue; @@ -638,13 +646,31 @@ public abstract class RegisteredServicesCache<V> { } @VisibleForTesting - protected ServiceInfo<V> parseServiceInfo(ResolveInfo service) + protected ServiceInfo<V> parseServiceInfo(ResolveInfo service, int userId) throws XmlPullParserException, IOException { android.content.pm.ServiceInfo si = service.serviceInfo; ComponentName componentName = new ComponentName(si.packageName, si.name); PackageManager pm = mContext.getPackageManager(); + // Check if the service has been in the service cache. + long lastUpdateTime = -1; + if (Flags.optimizeParsingInRegisteredServicesCache()) { + try { + PackageInfo packageInfo = pm.getPackageInfoAsUser(si.packageName, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); + lastUpdateTime = packageInfo.lastUpdateTime; + + ServiceInfo<V> serviceInfo = getServiceInfoFromServiceCache(si, lastUpdateTime); + if (serviceInfo != null) { + return serviceInfo; + } + } catch (NameNotFoundException | SecurityException e) { + Slog.d(TAG, "Fail to get the PackageInfo in parseServiceInfo: " + e); + } + } + XmlResourceParser parser = null; try { parser = si.loadXmlMetaData(pm, mMetaDataName); @@ -670,8 +696,13 @@ public abstract class RegisteredServicesCache<V> { if (v == null) { return null; } - final android.content.pm.ServiceInfo serviceInfo = service.serviceInfo; - return new ServiceInfo<V>(v, serviceInfo, componentName); + ServiceInfo<V> serviceInfo = new ServiceInfo<V>(v, si, componentName, lastUpdateTime); + if (Flags.optimizeParsingInRegisteredServicesCache()) { + synchronized (mServicesLock) { + mServiceInfoCaches.put(getServiceCacheKey(si), serviceInfo); + } + } + return serviceInfo; } catch (NameNotFoundException e) { throw new XmlPullParserException( "Unable to load resources for pacakge " + si.packageName); @@ -841,4 +872,28 @@ public abstract class RegisteredServicesCache<V> { mContext.unregisterReceiver(mExternalReceiver); mContext.unregisterReceiver(mUserRemovedReceiver); } + + private static String getServiceCacheKey(@NonNull android.content.pm.ServiceInfo serviceInfo) { + StringBuilder sb = new StringBuilder(serviceInfo.packageName); + sb.append('-'); + sb.append(serviceInfo.name); + return sb.toString(); + } + + private ServiceInfo<V> getServiceInfoFromServiceCache( + @NonNull android.content.pm.ServiceInfo serviceInfo, long lastUpdateTime) { + String serviceCacheKey = getServiceCacheKey(serviceInfo); + synchronized (mServicesLock) { + ServiceInfo<V> serviceCache = mServiceInfoCaches.get(serviceCacheKey); + if (serviceCache == null) { + return null; + } + if (serviceCache.lastUpdateTime == lastUpdateTime) { + return serviceCache; + } + // The service is not latest, remove it from the cache. + mServiceInfoCaches.remove(serviceCacheKey); + return null; + } + } } diff --git a/core/java/android/content/pm/SigningInfo.aidl b/core/java/android/content/pm/SigningInfo.aidl new file mode 100644 index 000000000000..bc986d1b214b --- /dev/null +++ b/core/java/android/content/pm/SigningInfo.aidl @@ -0,0 +1,19 @@ +/* + * 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.content.pm; + +parcelable SigningInfo;
\ No newline at end of file diff --git a/core/java/android/content/pm/SystemFeaturesCache.aidl b/core/java/android/content/pm/SystemFeaturesCache.aidl new file mode 100644 index 000000000000..18c1830a1859 --- /dev/null +++ b/core/java/android/content/pm/SystemFeaturesCache.aidl @@ -0,0 +1,19 @@ +/* +** Copyright 2025, 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.content.pm; + +parcelable SystemFeaturesCache; diff --git a/core/java/android/content/pm/SystemFeaturesCache.java b/core/java/android/content/pm/SystemFeaturesCache.java new file mode 100644 index 000000000000..c41a7abbbc35 --- /dev/null +++ b/core/java/android/content/pm/SystemFeaturesCache.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2025 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.content.pm; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Arrays; +import java.util.Collection; + +/** + * A simple cache for SDK-defined system feature versions. + * + * The dense representation minimizes any per-process memory impact (<1KB). The tradeoff is that + * custom, non-SDK defined features are not captured by the cache, for which we can rely on the + * usual IPC cache for related queries. + * + * @hide + */ +public final class SystemFeaturesCache implements Parcelable { + + // Sentinel value used for SDK-declared features that are unavailable on the current device. + private static final int UNAVAILABLE_FEATURE_VERSION = Integer.MIN_VALUE; + + // An array of versions for SDK-defined features, from [0, PackageManager.SDK_FEATURE_COUNT). + @NonNull + private final int[] mSdkFeatureVersions; + + /** + * Populates the cache from the set of all available {@link FeatureInfo} definitions. + * + * System features declared in {@link PackageManager} will be entered into the cache based on + * availability in this feature set. Other custom system features will be ignored. + */ + public SystemFeaturesCache(@NonNull ArrayMap<String, FeatureInfo> availableFeatures) { + this(availableFeatures.values()); + } + + @VisibleForTesting + public SystemFeaturesCache(@NonNull Collection<FeatureInfo> availableFeatures) { + // First set all SDK-defined features as unavailable. + mSdkFeatureVersions = new int[PackageManager.SDK_FEATURE_COUNT]; + Arrays.fill(mSdkFeatureVersions, UNAVAILABLE_FEATURE_VERSION); + + // Then populate SDK-defined feature versions from the full set of runtime features. + for (FeatureInfo fi : availableFeatures) { + int sdkFeatureIndex = PackageManager.maybeGetSdkFeatureIndex(fi.name); + if (sdkFeatureIndex >= 0) { + mSdkFeatureVersions[sdkFeatureIndex] = fi.version; + } + } + } + + /** Only used by @{code CREATOR.createFromParcel(...)} */ + private SystemFeaturesCache(@NonNull Parcel parcel) { + final int[] featureVersions = parcel.createIntArray(); + if (featureVersions == null) { + throw new IllegalArgumentException( + "Parceled SDK feature versions should never be null"); + } + if (featureVersions.length != PackageManager.SDK_FEATURE_COUNT) { + throw new IllegalArgumentException( + String.format( + "Unexpected cached SDK feature count: %d (expected %d)", + featureVersions.length, PackageManager.SDK_FEATURE_COUNT)); + } + mSdkFeatureVersions = featureVersions; + } + + /** + * @return Whether the given feature is available (for SDK-defined features), otherwise null. + */ + public Boolean maybeHasFeature(@NonNull String featureName, int version) { + // Features defined outside of the SDK aren't cached. + int sdkFeatureIndex = PackageManager.maybeGetSdkFeatureIndex(featureName); + if (sdkFeatureIndex < 0) { + return null; + } + + // As feature versions can in theory collide with our sentinel value, in the (extremely) + // unlikely event that the queried version matches the sentinel value, we can't distinguish + // between an unavailable feature and a feature with the defined sentinel value. + if (version == UNAVAILABLE_FEATURE_VERSION + && mSdkFeatureVersions[sdkFeatureIndex] == UNAVAILABLE_FEATURE_VERSION) { + return null; + } + + return mSdkFeatureVersions[sdkFeatureIndex] >= version; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + parcel.writeIntArray(mSdkFeatureVersions); + } + + @NonNull + public static final Parcelable.Creator<SystemFeaturesCache> CREATOR = + new Parcelable.Creator<SystemFeaturesCache>() { + + @Override + public SystemFeaturesCache createFromParcel(Parcel parcel) { + return new SystemFeaturesCache(parcel); + } + + @Override + public SystemFeaturesCache[] newArray(int size) { + return new SystemFeaturesCache[size]; + } + }; +} diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 7bba06c87813..e4b8c90d381d 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -383,3 +383,11 @@ flag { bug: "334024639" description: "Feature flag to check whether a given UID can access a content provider" } + +flag { + name: "optimize_parsing_in_registered_services_cache" + namespace: "package_manager_service" + description: "Feature flag to optimize RegisteredServicesCache ServiceInfo parsing by using caches." + bug: "319137634" + is_fixed_read_only: true +} diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index 908999b64961..075457885586 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -353,7 +353,7 @@ public final class ApkAssets { /** @hide */ public @NonNull String getDebugName() { synchronized (this) { - return nativeGetDebugName(mNativePtr); + return mNativePtr == 0 ? "<destroyed>" : nativeGetDebugName(mNativePtr); } } diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java index 2d3d25217357..868429c30631 100644 --- a/core/java/android/hardware/SystemSensorManager.java +++ b/core/java/android/hardware/SystemSensorManager.java @@ -16,8 +16,6 @@ package android.hardware; -import static android.companion.virtual.VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED; -import static android.companion.virtual.VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; import static android.content.Context.DEVICE_ID_DEFAULT; @@ -164,11 +162,7 @@ public class SystemSensorManager extends SensorManager { // initialize the sensor list for (int index = 0;; ++index) { Sensor sensor = new Sensor(); - if (android.companion.virtual.flags.Flags.enableNativeVdm()) { - if (!nativeGetDefaultDeviceSensorAtIndex(mNativeInstance, sensor, index)) break; - } else { - if (!nativeGetSensorAtIndex(mNativeInstance, sensor, index)) break; - } + if (!nativeGetDefaultDeviceSensorAtIndex(mNativeInstance, sensor, index)) break; mFullSensorsList.add(sensor); mHandleToSensor.put(sensor.getHandle(), sensor); } @@ -555,11 +549,7 @@ public class SystemSensorManager extends SensorManager { } private List<Sensor> createRuntimeSensorListLocked(int deviceId) { - if (android.companion.virtual.flags.Flags.vdmPublicApis()) { - setupVirtualDeviceListener(); - } else { - setupRuntimeSensorBroadcastReceiver(); - } + setupVirtualDeviceListener(); List<Sensor> list = new ArrayList<>(); nativeGetRuntimeSensors(mNativeInstance, deviceId, list); mFullRuntimeSensorListByDevice.put(deviceId, list); @@ -570,35 +560,6 @@ public class SystemSensorManager extends SensorManager { return list; } - private void setupRuntimeSensorBroadcastReceiver() { - if (mRuntimeSensorBroadcastReceiver == null) { - mRuntimeSensorBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(ACTION_VIRTUAL_DEVICE_REMOVED)) { - synchronized (mFullRuntimeSensorListByDevice) { - final int deviceId = intent.getIntExtra( - EXTRA_VIRTUAL_DEVICE_ID, DEVICE_ID_DEFAULT); - List<Sensor> removedSensors = - mFullRuntimeSensorListByDevice.removeReturnOld(deviceId); - if (removedSensors != null) { - for (Sensor s : removedSensors) { - cleanupSensorConnection(s); - } - } - mRuntimeSensorListByDeviceByType.remove(deviceId); - } - } - } - }; - - IntentFilter filter = new IntentFilter("virtual_device_removed"); - filter.addAction(ACTION_VIRTUAL_DEVICE_REMOVED); - mContext.registerReceiver(mRuntimeSensorBroadcastReceiver, filter, - Context.RECEIVER_NOT_EXPORTED); - } - } - private void setupVirtualDeviceListener() { if (mVirtualDeviceListener != null) { return; diff --git a/core/java/android/hardware/contexthub/HubEndpoint.java b/core/java/android/hardware/contexthub/HubEndpoint.java index 71702d996883..25cdc508fdce 100644 --- a/core/java/android/hardware/contexthub/HubEndpoint.java +++ b/core/java/android/hardware/contexthub/HubEndpoint.java @@ -137,6 +137,8 @@ public class HubEndpoint { serviceDescriptor, mLifecycleCallback.onSessionOpenRequest( initiator, serviceDescriptor))); + } else { + invokeCallbackFinished(); } } @@ -163,6 +165,8 @@ public class HubEndpoint { + result.getReason()); rejectSession(sessionId); } + + invokeCallbackFinished(); } private void acceptSession( @@ -249,7 +253,12 @@ public class HubEndpoint { activeSession.setOpened(); if (mLifecycleCallback != null) { mLifecycleCallbackExecutor.execute( - () -> mLifecycleCallback.onSessionOpened(activeSession)); + () -> { + mLifecycleCallback.onSessionOpened(activeSession); + invokeCallbackFinished(); + }); + } else { + invokeCallbackFinished(); } } @@ -278,7 +287,10 @@ public class HubEndpoint { synchronized (mLock) { mActiveSessions.remove(sessionId); } + invokeCallbackFinished(); }); + } else { + invokeCallbackFinished(); } } @@ -323,8 +335,17 @@ public class HubEndpoint { e.rethrowFromSystemServer(); } } + invokeCallbackFinished(); }); } + + private void invokeCallbackFinished() { + try { + mServiceToken.onCallbackFinished(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } }; /** Binder returned from system service, non-null while registered. */ diff --git a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl index 44f80c819e83..eb1255c06094 100644 --- a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl +++ b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl @@ -94,4 +94,10 @@ interface IContextHubEndpoint { */ @EnforcePermission("ACCESS_CONTEXT_HUB") void sendMessageDeliveryStatus(int sessionId, int messageSeqNumber, byte errorCode); + + /** + * Invoked when a callback from IContextHubEndpointCallback finishes. + */ + @EnforcePermission("ACCESS_CONTEXT_HUB") + void onCallbackFinished(); } diff --git a/core/java/android/hardware/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java index 0e2c05f92e7c..1d2f133ee759 100644 --- a/core/java/android/hardware/display/DisplayTopology.java +++ b/core/java/android/hardware/display/DisplayTopology.java @@ -679,8 +679,7 @@ public final class DisplayTopology implements Parcelable { } /** - * Tests whether two brightness float values are within a small enough tolerance - * of each other. + * Tests whether two float values are within a small enough tolerance of each other. * @param a first float to compare * @param b second float to compare * @return whether the two values are within a small enough tolerance value diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java index d84d29292ed5..8fbe05c4e9eb 100644 --- a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java +++ b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java @@ -121,15 +121,20 @@ public class FingerprintSensorPropertiesInternal extends SensorPropertiesInterna /** * Returns if sensor type is ultrasonic Udfps - * @return true if sensor is ultrasonic Udfps, false otherwise */ public boolean isUltrasonicUdfps() { return sensorType == TYPE_UDFPS_ULTRASONIC; } /** + * Returns if sensor type is optical Udfps + */ + public boolean isOpticalUdfps() { + return sensorType == TYPE_UDFPS_OPTICAL; + } + + /** * Returns if sensor type is side-FPS - * @return true if sensor is side-fps, false otherwise */ public boolean isAnySidefpsType() { switch (sensorType) { diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index ed510e467f82..2bb28a1b6b0b 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -266,6 +266,11 @@ interface IInputManager { @PermissionManuallyEnforced @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.MANAGE_KEY_GESTURES)") + AidlInputGestureData getInputGesture(int userId, in AidlInputGestureData.Trigger trigger); + + @PermissionManuallyEnforced + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MANAGE_KEY_GESTURES)") int addCustomInputGesture(int userId, in AidlInputGestureData data); @PermissionManuallyEnforced diff --git a/core/java/android/hardware/input/InputGestureData.java b/core/java/android/hardware/input/InputGestureData.java index f41550f6061e..75c652c973e4 100644 --- a/core/java/android/hardware/input/InputGestureData.java +++ b/core/java/android/hardware/input/InputGestureData.java @@ -48,27 +48,7 @@ public final class InputGestureData { /** Returns the trigger information for this input gesture */ public Trigger getTrigger() { - switch (mInputGestureData.trigger.getTag()) { - case AidlInputGestureData.Trigger.Tag.key: { - AidlInputGestureData.KeyTrigger trigger = mInputGestureData.trigger.getKey(); - if (trigger == null) { - throw new RuntimeException("InputGestureData is corrupted, null key trigger!"); - } - return createKeyTrigger(trigger.keycode, trigger.modifierState); - } - case AidlInputGestureData.Trigger.Tag.touchpadGesture: { - AidlInputGestureData.TouchpadGestureTrigger trigger = - mInputGestureData.trigger.getTouchpadGesture(); - if (trigger == null) { - throw new RuntimeException( - "InputGestureData is corrupted, null touchpad trigger!"); - } - return createTouchpadTrigger(trigger.gestureType); - } - default: - throw new RuntimeException("InputGestureData is corrupted, invalid trigger type!"); - - } + return createTriggerFromAidlTrigger(mInputGestureData.trigger); } /** Returns the action to perform for this input gesture */ @@ -147,18 +127,7 @@ public final class InputGestureData { "No app launch data for system action launch application"); } AidlInputGestureData data = new AidlInputGestureData(); - data.trigger = new AidlInputGestureData.Trigger(); - if (mTrigger instanceof KeyTrigger keyTrigger) { - data.trigger.setKey(new AidlInputGestureData.KeyTrigger()); - data.trigger.getKey().keycode = keyTrigger.getKeycode(); - data.trigger.getKey().modifierState = keyTrigger.getModifierState(); - } else if (mTrigger instanceof TouchpadTrigger touchpadTrigger) { - data.trigger.setTouchpadGesture(new AidlInputGestureData.TouchpadGestureTrigger()); - data.trigger.getTouchpadGesture().gestureType = - touchpadTrigger.getTouchpadGestureType(); - } else { - throw new IllegalArgumentException("Invalid trigger type!"); - } + data.trigger = mTrigger.getAidlTrigger(); data.gestureType = mKeyGestureType; if (mAppLaunchData != null) { if (mAppLaunchData instanceof AppLaunchData.CategoryData categoryData) { @@ -198,6 +167,7 @@ public final class InputGestureData { } public interface Trigger { + AidlInputGestureData.Trigger getAidlTrigger(); } /** Creates a input gesture trigger based on a key press */ @@ -210,85 +180,128 @@ public final class InputGestureData { return new TouchpadTrigger(touchpadGestureType); } + public static Trigger createTriggerFromAidlTrigger(AidlInputGestureData.Trigger aidlTrigger) { + switch (aidlTrigger.getTag()) { + case AidlInputGestureData.Trigger.Tag.key: { + AidlInputGestureData.KeyTrigger trigger = aidlTrigger.getKey(); + if (trigger == null) { + throw new RuntimeException("aidlTrigger is corrupted, null key trigger!"); + } + return new KeyTrigger(trigger); + } + case AidlInputGestureData.Trigger.Tag.touchpadGesture: { + AidlInputGestureData.TouchpadGestureTrigger trigger = + aidlTrigger.getTouchpadGesture(); + if (trigger == null) { + throw new RuntimeException( + "aidlTrigger is corrupted, null touchpad trigger!"); + } + return new TouchpadTrigger(trigger); + } + default: + throw new RuntimeException("aidlTrigger is corrupted, invalid trigger type!"); + + } + } + /** Key based input gesture trigger */ public static class KeyTrigger implements Trigger { - private static final int SHORTCUT_META_MASK = - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON - | KeyEvent.META_SHIFT_ON; - private final int mKeycode; - private final int mModifierState; + + AidlInputGestureData.KeyTrigger mAidlKeyTrigger; + + private KeyTrigger(@NonNull AidlInputGestureData.KeyTrigger aidlKeyTrigger) { + mAidlKeyTrigger = aidlKeyTrigger; + } private KeyTrigger(int keycode, int modifierState) { if (keycode <= KeyEvent.KEYCODE_UNKNOWN || keycode > KeyEvent.getMaxKeyCode()) { throw new IllegalArgumentException("Invalid keycode = " + keycode); } - mKeycode = keycode; - mModifierState = modifierState; + mAidlKeyTrigger = new AidlInputGestureData.KeyTrigger(); + mAidlKeyTrigger.keycode = keycode; + mAidlKeyTrigger.modifierState = modifierState; } public int getKeycode() { - return mKeycode; + return mAidlKeyTrigger.keycode; } public int getModifierState() { - return mModifierState; + return mAidlKeyTrigger.modifierState; + } + + public AidlInputGestureData.Trigger getAidlTrigger() { + AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger(); + trigger.setKey(mAidlKeyTrigger); + return trigger; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof KeyTrigger that)) return false; - return mKeycode == that.mKeycode && mModifierState == that.mModifierState; + return Objects.equals(mAidlKeyTrigger, that.mAidlKeyTrigger); } @Override public int hashCode() { - return Objects.hash(mKeycode, mModifierState); + return mAidlKeyTrigger.hashCode(); } @Override public String toString() { return "KeyTrigger{" + - "mKeycode=" + KeyEvent.keyCodeToString(mKeycode) + - ", mModifierState=" + mModifierState + + "mKeycode=" + KeyEvent.keyCodeToString(mAidlKeyTrigger.keycode) + + ", mModifierState=" + mAidlKeyTrigger.modifierState + '}'; } } /** Touchpad based input gesture trigger */ public static class TouchpadTrigger implements Trigger { - private final int mTouchpadGestureType; + AidlInputGestureData.TouchpadGestureTrigger mAidlTouchpadTrigger; + + private TouchpadTrigger( + @NonNull AidlInputGestureData.TouchpadGestureTrigger aidlTouchpadTrigger) { + mAidlTouchpadTrigger = aidlTouchpadTrigger; + } private TouchpadTrigger(int touchpadGestureType) { if (touchpadGestureType != TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP) { throw new IllegalArgumentException( "Invalid touchpadGestureType = " + touchpadGestureType); } - mTouchpadGestureType = touchpadGestureType; + mAidlTouchpadTrigger = new AidlInputGestureData.TouchpadGestureTrigger(); + mAidlTouchpadTrigger.gestureType = touchpadGestureType; } public int getTouchpadGestureType() { - return mTouchpadGestureType; + return mAidlTouchpadTrigger.gestureType; + } + + public AidlInputGestureData.Trigger getAidlTrigger() { + AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger(); + trigger.setTouchpadGesture(mAidlTouchpadTrigger); + return trigger; } @Override public String toString() { return "TouchpadTrigger{" + - "mTouchpadGestureType=" + mTouchpadGestureType + + "mTouchpadGestureType=" + mAidlTouchpadTrigger.gestureType + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TouchpadTrigger that = (TouchpadTrigger) o; - return mTouchpadGestureType == that.mTouchpadGestureType; + if (!(o instanceof TouchpadTrigger that)) return false; + return Objects.equals(mAidlTouchpadTrigger, that.mAidlTouchpadTrigger); } @Override public int hashCode() { - return Objects.hashCode(mTouchpadGestureType); + return mAidlTouchpadTrigger.hashCode(); } } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 10224c1be788..cf41e138047a 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1480,6 +1480,30 @@ public final class InputManager { mGlobal.unregisterKeyGestureEventHandler(handler); } + /** + * Find an input gesture mapped to a particular trigger. + * + * @param trigger to find the input gesture for + * @return input gesture mapped to the provided trigger, {@code null} if none found + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES) + @UserHandleAware + @Nullable + public InputGestureData getInputGesture(@NonNull InputGestureData.Trigger trigger) { + try { + AidlInputGestureData result = mIm.getInputGesture(mContext.getUserId(), + trigger.getAidlTrigger()); + if (result == null) { + return null; + } + return new InputGestureData(result); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** Adds a new custom input gesture * * @param inputGestureData gesture data to add as custom gesture diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java index 8da630c95135..b380e259577c 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -78,6 +78,24 @@ public class InputSettings { public static final int DEFAULT_POINTER_SPEED = 0; /** + * Pointer Speed: The minimum (slowest) mouse scrolling speed (-7). + * @hide + */ + public static final int MIN_MOUSE_SCROLLING_SPEED = -7; + + /** + * Pointer Speed: The maximum (fastest) mouse scrolling speed (7). + * @hide + */ + public static final int MAX_MOUSE_SCROLLING_SPEED = 7; + + /** + * Pointer Speed: The default mouse scrolling speed (0). + * @hide + */ + public static final int DEFAULT_MOUSE_SCROLLING_SPEED = 0; + + /** * Bounce Keys Threshold: The default value of the threshold (500 ms). * * @hide @@ -650,6 +668,54 @@ public class InputSettings { } /** + * Gets the mouse scrolling speed. + * + * The returned value only applies when mouse scrolling acceleration is not enabled. + * + * @param context The application context. + * @return The mouse scrolling speed as a value between {@link #MIN_MOUSE_SCROLLING_SPEED} and + * {@link #MAX_MOUSE_SCROLLING_SPEED}, or the default value + * {@link #DEFAULT_MOUSE_SCROLLING_SPEED}. + * + * @hide + */ + public static int getMouseScrollingSpeed(@NonNull Context context) { + if (!isMouseScrollingAccelerationFeatureFlagEnabled()) { + return 0; + } + + return Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.MOUSE_SCROLLING_SPEED, DEFAULT_MOUSE_SCROLLING_SPEED, + UserHandle.USER_CURRENT); + } + + /** + * Sets the mouse scrolling speed, and saves it in the settings. + * + * The new speed will only apply when mouse scrolling acceleration is not enabled. + * + * @param context The application context. + * @param speed The mouse scrolling speed as a value between {@link #MIN_MOUSE_SCROLLING_SPEED} + * and {@link #MAX_MOUSE_SCROLLING_SPEED}, or the default value + * {@link #DEFAULT_MOUSE_SCROLLING_SPEED}. + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_SETTINGS) + public static void setMouseScrollingSpeed(@NonNull Context context, int speed) { + if (isMouseScrollingAccelerationEnabled(context)) { + return; + } + + if (speed < MIN_MOUSE_SCROLLING_SPEED || speed > MAX_MOUSE_SCROLLING_SPEED) { + throw new IllegalArgumentException("speed out of range"); + } + + Settings.System.putIntForUser(context.getContentResolver(), + Settings.System.MOUSE_SCROLLING_SPEED, speed, UserHandle.USER_CURRENT); + } + + /** * Whether mouse vertical scrolling is reversed. This applies only to connected mice. * * @param context The application context. diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 66d073fa791e..4025242fd208 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -43,6 +43,9 @@ public final class KeyGestureEvent { private static final int LOG_EVENT_UNSPECIFIED = FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED; + // Used as a placeholder to identify if a gesture is reserved for system + public static final int KEY_GESTURE_TYPE_SYSTEM_RESERVED = -1; + // These values should not change and values should not be re-used as this data is persisted to // long term storage and must be kept backwards compatible. public static final int KEY_GESTURE_TYPE_UNSPECIFIED = 0; @@ -129,6 +132,7 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_RIGHT = 79; public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP = 80; public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN = 81; + public static final int KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS = 82; public static final int FLAG_CANCELLED = 1; @@ -143,6 +147,7 @@ public final class KeyGestureEvent { public static final int ACTION_GESTURE_COMPLETE = 2; @IntDef(prefix = "KEY_GESTURE_TYPE_", value = { + KEY_GESTURE_TYPE_SYSTEM_RESERVED, KEY_GESTURE_TYPE_UNSPECIFIED, KEY_GESTURE_TYPE_HOME, KEY_GESTURE_TYPE_RECENT_APPS, @@ -225,11 +230,32 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_MAGNIFICATION_PAN_RIGHT, KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP, KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS, }) @Retention(RetentionPolicy.SOURCE) public @interface KeyGestureType { } + /** + * Returns whether the key gesture type passed as argument is allowed for visible background + * users. + * + * @hide + */ + public static boolean isVisibleBackgrounduserAllowedGesture(int keyGestureType) { + switch (keyGestureType) { + case KEY_GESTURE_TYPE_SLEEP: + case KEY_GESTURE_TYPE_WAKEUP: + case KEY_GESTURE_TYPE_LAUNCH_ASSISTANT: + case KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT: + case KEY_GESTURE_TYPE_VOLUME_MUTE: + case KEY_GESTURE_TYPE_RECENT_APPS: + case KEY_GESTURE_TYPE_APP_SWITCH: + return false; + } + return true; + } + public KeyGestureEvent(@NonNull AidlKeyGestureEvent keyGestureEvent) { this.mKeyGestureEvent = keyGestureEvent; } @@ -643,6 +669,8 @@ public final class KeyGestureEvent { private static String keyGestureTypeToString(@KeyGestureType int value) { switch (value) { + case KEY_GESTURE_TYPE_SYSTEM_RESERVED: + return "KEY_GESTURE_TYPE_SYSTEM_RESERVED"; case KEY_GESTURE_TYPE_UNSPECIFIED: return "KEY_GESTURE_TYPE_UNSPECIFIED"; case KEY_GESTURE_TYPE_HOME: @@ -807,6 +835,8 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP"; case KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN: return "KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN"; + case KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: + return "KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS"; default: return Integer.toHexString(value); } diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index ecd90e46e432..1041041b2a27 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -386,6 +386,15 @@ public class BaseBundle implements Parcel.ClassLoaderProvider { } /** + * return true if the value corresponding to this key is still parceled. + * @hide + */ + public boolean isValueParceled(String key) { + if (mMap == null) return true; + int i = mMap.indexOfKey(key); + return (mMap.valueAt(i) instanceof BiFunction<?, ?, ?>); + } + /** * Returns the value for a certain position in the array map for expected return type {@code * clazz} (or pass {@code null} for no type check). * diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 2bc6ab5a18e9..d5640221e6bd 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -2783,4 +2783,12 @@ public final class Debug */ public static native boolean logAllocatorStats(); + /** + * Return the amount of memory (in kB) allocated by kernel drivers through CMA. + * @return a non-negative value or -1 on error. + * + * @hide + */ + public static native long getKernelCmaUsageKb(); + } diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java index 8f6a50843ddb..45a7afa014a1 100644 --- a/core/java/android/os/GraphicsEnvironment.java +++ b/core/java/android/os/GraphicsEnvironment.java @@ -78,6 +78,9 @@ public class GraphicsEnvironment { private static final String PROPERTY_GFX_DRIVER_PRERELEASE = "ro.gfx.driver.1"; private static final String PROPERTY_GFX_DRIVER_BUILD_TIME = "ro.gfx.driver_build_time"; + /// System properties related to EGL + private static final String PROPERTY_RO_HARDWARE_EGL = "ro.hardware.egl"; + // Metadata flags within the <application> tag in the AndroidManifest.xml file. private static final String METADATA_DRIVER_BUILD_TIME = "com.android.graphics.driver.build_time"; @@ -504,9 +507,11 @@ public class GraphicsEnvironment { final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, PackageManager.MATCH_SYSTEM_ONLY); - if (resolveInfos.size() != 1) { - Log.v(TAG, "Invalid number of ANGLE packages. Required: 1, Found: " - + resolveInfos.size()); + if (resolveInfos.isEmpty()) { + Log.v(TAG, "No ANGLE packages installed."); + return ""; + } else if (resolveInfos.size() > 1) { + Log.v(TAG, "Too many ANGLE packages found: " + resolveInfos.size()); if (DEBUG) { for (ResolveInfo resolveInfo : resolveInfos) { Log.d(TAG, "Found ANGLE package: " + resolveInfo.activityInfo.packageName); @@ -516,7 +521,7 @@ public class GraphicsEnvironment { } // Must be exactly 1 ANGLE PKG found to get here. - return resolveInfos.get(0).activityInfo.packageName; + return resolveInfos.getFirst().activityInfo.packageName; } /** @@ -545,10 +550,12 @@ public class GraphicsEnvironment { } /** - * Determine whether ANGLE should be used, attempt to set up from apk first, if ANGLE can be - * set up from apk, pass ANGLE details down to the C++ GraphicsEnv class via - * GraphicsEnv::setAngleInfo(). If apk setup fails, attempt to set up to use system ANGLE. - * Return false if both fail. + * If ANGLE is not the system driver, determine whether ANGLE should be used, and if so, pass + * down the necessary details to the C++ GraphicsEnv class via GraphicsEnv::setAngleInfo(). + * <p> + * If ANGLE is the system driver or the various flags indicate it should be used, attempt to + * set up ANGLE from the APK first, so the updatable libraries are used. If APK setup fails, + * attempt to set up the system ANGLE. Return false if both fail. * * @param context - Context of the application. * @param bundle - Bundle of the application. @@ -559,15 +566,26 @@ public class GraphicsEnvironment { */ private boolean setupAngle(Context context, Bundle bundle, PackageManager packageManager, String packageName) { - final String angleChoice = queryAngleChoice(context, bundle, packageName); - if (angleChoice.equals(ANGLE_GL_DRIVER_CHOICE_DEFAULT)) { - return false; - } - if (angleChoice.equals(ANGLE_GL_DRIVER_CHOICE_NATIVE)) { - nativeSetAngleInfo("", true, packageName, null); - return false; + final String eglDriverName = SystemProperties.get(PROPERTY_RO_HARDWARE_EGL); + + // The ANGLE choice only makes sense if ANGLE is not the system driver. + if (!eglDriverName.equals(ANGLE_DRIVER_NAME)) { + final String angleChoice = queryAngleChoice(context, bundle, packageName); + if (angleChoice.equals(ANGLE_GL_DRIVER_CHOICE_DEFAULT)) { + return false; + } + if (angleChoice.equals(ANGLE_GL_DRIVER_CHOICE_NATIVE)) { + nativeSetAngleInfo("", true, packageName, null); + return false; + } } + // If we reach here, it means either: + // 1. system driver is not ANGLE, but ANGLE is requested. + // 2. system driver is ANGLE. + // In both cases, setup ANGLE info. We attempt to setup the APK first, so + // updated/development libraries are used if the APK is present, falling back to the system + // libraries otherwise. return setupAngleFromApk(context, bundle, packageManager, packageName) || setupAngleFromSystem(context, bundle, packageName); } @@ -605,7 +623,6 @@ public class GraphicsEnvironment { if (angleInfo == null) { anglePkgName = getAnglePackageName(packageManager); if (TextUtils.isEmpty(anglePkgName)) { - Log.v(TAG, "Failed to find ANGLE package."); return false; } @@ -689,11 +706,11 @@ public class GraphicsEnvironment { * @param context */ public void showAngleInUseDialogBox(Context context) { - if (!shouldShowAngleInUseDialogBox(context)) { + if (!mShouldUseAngle) { return; } - if (!mShouldUseAngle) { + if (!shouldShowAngleInUseDialogBox(context)) { return; } diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 0879118ff856..4aa74621bd62 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -593,11 +593,11 @@ public final class Parcel { */ public final void recycle() { if (mRecycled) { - Log.wtf(TAG, "Recycle called on unowned Parcel. (recycle twice?) Here: " + String error = "Recycle called on unowned Parcel. (recycle twice?) Here: " + Log.getStackTraceString(new Throwable()) - + " Original recycle call (if DEBUG_RECYCLE): ", mStack); - - return; + + " Original recycle call (if DEBUG_RECYCLE): "; + Log.wtf(TAG, error, mStack); + throw new IllegalStateException(error, mStack); } mRecycled = true; diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 1801df048b3e..2a5666cbe83c 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -615,6 +615,7 @@ public final class PowerManager { WAKE_REASON_WAKE_KEY, WAKE_REASON_WAKE_MOTION, WAKE_REASON_HDMI, + WAKE_REASON_LID, WAKE_REASON_DISPLAY_GROUP_ADDED, WAKE_REASON_DISPLAY_GROUP_TURNED_ON, WAKE_REASON_UNFOLD_DEVICE, diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java index 9085fe09bdaa..a58fea891851 100644 --- a/core/java/android/os/ServiceManager.java +++ b/core/java/android/os/ServiceManager.java @@ -278,7 +278,7 @@ public final class ServiceManager { return service; } else { return Binder.allowBlocking( - getIServiceManager().checkService(name).getServiceWithMetadata().service); + getIServiceManager().checkService2(name).getServiceWithMetadata().service); } } catch (RemoteException e) { Log.e(TAG, "error in checkService", e); diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java index 7ea521ec5dd4..a5aa1b3efcd2 100644 --- a/core/java/android/os/ServiceManagerNative.java +++ b/core/java/android/os/ServiceManagerNative.java @@ -62,16 +62,23 @@ class ServiceManagerProxy implements IServiceManager { @UnsupportedAppUsage public IBinder getService(String name) throws RemoteException { // Same as checkService (old versions of servicemanager had both methods). - return checkService(name).getServiceWithMetadata().service; + return checkService2(name).getServiceWithMetadata().service; } public Service getService2(String name) throws RemoteException { // Same as checkService (old versions of servicemanager had both methods). - return checkService(name); + return checkService2(name); } - public Service checkService(String name) throws RemoteException { - return mServiceManager.checkService(name); + // TODO(b/355394904): This function has been deprecated, please use checkService2 instead. + @UnsupportedAppUsage + public IBinder checkService(String name) throws RemoteException { + // Same as checkService (old versions of servicemanager had both methods). + return checkService2(name).getServiceWithMetadata().service; + } + + public Service checkService2(String name) throws RemoteException { + return mServiceManager.checkService2(name); } public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority) diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index e24f08b7dfe5..8b8369890d1b 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -110,6 +110,14 @@ flag { } flag { + name: "allow_thermal_hal_skin_forecast" + is_exported: true + namespace: "game" + description: "Enable thermal HAL skin temperature forecast to be used by headroom API" + bug: "383211885" +} + +flag { name: "allow_thermal_headroom_thresholds" is_exported: true namespace: "game" diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c94526bcdcd7..baaaa464a4cf 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6372,6 +6372,19 @@ public final class Settings { "mouse_pointer_acceleration_enabled"; /** + * Mouse scrolling speed setting. + * + * This is an integer value in a range between -7 and +7, so there are 15 possible values. + * The setting only applies when mouse scrolling acceleration is not enabled. + * -7 = slowest + * 0 = default speed + * +7 = fastest + * + * @hide + */ + public static final String MOUSE_SCROLLING_SPEED = "mouse_scrolling_speed"; + + /** * Pointer fill style, specified by * {@link android.view.PointerIcon.PointerIconVectorStyleFill} constants. * @@ -6623,6 +6636,7 @@ public final class Settings { PRIVATE_SETTINGS.add(MOUSE_POINTER_ACCELERATION_ENABLED); PRIVATE_SETTINGS.add(PREFERRED_REGION); PRIVATE_SETTINGS.add(MOUSE_SCROLLING_ACCELERATION); + PRIVATE_SETTINGS.add(MOUSE_SCROLLING_SPEED); } /** @@ -9305,6 +9319,16 @@ public final class Settings { "accessibility_autoclick_delay"; /** + * Integer setting specifying the autoclick cursor area size (the radius of the autoclick + * ring indicator) when {@link #ACCESSIBILITY_AUTOCLICK_ENABLED} is set. + * + * @see #ACCESSIBILITY_AUTOCLICK_ENABLED + * @hide + */ + public static final String ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE = + "accessibility_autoclick_cursor_area_size"; + + /** * Whether or not larger size icons are used for the pointer of mouse/trackpad for * accessibility. * (0 = false, 1 = true) @@ -10501,6 +10525,15 @@ public final class Settings { public static final String SCREENSAVER_ACTIVATE_ON_SLEEP = "screensaver_activate_on_sleep"; /** + * If screensavers are enabled, whether the screensaver should be + * automatically launched when the device is stationary and upright. + * @hide + */ + @Readable + public static final String SCREENSAVER_ACTIVATE_ON_POSTURED = + "screensaver_activate_on_postured"; + + /** * If screensavers are enabled, the default screensaver component. * @hide */ @@ -13304,6 +13337,16 @@ public final class Settings { public static final String AUTO_TIME_ZONE_EXPLICIT = "auto_time_zone_explicit"; /** + * Value to specify if the device should send notifications when {@link #AUTO_TIME_ZONE} is + * on and the device's time zone changes. + * + * <p>1=yes, 0=no. + * + * @hide + */ + public static final String TIME_ZONE_NOTIFICATIONS = "time_zone_notifications"; + + /** * URI for the car dock "in" event sound. * @hide */ @@ -17395,13 +17438,6 @@ public final class Settings { /** - * Whether back preview animations are played when user does a back gesture or presses - * the back button. - * @hide - */ - public static final String ENABLE_BACK_ANIMATION = "enable_back_animation"; - - /** * An allow list of packages for which the user has granted the permission to communicate * across profiles. * diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index ebb6fb451699..4a9e945e62a9 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -42,6 +42,16 @@ flag { } flag { + name: "secure_array_zeroization" + namespace: "platform_security" + description: "Enable secure array zeroization" + bug: "320392352" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "deprecate_fsv_sig" namespace: "hardware_backed_security" description: "Feature flag for deprecating .fsv_sig" diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig index dfc11dcb5427..d3a230d1335d 100644 --- a/core/java/android/service/dreams/flags.aconfig +++ b/core/java/android/service/dreams/flags.aconfig @@ -77,3 +77,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "allow_dream_when_postured" + namespace: "systemui" + description: "Allow dreaming when device is stationary and upright" + bug: "383208131" +} diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index e254bf3e016f..3c53506990d1 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -16,7 +16,6 @@ package android.text; -import static com.android.graphics.hwui.flags.Flags.highContrastTextLuminance; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; @@ -77,7 +76,7 @@ public abstract class Layout { private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f; private static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP = 5f; // since we're not using soft light yet, this needs to be much lower than the spec'd 0.8 - private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.5f; + private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.7f; /** @hide */ @IntDef(prefix = { "BREAK_STRATEGY_" }, value = { @@ -670,15 +669,11 @@ public abstract class Layout { // High-contrast text mode // Determine if the text is black-on-white or white-on-black, so we know what blendmode will // give the highest contrast and most realistic text color. - // This equation should match the one in libs/hwui/hwui/DrawTextFunctor.h - if (highContrastTextLuminance()) { - var lab = new double[3]; - ColorUtils.colorToLAB(color, lab); - return lab[0] < 50.0; - } else { - int channelSum = Color.red(color) + Color.green(color) + Color.blue(color); - return channelSum < (128 * 3); - } + // LINT.IfChange(hct_darken) + var lab = new double[3]; + ColorUtils.colorToLAB(color, lab); + return lab[0] < 50.0; + // LINT.ThenChange(/libs/hwui/hwui/DrawTextFunctor.h:hct_darken) } private boolean isJustificationRequired(int lineNum) { diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java index 5c38a1597433..195896dc8edf 100644 --- a/core/java/android/view/InputEventConsistencyVerifier.java +++ b/core/java/android/view/InputEventConsistencyVerifier.java @@ -81,7 +81,7 @@ public final class InputEventConsistencyVerifier { // Bitfield of pointer ids that are currently down. // Assumes that the largest possible pointer id is 31, which is potentially subject to change. - // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) + // (See MAX_POINTER_ID in frameworks/native/include/input/input.h) private int mTouchEventStreamPointers; // The device id and source of the current stream of touch events. diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java index 6cd4a4033adf..3e529ccf064a 100644 --- a/core/java/android/view/InputWindowHandle.java +++ b/core/java/android/view/InputWindowHandle.java @@ -57,7 +57,7 @@ public final class InputWindowHandle { InputConfig.NO_INPUT_CHANNEL, InputConfig.NOT_FOCUSABLE, InputConfig.NOT_TOUCHABLE, - InputConfig.PREVENT_SPLITTING, + InputConfig.DEPRECATED_PREVENT_SPLITTING, InputConfig.DUPLICATE_TOUCH_TO_WALLPAPER, InputConfig.IS_WALLPAPER, InputConfig.PAUSE_DISPATCHING, diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java index b21e85aeeb6a..da3a817f0341 100644 --- a/core/java/android/view/PointerIcon.java +++ b/core/java/android/view/PointerIcon.java @@ -514,10 +514,14 @@ public final class PointerIcon implements Parcelable { final TypedArray a = resources.obtainAttributes( parser, com.android.internal.R.styleable.PointerIcon); bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0); - hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0) - * pointerScale; - hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0) - * pointerScale; + // Cast the hotspot dimensions to int before scaling to match the scaling logic of + // the bitmap, whose intrinsic size is also an int before it is scaled. + final int unscaledHotSpotX = + (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0); + final int unscaledHotSpotY = + (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0); + hotSpotX = unscaledHotSpotX * pointerScale; + hotSpotY = unscaledHotSpotY * pointerScale; a.recycle(); } catch (Exception ex) { throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex); diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 833f2d98554e..e665c08c63e4 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -160,6 +160,10 @@ public final class SurfaceControl implements Parcelable { float l, float t, float r, float b); private static native void nativeSetCornerRadius(long transactionObj, long nativeObject, float cornerRadius); + private static native void nativeSetClientDrawnCornerRadius(long transactionObj, + long nativeObject, float clientDrawnCornerRadius); + private static native void nativeSetClientDrawnShadows(long transactionObj, + long nativeObject, float clientDrawnShadows); private static native void nativeSetBackgroundBlurRadius(long transactionObj, long nativeObject, int blurRadius); private static native void nativeSetLayerStack(long transactionObj, long nativeObject, @@ -3654,6 +3658,66 @@ public final class SurfaceControl implements Parcelable { return this; } + + /** + * Disables corner radius of a {@link SurfaceControl}. When the radius set by + * {@link Transaction#setCornerRadius(SurfaceControl, float)} is equal to + * clientDrawnCornerRadius the corner radius drawn by SurfaceFlinger is disabled. + * + * @param sc SurfaceControl + * @param clientDrawnCornerRadius Corner radius drawn by the client + * @return Itself. + * @hide + */ + @NonNull + public Transaction setClientDrawnCornerRadius(@NonNull SurfaceControl sc, + float clientDrawnCornerRadius) { + checkPreconditions(sc); + if (SurfaceControlRegistry.sCallStackDebuggingEnabled) { + SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging( + "setClientDrawnCornerRadius", this, sc, "clientDrawnCornerRadius=" + + clientDrawnCornerRadius); + } + if (Flags.ignoreCornerRadiusAndShadows()) { + nativeSetClientDrawnCornerRadius(mNativeObject, sc.mNativeObject, + clientDrawnCornerRadius); + } else { + Log.w(TAG, "setClientDrawnCornerRadius was called but" + + "ignore_corner_radius_and_shadows flag is disabled"); + } + + return this; + } + + /** + * Disables shadows of a {@link SurfaceControl}. When the radius set by + * {@link Transaction#setClientDrawnShadows(SurfaceControl, float)} is equal to + * clientDrawnShadowRadius the shadows drawn by SurfaceFlinger is disabled. + * + * @param sc SurfaceControl + * @param clientDrawnShadowRadius Shadow radius drawn by the client + * @return Itself. + * @hide + */ + @NonNull + public Transaction setClientDrawnShadows(@NonNull SurfaceControl sc, + float clientDrawnShadowRadius) { + checkPreconditions(sc); + if (SurfaceControlRegistry.sCallStackDebuggingEnabled) { + SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging( + "setClientDrawnShadows", this, sc, + "clientDrawnShadowRadius=" + clientDrawnShadowRadius); + } + if (Flags.ignoreCornerRadiusAndShadows()) { + nativeSetClientDrawnShadows(mNativeObject, sc.mNativeObject, + clientDrawnShadowRadius); + } else { + Log.w(TAG, "setClientDrawnShadows was called but" + + "ignore_corner_radius_and_shadows flag is disabled"); + } + return this; + } + /** * Sets the background blur radius of the {@link SurfaceControl}. * diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index f50ea9106a61..25bd713d9191 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -453,6 +453,7 @@ public final class WindowManagerGlobal { try { root.setView(view, wparams, panelParentView, userId); } catch (RuntimeException e) { + Log.e(TAG, "Couldn't add view: " + view, e); final int viewIndex = (index >= 0) ? index : (mViews.size() - 1); // BadTokenException or InvalidDisplayException, clean up. if (viewIndex >= 0) { diff --git a/core/java/android/view/WindowManagerPolicyConstants.java b/core/java/android/view/WindowManagerPolicyConstants.java index 1f341caa8ed3..6d2c0d0061dd 100644 --- a/core/java/android/view/WindowManagerPolicyConstants.java +++ b/core/java/android/view/WindowManagerPolicyConstants.java @@ -30,7 +30,8 @@ import java.lang.annotation.RetentionPolicy; * @hide */ public interface WindowManagerPolicyConstants { - // Policy flags. These flags are also defined in frameworks/base/include/ui/Input.h and + // Policy flags. These flags are also defined in + // frameworks/native/include/input/Input.h and // frameworks/native/libs/input/android/os/IInputConstants.aidl int FLAG_WAKE = 0x00000001; int FLAG_VIRTUAL = 0x00000002; diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index fd57aec4180b..64277b14098d 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -148,6 +148,18 @@ public final class AccessibilityManager { /** @hide */ public static final int AUTOCLICK_DELAY_DEFAULT = 600; + /** @hide */ + public static final int AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT = 60; + + /** @hide */ + public static final int AUTOCLICK_CURSOR_AREA_SIZE_MIN = 20; + + /** @hide */ + public static final int AUTOCLICK_CURSOR_AREA_SIZE_MAX = 100; + + /** @hide */ + public static final int AUTOCLICK_CURSOR_AREA_INCREMENT_SIZE = 20; + /** * Activity action: Launch UI to manage which accessibility service or feature is assigned * to the navigation bar Accessibility button. diff --git a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig index b3bd92b37357..c871d568e625 100644 --- a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig +++ b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig @@ -45,3 +45,10 @@ flag { description: "If true, the APIs to manage content protection device policy will be enabled." bug: "319477846" } + +flag { + name: "exported_settings_activity_enabled" + namespace: "content_protection" + description: "If true, the content protection Settings Activity will be exported for launching externally." + bug: "385310141" +} diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 71a832d84f08..99fe0cbdca25 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -18,7 +18,6 @@ package android.widget; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.graphics.Paint.NEW_FONT_VARIATION_MANAGEMENT; import static android.view.ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT; import static android.view.ContentInfo.SOURCE_AUTOFILL; import static android.view.ContentInfo.SOURCE_CLIPBOARD; @@ -5544,13 +5543,32 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return true; } - final boolean useFontVariationStore = Flags.typefaceRedesignReadonly() - && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT); boolean effective; - if (useFontVariationStore) { + if (Flags.typefaceRedesignReadonly()) { if (mFontWeightAdjustment != 0 && mFontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) { - mTextPaint.setFontVariationSettings(fontVariationSettings, mFontWeightAdjustment); + List<FontVariationAxis> axes = FontVariationAxis.fromFontVariationSettingsForList( + fontVariationSettings); + if (axes == null) { + return false; // invalid format of the font variation settings. + } + boolean wghtAdjusted = false; + for (int i = 0; i < axes.size(); ++i) { + FontVariationAxis axis = axes.get(i); + if (axis.getOpenTypeTagValue() == 0x77676874 /* wght */) { + axes.set(i, new FontVariationAxis("wght", + Math.clamp(axis.getStyleValue() + mFontWeightAdjustment, + FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX))); + wghtAdjusted = true; + } + } + if (!wghtAdjusted) { + axes.add(new FontVariationAxis("wght", + Math.clamp(400 + mFontWeightAdjustment, + FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX))); + } + mTextPaint.setFontVariationSettings( + FontVariationAxis.toFontVariationSettings(axes)); } else { mTextPaint.setFontVariationSettings(fontVariationSettings); } diff --git a/core/java/android/window/DisplayAreaOrganizer.java b/core/java/android/window/DisplayAreaOrganizer.java index 84ce247264f6..bd711fc79083 100644 --- a/core/java/android/window/DisplayAreaOrganizer.java +++ b/core/java/android/window/DisplayAreaOrganizer.java @@ -121,6 +121,14 @@ public class DisplayAreaOrganizer extends WindowOrganizer { public static final int FEATURE_WINDOWING_LAYER = FEATURE_SYSTEM_FIRST + 9; /** + * Display area for rendering app zoom out. When there are multiple layers on the screen, + * we want to render these layers based on a depth model. Here we zoom out the layer behind, + * whether it's an app or the homescreen. + * @hide + */ + public static final int FEATURE_APP_ZOOM_OUT = FEATURE_SYSTEM_FIRST + 10; + + /** * The last boundary of display area for system features */ public static final int FEATURE_SYSTEM_LAST = 10_000; diff --git a/core/java/android/window/WindowInfosListenerForTest.java b/core/java/android/window/WindowInfosListenerForTest.java index ac9bec305fff..6461f2a0fcda 100644 --- a/core/java/android/window/WindowInfosListenerForTest.java +++ b/core/java/android/window/WindowInfosListenerForTest.java @@ -103,12 +103,6 @@ public class WindowInfosListenerForTest { public final boolean isFocusable; /** - * True if the window is preventing splitting - */ - @SuppressLint("UnflaggedApi") // The API is only used for tests. - public final boolean isPreventSplitting; - - /** * True if the window duplicates touches received to wallpaper. */ @SuppressLint("UnflaggedApi") // The API is only used for tests. @@ -133,8 +127,6 @@ public class WindowInfosListenerForTest { this.transform = transform; this.isTouchable = (inputConfig & InputConfig.NOT_TOUCHABLE) == 0; this.isFocusable = (inputConfig & InputConfig.NOT_FOCUSABLE) == 0; - this.isPreventSplitting = (inputConfig - & InputConfig.PREVENT_SPLITTING) != 0; this.isDuplicateTouchToWallpaper = (inputConfig & InputConfig.DUPLICATE_TOUCH_TO_WALLPAPER) != 0; this.isWatchOutsideTouch = (inputConfig diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 20e3f6b93bd0..2911b0a6643d 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -464,7 +464,12 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { * Returns false if the legacy back behavior should be used. */ public boolean isOnBackInvokedCallbackEnabled() { - return isOnBackInvokedCallbackEnabled(mChecker.getContext()); + final Context hostContext = mChecker.getContext(); + if (hostContext == null) { + Log.w(TAG, "OnBackInvokedCallback is disabled, host context is removed!"); + return false; + } + return isOnBackInvokedCallbackEnabled(hostContext); } /** @@ -695,7 +700,12 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { */ public boolean checkApplicationCallbackRegistration(int priority, OnBackInvokedCallback callback) { - if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(getContext()) + final Context hostContext = getContext(); + if (hostContext == null) { + Log.w(TAG, "OnBackInvokedCallback is disabled, host context is removed!"); + return false; + } + if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(hostContext) && !(callback instanceof CompatOnBackInvokedCallback)) { Log.w(TAG, "OnBackInvokedCallback is not enabled for the application." @@ -720,7 +730,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { return true; } - private Context getContext() { + @Nullable private Context getContext() { return mContext.get(); } } diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index ccb1e2b4b652..be0b4fea459c 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -478,10 +478,10 @@ flag { } flag { - name: "enable_multiple_desktops" + name: "enable_multiple_desktops_frontend" namespace: "lse_desktop_experience" - description: "Enable multiple desktop sessions for desktop windowing." - bug: "379158791" + description: "Enable multiple desktop sessions for desktop windowing (frontend)." + bug: "362720309" } flag { @@ -531,8 +531,11 @@ flag { } flag { - name: "enable_desktop_wallpaper_activity_on_system_user" + name: "enable_desktop_wallpaper_activity_for_system_user" namespace: "lse_desktop_experience" description: "Enables starting DesktopWallpaperActivity on system user." bug: "385294350" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig index bb4770768cb1..8ff2e6aebdd0 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -91,6 +91,14 @@ flag { } flag { + name: "ignore_corner_radius_and_shadows" + namespace: "window_surfaces" + description: "Ignore the corner radius and shadows of a SurfaceControl" + bug: "375624570" + is_fixed_read_only: true +} # ignore_corner_radius_and_shadows + +flag { name: "jank_api" namespace: "window_surfaces" description: "Adds the jank data listener to AttachedSurfaceControl" diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index a8641326b1f2..7a1078f8718f 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -113,13 +113,6 @@ flag { } flag { - name: "predictive_back_system_anims" - namespace: "systemui" - description: "Predictive back for system animations" - bug: "320510464" -} - -flag { name: "remove_activity_starter_dream_callback" namespace: "windowing_frontend" description: "Avoid a race with DreamManagerService callbacks for isDreaming by checking Activity state directly" @@ -422,6 +415,17 @@ flag { } flag { + name: "keep_app_window_hide_while_locked" + namespace: "windowing_frontend" + description: "Do not let app window visible while device is locked" + is_fixed_read_only: true + bug: "378088391" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "port_window_size_animation" namespace: "windowing_frontend" description: "Port window-resize animation from legacy to shell" diff --git a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java index 0b1ecf78d28c..d03bb5c3cb17 100644 --- a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java +++ b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java @@ -29,6 +29,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.os.Build; import android.os.UserHandle; import android.provider.Settings; @@ -351,4 +352,24 @@ public final class AccessibilityUtils { } return result; } + + /** Returns the {@link ComponentName} of an installed accessibility service by label. */ + @Nullable + public static ComponentName getInstalledAccessibilityServiceComponentNameByLabel( + Context context, String label) { + AccessibilityManager accessibilityManager = + context.getSystemService(AccessibilityManager.class); + List<AccessibilityServiceInfo> serviceInfos = + accessibilityManager.getInstalledAccessibilityServiceList(); + + for (AccessibilityServiceInfo service : serviceInfos) { + final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; + if (label.equals(serviceInfo.loadLabel(context.getPackageManager()).toString()) + && (serviceInfo.applicationInfo.isSystemApp() + || serviceInfo.applicationInfo.isUpdatedSystemApp())) { + return new ComponentName(serviceInfo.packageName, serviceInfo.name); + } + } + return null; + } } diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java index 4aebde536dcf..972c2ea403e0 100644 --- a/core/java/com/android/internal/notification/SystemNotificationChannels.java +++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java @@ -49,6 +49,7 @@ public class SystemNotificationChannels { public static final String NETWORK_ALERTS = "NETWORK_ALERTS"; public static final String NETWORK_AVAILABLE = "NETWORK_AVAILABLE"; public static final String VPN = "VPN"; + public static final String TIME = "TIME"; /** * @deprecated Legacy device admin channel with low importance which is no longer used, * Use the high importance {@link #DEVICE_ADMIN} channel instead. @@ -67,6 +68,7 @@ public class SystemNotificationChannels { @Deprecated public static final String SYSTEM_CHANGES_DEPRECATED = "SYSTEM_CHANGES"; public static final String SYSTEM_CHANGES = "SYSTEM_CHANGES_ALERTS"; public static final String ACCESSIBILITY_MAGNIFICATION = "ACCESSIBILITY_MAGNIFICATION"; + public static final String ACCESSIBILITY_HEARING_DEVICE = "ACCESSIBILITY_HEARING_DEVICE"; public static final String ACCESSIBILITY_SECURITY_POLICY = "ACCESSIBILITY_SECURITY_POLICY"; public static final String ABUSIVE_BACKGROUND_APPS = "ABUSIVE_BACKGROUND_APPS"; @@ -145,6 +147,12 @@ public class SystemNotificationChannels { NotificationManager.IMPORTANCE_LOW); channelsList.add(vpn); + final NotificationChannel time = new NotificationChannel( + TIME, + context.getString(R.string.notification_channel_system_time), + NotificationManager.IMPORTANCE_DEFAULT); + channelsList.add(time); + final NotificationChannel deviceAdmin = new NotificationChannel( DEVICE_ADMIN, getDeviceAdminNotificationChannelName(context), @@ -203,6 +211,13 @@ public class SystemNotificationChannels { newFeaturePrompt.setBlockable(true); channelsList.add(newFeaturePrompt); + final NotificationChannel accessibilityHearingDeviceChannel = new NotificationChannel( + ACCESSIBILITY_HEARING_DEVICE, + context.getString(R.string.notification_channel_accessibility_hearing_device), + NotificationManager.IMPORTANCE_HIGH); + accessibilityHearingDeviceChannel.setBlockable(true); + channelsList.add(accessibilityHearingDeviceChannel); + final NotificationChannel accessibilitySecurityPolicyChannel = new NotificationChannel( ACCESSIBILITY_SECURITY_POLICY, context.getString(R.string.notification_channel_accessibility_security_policy), diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index c9c4be1e2c93..dc440e36ca0d 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -19,6 +19,7 @@ package com.android.internal.os; import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_FINISH; import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_START; import static android.os.BatteryStats.HistoryItem.EVENT_STATE_CHANGE; +import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER; import android.annotation.NonNull; import android.annotation.Nullable; @@ -215,6 +216,7 @@ public class BatteryStatsHistory { private final ArraySet<PowerStats.Descriptor> mWrittenPowerStatsDescriptors = new ArraySet<>(); private byte mLastHistoryStepLevel = 0; private boolean mMutable = true; + private int mIteratorCookie; private final BatteryStatsHistory mWritableHistory; private static class BatteryHistoryFile implements Comparable<BatteryHistoryFile> { @@ -289,6 +291,7 @@ public class BatteryStatsHistory { } void load() { + Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); mDirectory.mkdirs(); if (!mDirectory.exists()) { Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath()); @@ -325,8 +328,11 @@ public class BatteryStatsHistory { } } finally { unlock(); + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); } }); + } else { + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); } } @@ -418,6 +424,7 @@ public class BatteryStatsHistory { } void writeToParcel(Parcel out, boolean useBlobs) { + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel"); lock(); try { final long start = SystemClock.uptimeMillis(); @@ -443,6 +450,7 @@ public class BatteryStatsHistory { } } finally { unlock(); + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } } @@ -482,34 +490,39 @@ public class BatteryStatsHistory { } private void cleanup() { - if (mDirectory == null) { - return; - } - - if (!tryLock()) { - mCleanupNeeded = true; - return; - } - - mCleanupNeeded = false; + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.cleanup"); try { - // if free disk space is less than 100MB, delete oldest history file. - if (!hasFreeDiskSpace(mDirectory)) { - BatteryHistoryFile oldest = mHistoryFiles.remove(0); - oldest.atomicFile.delete(); + if (mDirectory == null) { + return; + } + + if (!tryLock()) { + mCleanupNeeded = true; + return; } - // if there is more history stored than allowed, delete oldest history files. - int size = getSize(); - while (size > mMaxHistorySize) { - BatteryHistoryFile oldest = mHistoryFiles.get(0); - int length = (int) oldest.atomicFile.getBaseFile().length(); - oldest.atomicFile.delete(); - mHistoryFiles.remove(0); - size -= length; + mCleanupNeeded = false; + try { + // if free disk space is less than 100MB, delete oldest history file. + if (!hasFreeDiskSpace(mDirectory)) { + BatteryHistoryFile oldest = mHistoryFiles.remove(0); + oldest.atomicFile.delete(); + } + + // if there is more history stored than allowed, delete oldest history files. + int size = getSize(); + while (size > mMaxHistorySize) { + BatteryHistoryFile oldest = mHistoryFiles.get(0); + int length = (int) oldest.atomicFile.getBaseFile().length(); + oldest.atomicFile.delete(); + mHistoryFiles.remove(0); + size -= length; + } + } finally { + unlock(); } } finally { - unlock(); + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } } } @@ -710,13 +723,18 @@ public class BatteryStatsHistory { * in the system directory, so it is not safe while actively writing history. */ public BatteryStatsHistory copy() { - synchronized (this) { - // Make a copy of battery history to avoid concurrent modification. - Parcel historyBufferCopy = Parcel.obtain(); - historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.copy"); + try { + synchronized (this) { + // Make a copy of battery history to avoid concurrent modification. + Parcel historyBufferCopy = Parcel.obtain(); + historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); - return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, null, - null, mEventLogger, this); + return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, + null, null, mEventLogger, this); + } + } finally { + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } } @@ -826,7 +844,7 @@ public class BatteryStatsHistory { */ @NonNull public BatteryStatsHistoryIterator iterate(long startTimeMs, long endTimeMs) { - if (mMutable) { + if (mMutable || mIteratorCookie != 0) { return copy().iterate(startTimeMs, endTimeMs); } @@ -837,7 +855,12 @@ public class BatteryStatsHistory { mCurrentParcel = null; mCurrentParcelEnd = 0; mParcelIndex = 0; - return new BatteryStatsHistoryIterator(this, startTimeMs, endTimeMs); + BatteryStatsHistoryIterator iterator = new BatteryStatsHistoryIterator( + this, startTimeMs, endTimeMs); + mIteratorCookie = System.identityHashCode(iterator); + Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate", + mIteratorCookie); + return iterator; } /** @@ -848,6 +871,9 @@ public class BatteryStatsHistory { if (mHistoryDir != null) { mHistoryDir.unlock(); } + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate", + mIteratorCookie); + mIteratorCookie = 0; } /** @@ -949,28 +975,33 @@ public class BatteryStatsHistory { * @return true if success, false otherwise. */ public boolean readFileToParcel(Parcel out, AtomicFile file) { - byte[] raw = null; + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read"); try { - final long start = SystemClock.uptimeMillis(); - raw = file.readFully(); - if (DEBUG) { - Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath() - + " duration ms:" + (SystemClock.uptimeMillis() - start)); + byte[] raw = null; + try { + final long start = SystemClock.uptimeMillis(); + raw = file.readFully(); + if (DEBUG) { + Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath() + + " duration ms:" + (SystemClock.uptimeMillis() - start)); + } + } catch (Exception e) { + Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); + return false; } - } catch (Exception e) { - Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); - return false; - } - out.unmarshall(raw, 0, raw.length); - out.setDataPosition(0); - if (!verifyVersion(out)) { - return false; + out.unmarshall(raw, 0, raw.length); + out.setDataPosition(0); + if (!verifyVersion(out)) { + return false; + } + // skip monotonic time field. + out.readLong(); + // skip monotonic size field + out.readLong(); + return true; + } finally { + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } - // skip monotonic time field. - out.readLong(); - // skip monotonic size field - out.readLong(); - return true; } /** diff --git a/core/java/com/android/internal/policy/IKeyguardService.aidl b/core/java/com/android/internal/policy/IKeyguardService.aidl index d62c8f378af0..73c2265d5f6e 100644 --- a/core/java/com/android/internal/policy/IKeyguardService.aidl +++ b/core/java/com/android/internal/policy/IKeyguardService.aidl @@ -53,21 +53,21 @@ oneway interface IKeyguardService { * * @param pmSleepReason One of PowerManager.GO_TO_SLEEP_REASON_*, detailing the specific reason * we're going to sleep, such as GO_TO_SLEEP_REASON_POWER_BUTTON or GO_TO_SLEEP_REASON_TIMEOUT. - * @param cameraGestureTriggered whether the camera gesture was triggered between - * {@link #onStartedGoingToSleep} and this method; if it's been - * triggered, we shouldn't lock the device. + * @param powerButtonLaunchGestureTriggered whether the power button double tap gesture was + * triggered between {@link #onStartedGoingToSleep} and this + * method; if it's been triggered, we shouldn't lock the device. */ - void onFinishedGoingToSleep(int pmSleepReason, boolean cameraGestureTriggered); + void onFinishedGoingToSleep(int pmSleepReason, boolean powerButtonLaunchGestureTriggered); /** * Called when the device has started waking up. * @param pmWakeReason One of PowerManager.WAKE_REASON_*, detailing the reason we're waking up, * such as WAKE_REASON_POWER_BUTTON or WAKE_REASON_GESTURE. - * @param cameraGestureTriggered Whether we're waking up due to a power button double tap - * gesture. + * @param powerButtonLaunchGestureTriggered Whether we're waking up due to a power button + * double tap gesture. */ - void onStartedWakingUp(int pmWakeReason, boolean cameraGestureTriggered); + void onStartedWakingUp(int pmWakeReason, boolean powerButtonLaunchGestureTriggered); /** * Called when the device has finished waking up. diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java index 7f7ea8b28546..37500766a4ac 100644 --- a/core/java/com/android/internal/security/VerityUtils.java +++ b/core/java/com/android/internal/security/VerityUtils.java @@ -36,7 +36,6 @@ import com.android.internal.org.bouncycastle.cms.SignerInformationVerifier; import com.android.internal.org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import com.android.internal.org.bouncycastle.operator.OperatorCreationException; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -53,12 +52,6 @@ import java.security.cert.X509Certificate; public abstract class VerityUtils { private static final String TAG = "VerityUtils"; - /** - * File extension of the signature file. For example, foo.apk.fsv_sig is the signature file of - * foo.apk. - */ - public static final String FSVERITY_SIGNATURE_FILE_EXTENSION = ".fsv_sig"; - /** SHA256 hash size. */ private static final int HASH_SIZE_BYTES = 32; @@ -67,16 +60,6 @@ public abstract class VerityUtils { || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2; } - /** Returns true if the given file looks like containing an fs-verity signature. */ - public static boolean isFsveritySignatureFile(File file) { - return file.getName().endsWith(FSVERITY_SIGNATURE_FILE_EXTENSION); - } - - /** Returns the fs-verity signature file path of the given file. */ - public static String getFsveritySignatureFilePath(String filePath) { - return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION; - } - /** Enables fs-verity for the file without signature. */ public static void setUpFsverity(@NonNull String filePath) throws IOException { int errno = enableFsverityNative(filePath); diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index f14e1f63cdf6..ec0954d5590a 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -239,4 +239,7 @@ interface IStatusBarService /** Unbundle a categorized notification */ void unbundleNotification(String key); + + /** Rebundle an (un)categorized notification */ + void rebundleNotification(String key); } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 39ddea614ee4..74707703f5f2 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -65,6 +65,7 @@ import android.util.SparseLongArray; import android.view.InputDevice; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.google.android.collect.Lists; @@ -75,6 +76,7 @@ import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -292,6 +294,56 @@ public class LockPatternUtils { } + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static byte[] newNonMovableByteArray(int length) { + if (!android.security.Flags.secureArrayZeroization()) { + return new byte[length]; + } + return ArrayUtils.newNonMovableByteArray(length); + } + + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static char[] newNonMovableCharArray(int length) { + if (!android.security.Flags.secureArrayZeroization()) { + return new char[length]; + } + return ArrayUtils.newNonMovableCharArray(length); + } + + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static void zeroize(byte[] array) { + if (!android.security.Flags.secureArrayZeroization()) { + if (array != null) { + Arrays.fill(array, (byte) 0); + } + return; + } + ArrayUtils.zeroize(array); + } + + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static void zeroize(char[] array) { + if (!android.security.Flags.secureArrayZeroization()) { + if (array != null) { + Arrays.fill(array, (char) 0); + } + return; + } + ArrayUtils.zeroize(array); + } + @UnsupportedAppUsage public DevicePolicyManager getDevicePolicyManager() { if (mDevicePolicyManager == null) { diff --git a/core/java/com/android/internal/widget/LockscreenCredential.java b/core/java/com/android/internal/widget/LockscreenCredential.java index 54b9a225f944..92ce990c67df 100644 --- a/core/java/com/android/internal/widget/LockscreenCredential.java +++ b/core/java/com/android/internal/widget/LockscreenCredential.java @@ -246,7 +246,7 @@ public class LockscreenCredential implements Parcelable, AutoCloseable { */ public void zeroize() { if (mCredential != null) { - Arrays.fill(mCredential, (byte) 0); + LockPatternUtils.zeroize(mCredential); mCredential = null; } } @@ -346,7 +346,7 @@ public class LockscreenCredential implements Parcelable, AutoCloseable { byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword); byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword); - Arrays.fill(saltedPassword, (byte) 0); + LockPatternUtils.zeroize(saltedPassword); return HexEncoding.encodeToString(ArrayUtils.concat(sha1, md5)); } catch (NoSuchAlgorithmException e) { throw new AssertionError("Missing digest algorithm: ", e); diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java index 80bc4fd89c8d..dd12f69a56fb 100644 --- a/core/java/com/android/internal/widget/NotificationExpandButton.java +++ b/core/java/com/android/internal/widget/NotificationExpandButton.java @@ -56,8 +56,6 @@ public class NotificationExpandButton extends FrameLayout { private int mDefaultTextColor; private int mHighlightPillColor; private int mHighlightTextColor; - // Track whether this ever had mExpanded = true, so that we don't highlight it anymore. - private boolean mWasExpanded = false; public NotificationExpandButton(Context context) { this(context, null, 0, 0); @@ -136,7 +134,6 @@ public class NotificationExpandButton extends FrameLayout { int contentDescriptionId; if (mExpanded) { if (notificationsRedesignTemplates()) { - mWasExpanded = true; drawableId = R.drawable.ic_notification_2025_collapse; } else { drawableId = R.drawable.ic_collapse_notification; @@ -156,8 +153,6 @@ public class NotificationExpandButton extends FrameLayout { if (!notificationsRedesignTemplates()) { // changing the expanded state can affect the number display updateNumber(); - } else { - updateColors(); } } @@ -197,43 +192,22 @@ public class NotificationExpandButton extends FrameLayout { ); } - /** - * Use highlight colors for the expander for groups (when the number is showing) that haven't - * been opened before, as long as the colors are available. - */ - private boolean shouldBeHighlighted() { - return !mWasExpanded && shouldShowNumber() - && mHighlightPillColor != 0 && mHighlightTextColor != 0; - } - private void updateColors() { - if (notificationsRedesignTemplates()) { - if (shouldBeHighlighted()) { + if (shouldShowNumber()) { + if (mHighlightPillColor != 0) { mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor)); - mIconView.setColorFilter(mHighlightTextColor); + } + mIconView.setColorFilter(mHighlightTextColor); + if (mHighlightTextColor != 0) { mNumberView.setTextColor(mHighlightTextColor); - } else { - mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor)); - mIconView.setColorFilter(mDefaultTextColor); - mNumberView.setTextColor(mDefaultTextColor); } } else { - if (shouldShowNumber()) { - if (mHighlightPillColor != 0) { - mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor)); - } - mIconView.setColorFilter(mHighlightTextColor); - if (mHighlightTextColor != 0) { - mNumberView.setTextColor(mHighlightTextColor); - } - } else { - if (mDefaultPillColor != 0) { - mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor)); - } - mIconView.setColorFilter(mDefaultTextColor); - if (mDefaultTextColor != 0) { - mNumberView.setTextColor(mDefaultTextColor); - } + if (mDefaultPillColor != 0) { + mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor)); + } + mIconView.setColorFilter(mDefaultTextColor); + if (mDefaultTextColor != 0) { + mNumberView.setTextColor(mDefaultTextColor); } } } diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java index 8cd7843fe1d9..904b73f41e70 100644 --- a/core/java/com/android/internal/widget/NotificationProgressBar.java +++ b/core/java/com/android/internal/widget/NotificationProgressBar.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -31,6 +32,7 @@ import android.graphics.drawable.LayerDrawable; import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.view.RemotableViewMethod; import android.widget.ProgressBar; import android.widget.RemoteViews; @@ -40,14 +42,15 @@ import androidx.annotation.ColorInt; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; -import com.android.internal.widget.NotificationProgressDrawable.Part; -import com.android.internal.widget.NotificationProgressDrawable.Point; -import com.android.internal.widget.NotificationProgressDrawable.Segment; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePart; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint; +import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; @@ -56,18 +59,26 @@ import java.util.TreeSet; * represent Notification ProgressStyle progress, such as for ridesharing and navigation. */ @RemoteViews.RemoteView -public final class NotificationProgressBar extends ProgressBar { +public final class NotificationProgressBar extends ProgressBar implements + NotificationProgressDrawable.BoundsChangeListener { private static final String TAG = "NotificationProgressBar"; + private static final boolean DEBUG = false; private NotificationProgressDrawable mNotificationProgressDrawable; + private final Rect mProgressDrawableBounds = new Rect(); private NotificationProgressModel mProgressModel; @Nullable - private List<Part> mProgressDrawableParts = null; + private List<Part> mParts = null; + + // List of drawable parts before segment splitting by process. + @Nullable + private List<DrawablePart> mProgressDrawableParts = null; @Nullable private Drawable mTracker = null; + private boolean mHasTrackerIcon = false; /** @see R.styleable#NotificationProgressBar_trackerHeight */ private final int mTrackerHeight; @@ -76,7 +87,13 @@ public final class NotificationProgressBar extends ProgressBar { private final Matrix mMatrix = new Matrix(); private Matrix mTrackerDrawMatrix = null; - private float mScale = 0; + private float mProgressFraction = 0; + /** + * The location of progress on the stretched and rescaled progress bar, in fraction. Used for + * calculating the tracker position. If stretching and rescaling is not needed, == + * mProgressFraction. + */ + private float mAdjustedProgressFraction = 0; /** Indicates whether mTrackerPos needs to be recalculated before the tracker is drawn. */ private boolean mTrackerPosIsDirty = false; @@ -96,20 +113,21 @@ public final class NotificationProgressBar extends ProgressBar { int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - final TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes); saveAttributeDataForStyleable(context, R.styleable.NotificationProgressBar, attrs, a, defStyleAttr, defStyleRes); try { mNotificationProgressDrawable = getNotificationProgressDrawable(); + mNotificationProgressDrawable.setBoundsChangeListener(this); } catch (IllegalStateException ex) { Log.e(TAG, "Can't get NotificationProgressDrawable", ex); } // Supports setting the tracker in xml, but ProgressStyle notifications set/override it - // via {@code setProgressTrackerIcon}. + // via {@code #setProgressTrackerIcon}. final Drawable tracker = a.getDrawable(R.styleable.NotificationProgressBar_tracker); setTracker(tracker); @@ -126,8 +144,7 @@ public final class NotificationProgressBar extends ProgressBar { */ @RemotableViewMethod public void setProgressModel(@Nullable Bundle bundle) { - Preconditions.checkArgument(bundle != null, - "Bundle shouldn't be null"); + Preconditions.checkArgument(bundle != null, "Bundle shouldn't be null"); mProgressModel = NotificationProgressModel.fromBundle(bundle); final boolean isIndeterminate = mProgressModel.isIndeterminate(); @@ -137,20 +154,25 @@ public final class NotificationProgressBar extends ProgressBar { final int indeterminateColor = mProgressModel.getIndeterminateColor(); setIndeterminateTintList(ColorStateList.valueOf(indeterminateColor)); } else { + // TODO: b/372908709 - maybe don't rerun the entire calculation every time the + // progress model is updated? For example, if the segments and parts aren't changed, + // there is no need to call `processAndConvertToViewParts` again. + final int progress = mProgressModel.getProgress(); final int progressMax = mProgressModel.getProgressMax(); - mProgressDrawableParts = processAndConvertToDrawableParts(mProgressModel.getSegments(), + + mParts = processAndConvertToViewParts(mProgressModel.getSegments(), mProgressModel.getPoints(), progress, - progressMax, - mProgressModel.isStyledByProgress()); - - if (mNotificationProgressDrawable != null) { - mNotificationProgressDrawable.setParts(mProgressDrawableParts); - } + progressMax); setMax(progressMax); setProgress(progress); + + if (mNotificationProgressDrawable != null + && mNotificationProgressDrawable.getBounds().width() != 0) { + updateDrawableParts(); + } } } @@ -200,9 +222,7 @@ public final class NotificationProgressBar extends ProgressBar { } else { progressTrackerDrawable = null; } - return () -> { - setTracker(progressTrackerDrawable); - }; + return () -> setTracker(progressTrackerDrawable); } private void setTracker(@Nullable Drawable tracker) { @@ -226,8 +246,14 @@ public final class NotificationProgressBar extends ProgressBar { final boolean trackerSizeChanged = trackerSizeChanged(tracker, mTracker); mTracker = tracker; - if (mNotificationProgressDrawable != null) { - mNotificationProgressDrawable.setHasTrackerIcon(mTracker != null); + final boolean hasTrackerIcon = (mTracker != null); + if (mHasTrackerIcon != hasTrackerIcon) { + mHasTrackerIcon = hasTrackerIcon; + if (mNotificationProgressDrawable != null + && mNotificationProgressDrawable.getBounds().width() != 0 + && mProgressModel.isStyledByProgress()) { + updateDrawableParts(); + } } configureTrackerBounds(); @@ -293,6 +319,8 @@ public final class NotificationProgressBar extends ProgressBar { mTrackerDrawMatrix.postTranslate(Math.round(dx), Math.round(dy)); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setProgress(int progress) { super.setProgress(progress); @@ -300,6 +328,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public void setProgress(int progress, boolean animate) { // Animation isn't supported by NotificationProgressBar. @@ -308,6 +338,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setMin(int min) { super.setMin(min); @@ -315,6 +347,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setMax(int max) { super.setMax(max); @@ -323,10 +357,10 @@ public final class NotificationProgressBar extends ProgressBar { } private void onMaybeVisualProgressChanged() { - float scale = getScale(); - if (mScale == scale) return; + float progressFraction = getProgressFraction(); + if (mProgressFraction == progressFraction) return; - mScale = scale; + mProgressFraction = progressFraction; mTrackerPosIsDirty = true; invalidate(); } @@ -350,8 +384,7 @@ public final class NotificationProgressBar extends ProgressBar { super.drawableStateChanged(); final Drawable tracker = mTracker; - if (tracker != null && tracker.isStateful() - && tracker.setState(getDrawableState())) { + if (tracker != null && tracker.isStateful() && tracker.setState(getDrawableState())) { invalidateDrawable(tracker); } } @@ -372,6 +405,65 @@ public final class NotificationProgressBar extends ProgressBar { updateTrackerAndBarPos(w, h); } + @Override + public void onDrawableBoundsChanged() { + final Rect progressDrawableBounds = mNotificationProgressDrawable.getBounds(); + + if (mProgressDrawableBounds.equals(progressDrawableBounds)) return; + + if (mProgressDrawableBounds.width() != progressDrawableBounds.width()) { + updateDrawableParts(); + } + + mProgressDrawableBounds.set(progressDrawableBounds); + } + + private void updateDrawableParts() { + if (DEBUG) { + Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = " + + mNotificationProgressDrawable + ", mParts = " + mParts); + } + + if (mNotificationProgressDrawable == null) return; + if (mParts == null) return; + + final float width = mNotificationProgressDrawable.getBounds().width(); + if (width == 0) { + if (mProgressDrawableParts != null) { + if (DEBUG) { + Log.d(TAG, "Clearing mProgressDrawableParts"); + } + mProgressDrawableParts.clear(); + mNotificationProgressDrawable.setParts(mProgressDrawableParts); + } + return; + } + + mProgressDrawableParts = processAndConvertToDrawableParts( + mParts, + width, + mNotificationProgressDrawable.getSegSegGap(), + mNotificationProgressDrawable.getSegPointGap(), + mNotificationProgressDrawable.getPointRadius(), + mHasTrackerIcon + ); + Pair<List<DrawablePart>, Float> p = maybeStretchAndRescaleSegments( + mParts, + mProgressDrawableParts, + mNotificationProgressDrawable.getSegmentMinWidth(), + mNotificationProgressDrawable.getPointRadius(), + getProgressFraction(), + width, + mProgressModel.isStyledByProgress(), + mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap()); + + if (DEBUG) { + Log.d(TAG, "Updating NotificationProgressDrawable parts"); + } + mNotificationProgressDrawable.setParts(p.first); + mAdjustedProgressFraction = p.second / width; + } + private void updateTrackerAndBarPos(int w, int h) { final int paddedHeight = h - mPaddingTop - mPaddingBottom; final Drawable bar = getCurrentDrawable(); @@ -402,11 +494,11 @@ public final class NotificationProgressBar extends ProgressBar { } if (tracker != null) { - setTrackerPos(w, tracker, mScale, trackerOffsetY); + setTrackerPos(w, tracker, mAdjustedProgressFraction, trackerOffsetY); } } - private float getScale() { + private float getProgressFraction() { int min = getMin(); int max = getMax(); int range = max - min; @@ -416,19 +508,19 @@ public final class NotificationProgressBar extends ProgressBar { /** * Updates the tracker drawable bounds. * - * @param w Width of the view, including padding - * @param tracker Drawable used for the tracker - * @param scale Current progress between 0 and 1 - * @param offsetY Vertical offset for centering. If set to - * {@link Integer#MIN_VALUE}, the current offset will be used. + * @param w Width of the view, including padding + * @param tracker Drawable used for the tracker + * @param progressFraction Current progress between 0 and 1 + * @param offsetY Vertical offset for centering. If set to + * {@link Integer#MIN_VALUE}, the current offset will be used. */ - private void setTrackerPos(int w, Drawable tracker, float scale, int offsetY) { + private void setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY) { int available = w - mPaddingLeft - mPaddingRight; final int trackerWidth = tracker.getIntrinsicWidth(); final int trackerHeight = tracker.getIntrinsicHeight(); available -= ((mTrackerHeight <= 0) ? trackerWidth : mTrackerWidth); - final int trackerPos = (int) (scale * available + 0.5f); + final int trackerPos = (int) (progressFraction * available + 0.5f); final int top, bottom; if (offsetY == Integer.MIN_VALUE) { @@ -448,8 +540,8 @@ public final class NotificationProgressBar extends ProgressBar { if (background != null) { final int bkgOffsetX = mPaddingLeft; final int bkgOffsetY = mPaddingTop; - background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, - right + bkgOffsetX, bottom + bkgOffsetY); + background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, right + bkgOffsetX, + bottom + bkgOffsetY); } // Canvas will be translated, so 0,0 is where we start drawing @@ -482,7 +574,7 @@ public final class NotificationProgressBar extends ProgressBar { if (mTracker == null) return; if (mTrackerPosIsDirty) { - setTrackerPos(getWidth(), mTracker, mScale, Integer.MIN_VALUE); + setTrackerPos(getWidth(), mTracker, mAdjustedProgressFraction, Integer.MIN_VALUE); } final int saveCount = canvas.save(); @@ -531,7 +623,7 @@ public final class NotificationProgressBar extends ProgressBar { final Drawable tracker = mTracker; if (tracker != null) { - setTrackerPos(getWidth(), tracker, mScale, Integer.MIN_VALUE); + setTrackerPos(getWidth(), tracker, mAdjustedProgressFraction, Integer.MIN_VALUE); // Since we draw translated, the drawable's bounds that it signals // for invalidation won't be the actual bounds we want invalidated, @@ -541,16 +633,14 @@ public final class NotificationProgressBar extends ProgressBar { } /** - * Processes the ProgressStyle data and convert to list of {@code - * NotificationProgressDrawable.Part}. + * Processes the ProgressStyle data and convert to a list of {@code Part}. */ @VisibleForTesting - public static List<Part> processAndConvertToDrawableParts( + public static List<Part> processAndConvertToViewParts( List<ProgressStyle.Segment> segments, List<ProgressStyle.Point> points, int progress, - int progressMax, - boolean isStyledByProgress + int progressMax ) { if (segments.isEmpty()) { throw new IllegalArgumentException("List of segments shouldn't be empty"); @@ -571,6 +661,7 @@ public final class NotificationProgressBar extends ProgressBar { if (progress < 0 || progress > progressMax) { throw new IllegalArgumentException("Invalid progress : " + progress); } + for (ProgressStyle.Point point : points) { final int pos = point.getPosition(); if (pos < 0 || pos > progressMax) { @@ -583,23 +674,21 @@ public final class NotificationProgressBar extends ProgressBar { final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap( points); final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap, - positionToPointMap, progress, isStyledByProgress); + positionToPointMap); - final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = - splitSegmentsByPointsAndProgress( - startToSegmentMap, sortedPos, progressMax); + final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = splitSegmentsByPoints( + startToSegmentMap, sortedPos, progressMax); - return convertToDrawableParts(startToSplitSegmentMap, positionToPointMap, sortedPos, - progress, progressMax, - isStyledByProgress); + return convertToViewParts(startToSplitSegmentMap, positionToPointMap, sortedPos, + progressMax); } // Any segment with a point on it gets split by the point. - // If isStyledByProgress is true, also split the segment with the progress value in its range. - private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPointsAndProgress( + private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPoints( Map<Integer, ProgressStyle.Segment> startToSegmentMap, SortedSet<Integer> sortedPos, - int progressMax) { + int progressMax + ) { int prevSegStart = 0; for (Integer pos : sortedPos) { if (pos == 0 || pos == progressMax) continue; @@ -610,8 +699,7 @@ public final class NotificationProgressBar extends ProgressBar { final ProgressStyle.Segment prevSeg = startToSegmentMap.get(prevSegStart); final ProgressStyle.Segment leftSeg = new ProgressStyle.Segment( - pos - prevSegStart).setColor( - prevSeg.getColor()); + pos - prevSegStart).setColor(prevSeg.getColor()); final ProgressStyle.Segment rightSeg = new ProgressStyle.Segment( prevSegStart + prevSeg.getLength() - pos).setColor(prevSeg.getColor()); @@ -624,32 +712,21 @@ public final class NotificationProgressBar extends ProgressBar { return startToSegmentMap; } - private static List<Part> convertToDrawableParts( + private static List<Part> convertToViewParts( Map<Integer, ProgressStyle.Segment> startToSegmentMap, Map<Integer, ProgressStyle.Point> positionToPointMap, SortedSet<Integer> sortedPos, - int progress, - int progressMax, - boolean isStyledByProgress + int progressMax ) { List<Part> parts = new ArrayList<>(); - boolean styleRemainingParts = false; for (Integer pos : sortedPos) { if (positionToPointMap.containsKey(pos)) { final ProgressStyle.Point point = positionToPointMap.get(pos); - final int color = maybeGetFadedColor(point.getColor(), styleRemainingParts); - parts.add(new Point(null, color, styleRemainingParts)); - } - // We want the Point at the current progress to be filled (not faded), but a Segment - // starting at this progress to be faded. - if (isStyledByProgress && !styleRemainingParts && pos == progress) { - styleRemainingParts = true; + parts.add(new Point(point.getColor())); } if (startToSegmentMap.containsKey(pos)) { final ProgressStyle.Segment seg = startToSegmentMap.get(pos); - final int color = maybeGetFadedColor(seg.getColor(), styleRemainingParts); - parts.add(new Segment( - (float) seg.getLength() / progressMax, color, styleRemainingParts)); + parts.add(new Segment((float) seg.getLength() / progressMax, seg.getColor())); } } @@ -660,11 +737,24 @@ public final class NotificationProgressBar extends ProgressBar { private static int maybeGetFadedColor(@ColorInt int color, boolean fade) { if (!fade) return color; - return NotificationProgressDrawable.getFadedColor(color); + return getFadedColor(color); + } + + /** + * Get a color with an opacity that's 40% of the input color. + */ + @ColorInt + static int getFadedColor(@ColorInt int color) { + return Color.argb( + (int) (Color.alpha(color) * 0.4f + 0.5f), + Color.red(color), + Color.green(color), + Color.blue(color)); } private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap( - List<ProgressStyle.Segment> segments) { + List<ProgressStyle.Segment> segments + ) { final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>(); int currentStart = 0; // Initial start position is 0 @@ -681,7 +771,8 @@ public final class NotificationProgressBar extends ProgressBar { } private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap( - List<ProgressStyle.Point> points) { + List<ProgressStyle.Point> points + ) { final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>(); for (ProgressStyle.Point point : points) { @@ -693,14 +784,392 @@ public final class NotificationProgressBar extends ProgressBar { private static SortedSet<Integer> generateSortedPositionSet( Map<Integer, ProgressStyle.Segment> startToSegmentMap, - Map<Integer, ProgressStyle.Point> positionToPointMap, int progress, - boolean isStyledByProgress) { + Map<Integer, ProgressStyle.Point> positionToPointMap + ) { final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet()); sortedPos.addAll(positionToPointMap.keySet()); - if (isStyledByProgress) { - sortedPos.add(progress); - } return sortedPos; } + + /** + * Processes the list of {@code Part} and convert to a list of {@code DrawablePart}. + */ + @VisibleForTesting + public static List<DrawablePart> processAndConvertToDrawableParts( + List<Part> parts, + float totalWidth, + float segSegGap, + float segPointGap, + float pointRadius, + boolean hasTrackerIcon + ) { + List<DrawablePart> drawableParts = new ArrayList<>(); + + // generally, we will start drawing at (x, y) and end at (x+w, y) + float x = (float) 0; + + final int nParts = parts.size(); + for (int iPart = 0; iPart < nParts; iPart++) { + final Part part = parts.get(iPart); + final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1); + final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1); + if (part instanceof Segment segment) { + final float segWidth = segment.mFraction * totalWidth; + // Advance the start position to account for a point immediately prior. + final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x); + final float start = x + startOffset; + // Retract the end position to account for the padding and a point immediately + // after. + final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, + segSegGap, x + segWidth, totalWidth, hasTrackerIcon); + final float end = x + segWidth - endOffset; + + drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded)); + + segment.mStart = x; + segment.mEnd = x + segWidth; + + // Advance the current position to account for the segment's fraction of the total + // width (ignoring offset and padding) + x += segWidth; + } else if (part instanceof Point point) { + final float pointWidth = 2 * pointRadius; + float start = x - pointRadius; + if (start < 0) start = 0; + float end = start + pointWidth; + if (end > totalWidth) { + end = totalWidth; + if (totalWidth > pointWidth) start = totalWidth - pointWidth; + } + + drawableParts.add(new DrawablePoint(start, end, point.mColor)); + } + } + + return drawableParts; + } + + private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, + float startX) { + if (!(prevPart instanceof Point)) return 0F; + final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0; + return pointOffset + pointRadius + segPointGap; + } + + private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, + float segPointGap, float segSegGap, float endX, float totalWidth, + boolean hasTrackerIcon) { + if (nextPart == null) return 0F; + if (nextPart instanceof Segment nextSeg) { + if (!seg.mFaded && nextSeg.mFaded) { + // @see Segment#mFaded + return hasTrackerIcon ? 0F : segSegGap; + } + return segSegGap; + } + + final float pointWidth = 2 * pointRadius; + final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth) + ? (endX + pointRadius - totalWidth) : 0; + return segPointGap + pointRadius + pointOffset; + } + + /** + * Processes the list of {@code DrawablePart} data and convert to a pair of: + * - list of processed {@code DrawablePart}. + * - location of progress on the stretched and rescaled progress bar. + */ + @VisibleForTesting + public static Pair<List<DrawablePart>, Float> maybeStretchAndRescaleSegments( + List<Part> parts, + List<DrawablePart> drawableParts, + float segmentMinWidth, + float pointRadius, + float progressFraction, + float totalWidth, + boolean isStyledByProgress, + float progressGap + ) { + final List<DrawableSegment> drawableSegments = drawableParts + .stream() + .filter(DrawableSegment.class::isInstance) + .map(DrawableSegment.class::cast) + .toList(); + float totalExcessWidth = 0; + float totalPositiveExcessWidth = 0; + for (DrawableSegment drawableSegment : drawableSegments) { + final float excessWidth = drawableSegment.getWidth() - segmentMinWidth; + totalExcessWidth += excessWidth; + if (excessWidth > 0) totalPositiveExcessWidth += excessWidth; + } + + // All drawable segments are above minimum width. No need to stretch and rescale. + if (totalExcessWidth == totalPositiveExcessWidth) { + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + if (totalExcessWidth < 0) { + // TODO: b/372908709 - throw an error so that the caller can catch and go to fallback + // option. (instead of return.) + Log.w(TAG, "Not enough width to satisfy the minimum width for segments."); + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + final int nParts = drawableParts.size(); + float startOffset = 0; + for (int iPart = 0; iPart < nParts; iPart++) { + final DrawablePart drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof DrawableSegment drawableSegment) { + final float origDrawableSegmentWidth = drawableSegment.getWidth(); + + float drawableSegmentWidth = segmentMinWidth; + // Allocate the totalExcessWidth to the segments above minimum, proportionally to + // their initial excessWidth. + if (origDrawableSegmentWidth > segmentMinWidth) { + drawableSegmentWidth += + totalExcessWidth * (origDrawableSegmentWidth - segmentMinWidth) + / totalPositiveExcessWidth; + } + + final float widthDiff = drawableSegmentWidth - drawableSegment.getWidth(); + + // Adjust drawable segments to new widths + drawableSegment.setStart(drawableSegment.getStart() + startOffset); + drawableSegment.setEnd( + drawableSegment.getStart() + origDrawableSegmentWidth + widthDiff); + + // Also adjust view segments to new width. (For view segments, only start is + // needed?) + // Check that segments and drawableSegments are of the same size? + final Segment segment = (Segment) parts.get(iPart); + final float origSegmentWidth = segment.getWidth(); + segment.mStart = segment.mStart + startOffset; + segment.mEnd = segment.mStart + origSegmentWidth + widthDiff; + + // Increase startOffset for the subsequent segments. + startOffset += widthDiff; + } else if (drawablePart instanceof DrawablePoint drawablePoint) { + drawablePoint.setStart(drawablePoint.getStart() + startOffset); + drawablePoint.setEnd(drawablePoint.getStart() + 2 * pointRadius); + } + } + + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + /** + * Find the location of progress on the stretched and rescaled progress bar. + * If isStyledByProgress is true, also split the drawable segment with the progress value in its + * range. Style the drawable parts after process with reduced opacity and segment height. + */ + private static Pair<List<DrawablePart>, Float> maybeSplitDrawableSegmentsByProgress( + // Needed to get the original segment start and end positions in pixels. + List<Part> parts, + List<DrawablePart> drawableParts, + float progressFraction, + float totalWidth, + boolean isStyledByProgress, + float progressGap + ) { + if (progressFraction == 1) return new Pair<>(drawableParts, totalWidth); + + int iPartFirstSegmentToStyle = -1; + int iPartSegmentToSplit = -1; + float rescaledProgressX = 0; + float startFraction = 0; + final int nParts = parts.size(); + for (int iPart = 0; iPart < nParts; iPart++) { + final Part part = parts.get(iPart); + if (!(part instanceof Segment)) continue; + final Segment segment = (Segment) part; + if (startFraction == progressFraction) { + iPartFirstSegmentToStyle = iPart; + rescaledProgressX = segment.mStart; + break; + } else if (startFraction < progressFraction + && progressFraction < startFraction + segment.mFraction) { + iPartSegmentToSplit = iPart; + rescaledProgressX = segment.mStart + + (progressFraction - startFraction) / segment.mFraction + * segment.getWidth(); + break; + } + startFraction += segment.mFraction; + } + + if (!isStyledByProgress) return new Pair<>(drawableParts, rescaledProgressX); + + List<DrawablePart> splitDrawableParts = new ArrayList<>(); + boolean styleRemainingParts = false; + for (int iPart = 0; iPart < nParts; iPart++) { + final DrawablePart drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof DrawablePoint drawablePoint) { + final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts); + splitDrawableParts.add( + new DrawablePoint(drawablePoint.getStart(), drawablePoint.getEnd(), color)); + } + if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true; + if (drawablePart instanceof DrawableSegment drawableSegment) { + if (iPart == iPartSegmentToSplit) { + if (rescaledProgressX <= drawableSegment.getStart()) { + styleRemainingParts = true; + final int color = maybeGetFadedColor(drawableSegment.getColor(), true); + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), color, true)); + } else if (drawableSegment.getStart() < rescaledProgressX + && rescaledProgressX < drawableSegment.getEnd()) { + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + rescaledProgressX - progressGap, drawableSegment.getColor())); + final int color = maybeGetFadedColor(drawableSegment.getColor(), true); + splitDrawableParts.add( + new DrawableSegment(rescaledProgressX, drawableSegment.getEnd(), + color, true)); + styleRemainingParts = true; + } else { + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), drawableSegment.getColor())); + styleRemainingParts = true; + } + } else { + final int color = maybeGetFadedColor(drawableSegment.getColor(), + styleRemainingParts); + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), color, styleRemainingParts)); + } + } + } + + return new Pair<>(splitDrawableParts, rescaledProgressX); + } + + /** + * A part of the progress bar, which is either a {@link Segment} with non-zero length, or a + * {@link Point} with zero length. + */ + // TODO: b/372908709 - maybe this should be made private? Only test the final + // NotificationDrawable.Parts. + public interface Part { + } + + /** + * A segment is a part of the progress bar with non-zero length. For example, it can + * represent a portion in a navigation journey with certain traffic condition. + */ + public static final class Segment implements Part { + private final float mFraction; + @ColorInt + private final int mColor; + /** + * Whether the segment is faded or not. + * <p> + * <pre> + * When mFaded is set to true, a combination of the following is done to the segment: + * 1. The drawing color is mColor with opacity updated to 40%. + * 2. The gap between faded and non-faded segments is: + * - the segment-segment gap, when there is no tracker icon + * - 0, when there is tracker icon + * </pre> + * </p> + */ + private final boolean mFaded; + + /** Start position (in pixels) */ + private float mStart; + /** End position (in pixels */ + private float mEnd; + + public Segment(float fraction, @ColorInt int color) { + this(fraction, color, false); + } + + public Segment(float fraction, @ColorInt int color, boolean faded) { + mFraction = fraction; + mColor = color; + mFaded = faded; + } + + /** Returns the calculated drawing width of the part */ + public float getWidth() { + return mEnd - mStart; + } + + @Override + public String toString() { + return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded=" + + this.mFaded + "), mStart = " + this.mStart + ", mEnd = " + this.mEnd; + } + + // Needed for unit tests + @Override + public boolean equals(@androidx.annotation.Nullable Object other) { + if (this == other) return true; + + if (other == null || getClass() != other.getClass()) return false; + + Segment that = (Segment) other; + if (Float.compare(this.mFraction, that.mFraction) != 0) return false; + if (this.mColor != that.mColor) return false; + return this.mFaded == that.mFaded; + } + + @Override + public int hashCode() { + return Objects.hash(mFraction, mColor, mFaded); + } + } + + /** + * A point is a part of the progress bar with zero length. Points are designated points within a + * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop + * ride-share journey. + */ + public static final class Point implements Part { + @ColorInt + private final int mColor; + + public Point(@ColorInt int color) { + mColor = color; + } + + @Override + public String toString() { + return "Point(color=" + this.mColor + ")"; + } + + // Needed for unit tests. + @Override + public boolean equals(@androidx.annotation.Nullable Object other) { + if (this == other) return true; + + if (other == null || getClass() != other.getClass()) return false; + + Point that = (Point) other; + + return this.mColor == that.mColor; + } + + @Override + public int hashCode() { + return Objects.hash(mColor); + } + } } diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java index 8629a1c95202..4ece81c24edc 100644 --- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java +++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java @@ -21,7 +21,6 @@ import android.content.res.Resources; import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; @@ -49,22 +48,24 @@ import java.util.Objects; /** * This is used by NotificationProgressBar for displaying a custom background. It composes of - * segments, which have non-zero length, and points, which have zero length. + * segments, which have non-zero length varying drawing width, and points, which have zero length + * and fixed size for drawing. * - * @see Segment - * @see Point + * @see DrawableSegment + * @see DrawablePoint */ public final class NotificationProgressDrawable extends Drawable { private static final String TAG = "NotifProgressDrawable"; + @Nullable + private BoundsChangeListener mBoundsChangeListener = null; + private State mState; private boolean mMutated; - private final ArrayList<Part> mParts = new ArrayList<>(); - private boolean mHasTrackerIcon; + private final ArrayList<DrawablePart> mParts = new ArrayList<>(); private final RectF mSegRectF = new RectF(); - private final Rect mPointRect = new Rect(); private final RectF mPointRectF = new RectF(); private final Paint mFillPaint = new Paint(); @@ -80,33 +81,37 @@ public final class NotificationProgressDrawable extends Drawable { } /** - * <p>Set the segment default color for the drawable.</p> - * <p>Note: changing this property will affect all instances of a drawable loaded from a - * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p> - * - * @param color The color of the stroke - * @see #mutate() + * Returns the gap between two segments. */ - public void setSegmentDefaultColor(@ColorInt int color) { - mState.setSegmentColor(color); + public float getSegSegGap() { + return mState.mSegSegGap; } /** - * <p>Set the point rect default color for the drawable.</p> - * <p>Note: changing this property will affect all instances of a drawable loaded from a - * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p> - * - * @param color The color of the point rect - * @see #mutate() + * Returns the gap between a segment and a point. + */ + public float getSegPointGap() { + return mState.mSegPointGap; + } + + /** + * Returns the gap between a segment and a point. */ - public void setPointRectDefaultColor(@ColorInt int color) { - mState.setPointRectColor(color); + public float getSegmentMinWidth() { + return mState.mSegmentMinWidth; + } + + /** + * Returns the radius for the points. + */ + public float getPointRadius() { + return mState.mPointRadius; } /** * Set the segments and points that constitute the drawable. */ - public void setParts(List<Part> parts) { + public void setParts(List<DrawablePart> parts) { mParts.clear(); mParts.addAll(parts); @@ -116,51 +121,22 @@ public final class NotificationProgressDrawable extends Drawable { /** * Set the segments and points that constitute the drawable. */ - public void setParts(@NonNull Part... parts) { + public void setParts(@NonNull DrawablePart... parts) { setParts(Arrays.asList(parts)); } - /** - * Set whether a tracker is drawn on top of this NotificationProgressDrawable. - */ - public void setHasTrackerIcon(boolean hasTrackerIcon) { - if (mHasTrackerIcon != hasTrackerIcon) { - mHasTrackerIcon = hasTrackerIcon; - invalidateSelf(); - } - } - @Override public void draw(@NonNull Canvas canvas) { - final float pointRadius = - mState.mPointRadius; // how big the point icon will be, halved - - // generally, we will start drawing at (x, y) and end at (x+w, y) - float x = (float) getBounds().left; + final float pointRadius = mState.mPointRadius; + final float left = (float) getBounds().left; final float centerY = (float) getBounds().centerY(); - final float totalWidth = (float) getBounds().width(); - float segPointGap = mState.mSegPointGap; final int numParts = mParts.size(); for (int iPart = 0; iPart < numParts; iPart++) { - final Part part = mParts.get(iPart); - final Part prevPart = iPart == 0 ? null : mParts.get(iPart - 1); - final Part nextPart = iPart + 1 == numParts ? null : mParts.get(iPart + 1); - if (part instanceof Segment segment) { - final float segWidth = segment.mFraction * totalWidth; - // Advance the start position to account for a point immediately prior. - final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x); - final float start = x + startOffset; - // Retract the end position to account for the padding and a point immediately - // after. - final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, - mState.mSegSegGap, x + segWidth, totalWidth, mHasTrackerIcon); - final float end = x + segWidth - endOffset; - - // Advance the current position to account for the segment's fraction of the total - // width (ignoring offset and padding) - x += segWidth; - + final DrawablePart part = mParts.get(iPart); + final float start = left + part.mStart; + final float end = left + part.mEnd; + if (part instanceof DrawableSegment segment) { // No space left to draw the segment if (start > end) continue; @@ -168,67 +144,23 @@ public final class NotificationProgressDrawable extends Drawable { : mState.mSegmentHeight / 2F; final float cornerRadius = mState.mSegmentCornerRadius; - mFillPaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor - : (segment.mFaded ? mState.mFadedSegmentColor : mState.mSegmentColor)); + mFillPaint.setColor(segment.mColor); mSegRectF.set(start, centerY - radiusY, end, centerY + radiusY); canvas.drawRoundRect(mSegRectF, cornerRadius, cornerRadius, mFillPaint); - } else if (part instanceof Point point) { - final float pointWidth = 2 * pointRadius; - float start = x - pointRadius; - if (start < 0) start = 0; - float end = start + pointWidth; - if (end > totalWidth) { - end = totalWidth; - if (totalWidth > pointWidth) start = totalWidth - pointWidth; - } - mPointRect.set((int) start, (int) (centerY - pointRadius), (int) end, - (int) (centerY + pointRadius)); - - if (point.mIcon != null) { - point.mIcon.setBounds(mPointRect); - point.mIcon.draw(canvas); - } else { - // TODO: b/367804171 - actually use a vector asset for the default point - // rather than drawing it as a box? - mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); - final float inset = mState.mPointRectInset; - final float cornerRadius = mState.mPointRectCornerRadius; - mPointRectF.inset(inset, inset); - - mFillPaint.setColor(point.mColor != Color.TRANSPARENT ? point.mColor - : (point.mFaded ? mState.mFadedPointRectColor - : mState.mPointRectColor)); - - canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint); - } - } - } - } + } else if (part instanceof DrawablePoint point) { + // TODO: b/367804171 - actually use a vector asset for the default point + // rather than drawing it as a box? + mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); + final float inset = mState.mPointRectInset; + final float cornerRadius = mState.mPointRectCornerRadius; + mPointRectF.inset(inset, inset); - private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, - float startX) { - if (!(prevPart instanceof Point)) return 0F; - final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0; - return pointOffset + pointRadius + segPointGap; - } + mFillPaint.setColor(point.mColor); - private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, - float segPointGap, - float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) { - if (nextPart == null) return 0F; - if (nextPart instanceof Segment nextSeg) { - if (!seg.mFaded && nextSeg.mFaded) { - // @see Segment#mFaded - return hasTrackerIcon ? 0F : segSegGap; + canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint); } - return segSegGap; } - - final float pointWidth = 2 * pointRadius; - final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth) - ? (endX + pointRadius - totalWidth) : 0; - return segPointGap + pointRadius + pointOffset; } @Override @@ -260,6 +192,19 @@ public final class NotificationProgressDrawable extends Drawable { return PixelFormat.UNKNOWN; } + public void setBoundsChangeListener(BoundsChangeListener listener) { + mBoundsChangeListener = listener; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + if (mBoundsChangeListener != null) { + mBoundsChangeListener.onDrawableBoundsChanged(); + } + } + @Override public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) @@ -384,6 +329,8 @@ public final class NotificationProgressDrawable extends Drawable { // Extract the theme attributes, if any. state.mThemeAttrsSegments = a.extractThemeAttrs(); + state.mSegmentMinWidth = a.getDimension( + R.styleable.NotificationProgressDrawableSegments_minWidth, state.mSegmentMinWidth); state.mSegmentHeight = a.getDimension( R.styleable.NotificationProgressDrawableSegments_height, state.mSegmentHeight); state.mFadedSegmentHeight = a.getDimension( @@ -392,9 +339,6 @@ public final class NotificationProgressDrawable extends Drawable { state.mSegmentCornerRadius = a.getDimension( R.styleable.NotificationProgressDrawableSegments_cornerRadius, state.mSegmentCornerRadius); - final int color = a.getColor(R.styleable.NotificationProgressDrawableSegments_color, - state.mSegmentColor); - setSegmentDefaultColor(color); } private void updatePointsFromTypedArray(TypedArray a) { @@ -413,9 +357,6 @@ public final class NotificationProgressDrawable extends Drawable { state.mPointRectCornerRadius = a.getDimension( R.styleable.NotificationProgressDrawablePoints_cornerRadius, state.mPointRectCornerRadius); - final int color = a.getColor(R.styleable.NotificationProgressDrawablePoints_color, - state.mPointRectColor); - setPointRectDefaultColor(color); } static int resolveDensity(@Nullable Resources r, int parentDensity) { @@ -464,63 +405,57 @@ public final class NotificationProgressDrawable extends Drawable { } /** - * A part of the progress bar, which is either a S{@link Segment} with non-zero length, or a - * {@link Point} with zero length. + * Listener to receive updates about drawable bounds changing */ - public interface Part { + public interface BoundsChangeListener { + /** Called when bounds have changed */ + void onDrawableBoundsChanged(); } /** - * A segment is a part of the progress bar with non-zero length. For example, it can - * represent a portion in a navigation journey with certain traffic condition. - * + * A part of the progress drawable, which is either a {@link DrawableSegment} with non-zero + * length and varying drawing width, or a {@link DrawablePoint} with zero length and fixed size + * for drawing. */ - public static final class Segment implements Part { - private final float mFraction; - @ColorInt private final int mColor; - /** Whether the segment is faded or not. - * <p> - * <pre> - * When mFaded is set to true, a combination of the following is done to the segment: - * 1. The drawing color is mColor with opacity updated to 40%. - * 2. The gap between faded and non-faded segments is: - * - the segment-segment gap, when there is no tracker icon - * - 0, when there is tracker icon - * </pre> - * </p> - */ - private final boolean mFaded; - - public Segment(float fraction) { - this(fraction, Color.TRANSPARENT); + public abstract static class DrawablePart { + // TODO: b/372908709 - maybe rename start/end to left/right, to be consistent with the + // bounds rect. + /** Start position for drawing (in pixels) */ + protected float mStart; + /** End position for drawing (in pixels) */ + protected float mEnd; + /** Drawing color. */ + @ColorInt protected final int mColor; + + protected DrawablePart(float start, float end, @ColorInt int color) { + mStart = start; + mEnd = end; + mColor = color; } - public Segment(float fraction, @ColorInt int color) { - this(fraction, color, false); + public float getStart() { + return this.mStart; } - public Segment(float fraction, @ColorInt int color, boolean faded) { - mFraction = fraction; - mColor = color; - mFaded = faded; + public void setStart(float start) { + mStart = start; } - public float getFraction() { - return this.mFraction; + public float getEnd() { + return this.mEnd; } - public int getColor() { - return this.mColor; + public void setEnd(float end) { + mEnd = end; } - public boolean getFaded() { - return this.mFaded; + /** Returns the calculated drawing width of the part */ + public float getWidth() { + return mEnd - mStart; } - @Override - public String toString() { - return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded=" - + this.mFaded + ')'; + public int getColor() { + return this.mColor; } // Needed for unit tests @@ -530,80 +465,79 @@ public final class NotificationProgressDrawable extends Drawable { if (other == null || getClass() != other.getClass()) return false; - Segment that = (Segment) other; - if (Float.compare(this.mFraction, that.mFraction) != 0) return false; - if (this.mColor != that.mColor) return false; - return this.mFaded == that.mFaded; + DrawablePart that = (DrawablePart) other; + if (Float.compare(this.mStart, that.mStart) != 0) return false; + if (Float.compare(this.mEnd, that.mEnd) != 0) return false; + return this.mColor == that.mColor; } @Override public int hashCode() { - return Objects.hash(mFraction, mColor, mFaded); + return Objects.hash(mStart, mEnd, mColor); } } /** - * A point is a part of the progress bar with zero length. Points are designated points within a - * progressbar to visualize distinct stages or milestones. For example, a stop in a multi-stop - * ride-share journey. + * A segment is a part of the progress bar with non-zero length. For example, it can + * represent a portion in a navigation journey with certain traffic condition. + * <p> + * The start and end positions for drawing a segment are assumed to have been adjusted for + * the Points and gaps neighboring the segment. + * </p> */ - public static final class Point implements Part { - @Nullable - private final Drawable mIcon; - @ColorInt private final int mColor; + public static final class DrawableSegment extends DrawablePart { + /** + * Whether the segment is faded or not. + * <p> + * Faded segments and non-faded segments are drawn with different heights. + * </p> + */ private final boolean mFaded; - public Point(@Nullable Drawable icon) { - this(icon, Color.TRANSPARENT, false); - } - - public Point(@Nullable Drawable icon, @ColorInt int color) { - this(icon, color, false); - + public DrawableSegment(float start, float end, int color) { + this(start, end, color, false); } - public Point(@Nullable Drawable icon, @ColorInt int color, boolean faded) { - mIcon = icon; - mColor = color; + public DrawableSegment(float start, float end, int color, boolean faded) { + super(start, end, color); mFaded = faded; } - @Nullable - public Drawable getIcon() { - return this.mIcon; - } - - public int getColor() { - return this.mColor; - } - - public boolean getFaded() { - return this.mFaded; - } - @Override public String toString() { - return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ", faded=" + this.mFaded - + ")"; + return "Segment(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor + + ", faded=" + this.mFaded + ')'; } // Needed for unit tests. @Override public boolean equals(@Nullable Object other) { - if (this == other) return true; - - if (other == null || getClass() != other.getClass()) return false; + if (!super.equals(other)) return false; - Point that = (Point) other; - - if (!Objects.equals(this.mIcon, that.mIcon)) return false; - if (this.mColor != that.mColor) return false; + DrawableSegment that = (DrawableSegment) other; return this.mFaded == that.mFaded; } @Override public int hashCode() { - return Objects.hash(mIcon, mColor, mFaded); + return Objects.hash(super.hashCode(), mFaded); + } + } + + /** + * A point is a part of the progress bar with zero length. Points are designated points within a + * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop + * ride-share journey. + */ + public static final class DrawablePoint extends DrawablePart { + public DrawablePoint(float start, float end, int color) { + super(start, end, color); + } + + @Override + public String toString() { + return "Point(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor + + ")"; } } @@ -628,16 +562,14 @@ public final class NotificationProgressDrawable extends Drawable { int mChangingConfigurations; float mSegSegGap = 0.0f; float mSegPointGap = 0.0f; + float mSegmentMinWidth = 0.0f; float mSegmentHeight; float mFadedSegmentHeight; float mSegmentCornerRadius; - int mSegmentColor; - int mFadedSegmentColor; + // how big the point icon will be, halved float mPointRadius; float mPointRectInset; float mPointRectCornerRadius; - int mPointRectColor; - int mFadedPointRectColor; int[] mThemeAttrs; int[] mThemeAttrsSegments; @@ -652,16 +584,13 @@ public final class NotificationProgressDrawable extends Drawable { mChangingConfigurations = orig.mChangingConfigurations; mSegSegGap = orig.mSegSegGap; mSegPointGap = orig.mSegPointGap; + mSegmentMinWidth = orig.mSegmentMinWidth; mSegmentHeight = orig.mSegmentHeight; mFadedSegmentHeight = orig.mFadedSegmentHeight; mSegmentCornerRadius = orig.mSegmentCornerRadius; - mSegmentColor = orig.mSegmentColor; - mFadedSegmentColor = orig.mFadedSegmentColor; mPointRadius = orig.mPointRadius; mPointRectInset = orig.mPointRectInset; mPointRectCornerRadius = orig.mPointRectCornerRadius; - mPointRectColor = orig.mPointRectColor; - mFadedPointRectColor = orig.mFadedPointRectColor; mThemeAttrs = orig.mThemeAttrs; mThemeAttrsSegments = orig.mThemeAttrsSegments; @@ -674,6 +603,18 @@ public final class NotificationProgressDrawable extends Drawable { } private void applyDensityScaling(int sourceDensity, int targetDensity) { + if (mSegSegGap > 0) { + mSegSegGap = scaleFromDensity( + mSegSegGap, sourceDensity, targetDensity); + } + if (mSegPointGap > 0) { + mSegPointGap = scaleFromDensity( + mSegPointGap, sourceDensity, targetDensity); + } + if (mSegmentMinWidth > 0) { + mSegmentMinWidth = scaleFromDensity( + mSegmentMinWidth, sourceDensity, targetDensity); + } if (mSegmentHeight > 0) { mSegmentHeight = scaleFromDensity( mSegmentHeight, sourceDensity, targetDensity); @@ -740,28 +681,6 @@ public final class NotificationProgressDrawable extends Drawable { applyDensityScaling(sourceDensity, targetDensity); } } - - public void setSegmentColor(int color) { - mSegmentColor = color; - mFadedSegmentColor = getFadedColor(color); - } - - public void setPointRectColor(int color) { - mPointRectColor = color; - mFadedPointRectColor = getFadedColor(color); - } - } - - /** - * Get a color with an opacity that's 25% of the input color. - */ - @ColorInt - static int getFadedColor(@ColorInt int color) { - return Color.argb( - (int) (Color.alpha(color) * 0.4f + 0.5f), - Color.red(color), - Color.green(color), - Color.blue(color)); } @Override diff --git a/core/jni/android_hardware_UsbDeviceConnection.cpp b/core/jni/android_hardware_UsbDeviceConnection.cpp index b1221ee38db3..68ef3d424d12 100644 --- a/core/jni/android_hardware_UsbDeviceConnection.cpp +++ b/core/jni/android_hardware_UsbDeviceConnection.cpp @@ -165,19 +165,25 @@ android_hardware_UsbDeviceConnection_control_request(JNIEnv *env, jobject thiz, return -1; } - jbyte* bufferBytes = NULL; - if (buffer) { - bufferBytes = (jbyte*)env->GetPrimitiveArrayCritical(buffer, NULL); + bool is_dir_in = (requestType & USB_ENDPOINT_DIR_MASK) == USB_DIR_IN; + std::unique_ptr<jbyte[]> bufferBytes(new (std::nothrow) jbyte[length]); + if (!bufferBytes) { + jniThrowException(env, "java/lang/OutOfMemoryError", NULL); + return -1; + } + + if (!is_dir_in && buffer) { + env->GetByteArrayRegion(buffer, start, length, bufferBytes.get()); } - jint result = usb_device_control_transfer(device, requestType, request, - value, index, bufferBytes + start, length, timeout); + jint bytes_transferred = usb_device_control_transfer(device, requestType, request, + value, index, bufferBytes.get(), length, timeout); - if (bufferBytes) { - env->ReleasePrimitiveArrayCritical(buffer, bufferBytes, 0); + if (bytes_transferred > 0 && is_dir_in) { + env->SetByteArrayRegion(buffer, start, bytes_transferred, bufferBytes.get()); } - return result; + return bytes_transferred; } static jint diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp index 3c2dccd451d4..9ef17e82c38e 100644 --- a/core/jni/android_os_Debug.cpp +++ b/core/jni/android_os_Debug.cpp @@ -729,6 +729,17 @@ static jlong android_os_Debug_getGpuPrivateMemoryKb(JNIEnv* env, jobject clazz) return gpuPrivateMem / 1024; } +static jlong android_os_Debug_getKernelCmaUsageKb(JNIEnv* env, jobject clazz) { + jlong totalKernelCmaUsageKb = -1; + uint64_t size; + + if (meminfo::ReadKernelCmaUsageKb(&size)) { + totalKernelCmaUsageKb = size; + } + + return totalKernelCmaUsageKb; +} + static jlong android_os_Debug_getDmabufMappedSizeKb(JNIEnv* env, jobject clazz) { jlong dmabufPss = 0; std::vector<dmabufinfo::DmaBuffer> dmabufs; @@ -836,6 +847,7 @@ static const JNINativeMethod gMethods[] = { {"getGpuTotalUsageKb", "()J", (void*)android_os_Debug_getGpuTotalUsageKb}, {"isVmapStack", "()Z", (void*)android_os_Debug_isVmapStack}, {"logAllocatorStats", "()Z", (void*)android_os_Debug_logAllocatorStats}, + {"getKernelCmaUsageKb", "()J", (void*)android_os_Debug_getKernelCmaUsageKb}, }; int register_android_os_Debug(JNIEnv *env) diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index 0c243d1dc185..6f69e4005b80 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -1113,6 +1113,22 @@ static void nativeSetCornerRadius(JNIEnv* env, jclass clazz, jlong transactionOb transaction->setCornerRadius(ctrl, cornerRadius); } +static void nativeSetClientDrawnCornerRadius(JNIEnv* env, jclass clazz, jlong transactionObj, + jlong nativeObject, jfloat clientDrawnCornerRadius) { + auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); + + SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject); + transaction->setClientDrawnCornerRadius(ctrl, clientDrawnCornerRadius); +} + +static void nativeSetClientDrawnShadows(JNIEnv* env, jclass clazz, jlong transactionObj, + jlong nativeObject, jfloat clientDrawnShadowRadius) { + auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); + + SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject); + transaction->setClientDrawnShadowRadius(ctrl, clientDrawnShadowRadius); +} + static void nativeSetBackgroundBlurRadius(JNIEnv* env, jclass clazz, jlong transactionObj, jlong nativeObject, jint blurRadius) { auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); @@ -2547,6 +2563,10 @@ static const JNINativeMethod sSurfaceControlMethods[] = { (void*)nativeSetCrop }, {"nativeSetCornerRadius", "(JJF)V", (void*)nativeSetCornerRadius }, + {"nativeSetClientDrawnCornerRadius", "(JJF)V", + (void*) nativeSetClientDrawnCornerRadius }, + {"nativeSetClientDrawnShadows", "(JJF)V", + (void*) nativeSetClientDrawnShadows }, {"nativeSetBackgroundBlurRadius", "(JJI)V", (void*)nativeSetBackgroundBlurRadius }, {"nativeSetLayerStack", "(JJI)V", diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 96d34a0230b3..5d0b340ac839 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -107,6 +107,8 @@ message SecureSettingsProto { optional SettingProto accessibility_key_gesture_targets = 59 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto hct_rect_prompt_status = 60 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto em_value = 61 [ (android.privacy).dest = DEST_AUTOMATIC ]; + // Settings for accessibility autoclick + optional SettingProto autoclick_cursor_area_size = 62 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Accessibility accessibility = 2; @@ -578,6 +580,7 @@ message SecureSettingsProto { optional SettingProto activate_on_dock = 3 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto activate_on_sleep = 4 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto default_component = 5 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto activate_on_postured = 6 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Screensaver screensaver = 47; diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto index 0d99200f4e6f..64c9f540a97b 100644 --- a/core/proto/android/providers/settings/system.proto +++ b/core/proto/android/providers/settings/system.proto @@ -229,6 +229,7 @@ message SystemSettingsProto { optional SettingProto swap_primary_button = 2 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto scrolling_acceleration = 3 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto pointer_acceleration_enabled = 4 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto scrolling_speed = 5 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Mouse mouse = 38; diff --git a/core/res/res/drawable/notification_progress.xml b/core/res/res/drawable/notification_progress.xml index 5d272fb00e34..ff5450ee106f 100644 --- a/core/res/res/drawable/notification_progress.xml +++ b/core/res/res/drawable/notification_progress.xml @@ -24,6 +24,7 @@ android:segPointGap="@dimen/notification_progress_segPoint_gap"> <segments android:color="?attr/colorProgressBackgroundNormal" + android:minWidth="@dimen/notification_progress_segments_min_width" android:height="@dimen/notification_progress_segments_height" android:fadedHeight="@dimen/notification_progress_segments_faded_height" android:cornerRadius="@dimen/notification_progress_segments_corner_radius"/> diff --git a/core/res/res/layout/notification_2025_conversation_header.xml b/core/res/res/layout/notification_2025_conversation_header.xml index db79e79c96df..1bde17358825 100644 --- a/core/res/res/layout/notification_2025_conversation_header.xml +++ b/core/res/res/layout/notification_2025_conversation_header.xml @@ -136,10 +136,10 @@ <ImageView android:id="@+id/phishing_alert" - android:layout_width="@dimen/notification_phishing_alert_size" - android:layout_height="@dimen/notification_phishing_alert_size" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:baseline="10dp" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" android:scaleType="fitCenter" android:src="@drawable/ic_dialog_alert_material" android:visibility="gone" @@ -148,10 +148,10 @@ <ImageView android:id="@+id/profile_badge" - android:layout_width="@dimen/notification_badge_size" - android:layout_height="@dimen/notification_badge_size" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:baseline="10dp" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" android:scaleType="fitCenter" android:visibility="gone" android:contentDescription="@string/notification_work_profile_content_description" @@ -159,10 +159,10 @@ <ImageView android:id="@+id/alerted_icon" - android:layout_width="@dimen/notification_alerted_size" - android:layout_height="@dimen/notification_alerted_size" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:baseline="10dp" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" android:contentDescription="@string/notification_alerted_content_description" android:scaleType="fitCenter" android:src="@drawable/ic_notifications_alerted" diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index f108ce5bd1b9..d29b7af9e24e 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -87,7 +87,7 @@ > <!-- - NOTE: The notification_top_line_views layout contains the app_name_text. + NOTE: The notification_2025_top_line_views layout contains the app_name_text. In order to include the title view at the beginning, the Notification.Builder has logic to hide that view whenever this title view is to be visible. --> @@ -104,7 +104,7 @@ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" /> - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml index bd17a3a0a74e..5beab508aecf 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -89,7 +89,7 @@ > <!-- - NOTE: The notification_top_line_views layout contains the app_name_text. + NOTE: The notification_2025_top_line_views layout contains the app_name_text. In order to include the title view at the beginning, the Notification.Builder has logic to hide that view whenever this title view is to be visible. --> @@ -106,7 +106,7 @@ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" /> - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml index edbebb17f825..d7c3263904d4 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml @@ -115,7 +115,7 @@ > <!-- - NOTE: The notification_top_line_views layout contains the app_name_text. + NOTE: The notification_2025_top_line_views layout contains the app_name_text. In order to include the title view at the beginning, the Notification.Builder has logic to hide that view whenever this title view is to be visible. --> @@ -132,7 +132,7 @@ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" /> - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml index 0c07053d428a..72b3798e0780 100644 --- a/core/res/res/layout/notification_2025_template_header.xml +++ b/core/res/res/layout/notification_2025_template_header.xml @@ -68,7 +68,7 @@ android:theme="@style/Theme.DeviceDefault.Notification" > - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_heads_up_base.xml b/core/res/res/layout/notification_2025_template_heads_up_base.xml index e4ff835a3524..084ec7daa683 100644 --- a/core/res/res/layout/notification_2025_template_heads_up_base.xml +++ b/core/res/res/layout/notification_2025_template_heads_up_base.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?><!-- - ~ Copyright (C) 2014 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/core/res/res/layout/notification_2025_top_line_views.xml b/core/res/res/layout/notification_2025_top_line_views.xml new file mode 100644 index 000000000000..74873463391e --- /dev/null +++ b/core/res/res/layout/notification_2025_top_line_views.xml @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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 + --> +<!-- + This layout file should be included inside a NotificationTopLineView, sometimes after a + <TextView android:id="@+id/title"/> +--> +<merge + xmlns:android="http://schemas.android.com/apk/res/android"> + + <TextView + android:id="@+id/app_name_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:singleLine="true" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:visibility="?attr/notificationHeaderAppNameVisibility" + /> + + <TextView + android:id="@+id/header_text_secondary_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:visibility="gone" + /> + + <TextView + android:id="@+id/header_text_secondary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:visibility="gone" + android:singleLine="true" + /> + + <TextView + android:id="@+id/header_text_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:visibility="gone" + /> + + <TextView + android:id="@+id/header_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:visibility="gone" + android:singleLine="true" + /> + + <TextView + android:id="@+id/time_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:singleLine="true" + android:visibility="gone" + /> + + <DateTimeView + android:id="@+id/time" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:showRelative="true" + android:singleLine="true" + android:visibility="gone" + /> + + <ViewStub + android:id="@+id/chronometer" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:layout="@layout/notification_template_part_chronometer" + android:visibility="gone" + /> + + <ImageButton + android:id="@+id/feedback" + android:layout_width="@dimen/notification_feedback_size" + android:layout_height="@dimen/notification_feedback_size" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:baseline="13dp" + android:scaleType="fitCenter" + android:src="@drawable/ic_feedback_indicator" + android:background="?android:selectableItemBackgroundBorderless" + android:visibility="gone" + android:contentDescription="@string/notification_feedback_indicator" + /> + + <ImageView + android:id="@+id/phishing_alert" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" + android:scaleType="fitCenter" + android:src="@drawable/ic_dialog_alert_material" + android:visibility="gone" + android:contentDescription="@string/notification_phishing_alert_content_description" + /> + + <ImageView + android:id="@+id/profile_badge" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" + android:scaleType="fitCenter" + android:visibility="gone" + android:contentDescription="@string/notification_work_profile_content_description" + /> + + <ImageView + android:id="@+id/alerted_icon" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" + android:contentDescription="@string/notification_alerted_content_description" + android:scaleType="fitCenter" + android:src="@drawable/ic_notifications_alerted" + android:visibility="gone" + /> +</merge> + diff --git a/core/res/res/values-round-watch/dimens.xml b/core/res/res/values-round-watch/dimens.xml index f288b41fb556..59ee554798bc 100644 --- a/core/res/res/values-round-watch/dimens.xml +++ b/core/res/res/values-round-watch/dimens.xml @@ -26,6 +26,6 @@ <item name="input_extract_action_button_height" type="dimen">32dp</item> <item name="input_extract_action_icon_padding" type="dimen">5dp</item> - <item name="global_actions_vertical_padding_percentage" type="fraction">20.8%</item> + <item name="global_actions_vertical_padding_percentage" type="fraction">21.8%</item> <item name="global_actions_horizontal_padding_percentage" type="fraction">5.2%</item> </resources> diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml index e6295ea06177..4ff3f8825cc4 100644 --- a/core/res/res/values-watch/config.xml +++ b/core/res/res/values-watch/config.xml @@ -101,6 +101,13 @@ P.S this is a change only intended for wear devices. --> <bool name="config_enableViewGroupScalingFading">true</bool> - <!-- Allow the gesture to double tap the power button to trigger a target action. --> - <bool name="config_doubleTapPowerGestureEnabled">false</bool> + <!-- Controls the double tap power button gesture to trigger a target action. + 0: Gesture is disabled + 1: Launch camera mode, allowing the user to disable/enable the double tap power gesture + from launching the camera application. + 2: Multi target mode, allowing the user to select one of the targets defined in + config_doubleTapPowerGestureMultiTargetDefaultAction and to disable/enable the double + tap power gesture from triggering the selected target action. + --> + <integer name="config_doubleTapPowerGestureMode">0</integer> </resources> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 728c856f5855..8372aecf0d27 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -7572,25 +7572,31 @@ <!-- NotificationProgressDrawable class --> <!-- ================================== --> - <!-- Drawable used to render a segmented bar, with segments and points. --> + <!-- Drawable used to render a notification progress bar, with segments and points. --> <!-- @hide internal use only --> <declare-styleable name="NotificationProgressDrawable"> - <!-- Default color for the parts. --> + <!-- The gap between two segments. --> <attr name="segSegGap" format="dimension" /> + <!-- The gap between a segment and a point. --> <attr name="segPointGap" format="dimension" /> </declare-styleable> <!-- Used to config the segments of a NotificationProgressDrawable. --> <!-- @hide internal use only --> <declare-styleable name="NotificationProgressDrawableSegments"> - <!-- Height of the solid segments --> + <!-- TODO: b/372908709 - maybe move this to NotificationProgressBar, because that's the only + place this is used actually. Same for NotificationProgressDrawable.segSegGap/segPointGap + above. --> + <!-- Minimum required drawing width. The drawing width refers to the width after + the original segments have been adjusted for the neighboring Points and gaps. This is + enforced by stretching the segments that are too short. --> + <attr name="minWidth" format="dimension" /> + <!-- Height of the solid segments. --> <attr name="height" /> - <!-- Height of the faded segments --> - <attr name="fadedHeight" format="dimension"/> + <!-- Height of the faded segments. --> + <attr name="fadedHeight" format="dimension" /> <!-- Corner radius of the segment rect. --> <attr name="cornerRadius" format="dimension" /> - <!-- Default color of the segment. --> - <attr name="color" /> </declare-styleable> <!-- Used to config the points of a NotificationProgressDrawable. --> @@ -7602,8 +7608,6 @@ <attr name="inset" /> <!-- Corner radius of the point rect. --> <attr name="cornerRadius"/> - <!-- Default color of the point rect. --> - <attr name="color" /> </declare-styleable> <!-- ========================== --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 45a5d85a097d..e14cffd72b0c 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2083,6 +2083,18 @@ See com.android.server.timezonedetector.TimeZoneDetectorStrategy for more information. --> <bool name="config_supportTelephonyTimeZoneFallback" translatable="false">true</bool> + <!-- Whether the time notifications feature is enabled. Settings this to false means the feature + cannot be used. Setting this to true means the feature can be enabled on the device. --> + <bool name="config_enableTimeZoneNotificationsSupported" translatable="false">true</bool> + + <!-- Whether the time zone notifications tracking feature is enabled. Settings this to false + means the feature cannot be used. --> + <bool name="config_enableTimeZoneNotificationsTrackingSupported" translatable="false">true</bool> + + <!-- Whether the time zone manual change tracking feature is enabled. Settings this to false + means the feature cannot be used. --> + <bool name="config_enableTimeZoneManualChangeTrackingSupported" translatable="false">true</bool> + <!-- Whether to enable network location overlay which allows network location provider to be replaced by an app at run-time. When disabled, only the config_networkLocationProviderPackageName package will be searched for network location @@ -2754,6 +2766,9 @@ <bool name="config_dreamsActivatedOnDockByDefault">true</bool> <!-- If supported and enabled, are dreams activated when asleep and charging? (by default) --> <bool name="config_dreamsActivatedOnSleepByDefault">false</bool> + <!-- If supported and enabled, are dreams enabled while device is stationary and upright? + (by default) --> + <bool name="config_dreamsActivatedOnPosturedByDefault">false</bool> <!-- ComponentName of the default dream (Settings.Secure.DEFAULT_SCREENSAVER_COMPONENT) --> <string name="config_dreamsDefaultComponent" translatable="false">com.android.deskclock/com.android.deskclock.Screensaver</string> <!-- ComponentNames of the dreams that we should hide --> @@ -4244,12 +4259,19 @@ is non-interactive. --> <bool name="config_cameraDoubleTapPowerGestureEnabled">true</bool> - <!-- Allow the gesture to double tap the power button to trigger a target action. --> - <bool name="config_doubleTapPowerGestureEnabled">true</bool> - <!-- Default target action for double tap of the power button gesture. + <!-- Controls the double tap power button gesture to trigger a target action. + 0: Gesture is disabled + 1: Launch camera mode, allowing the user to disable/enable the double tap power gesture + from launching the camera application. + 2: Multi target mode, allowing the user to select one of the targets defined in + config_doubleTapPowerGestureMultiTargetDefaultAction and to disable/enable the double + tap power gesture from triggering the selected target action. + --> + <integer name="config_doubleTapPowerGestureMode">2</integer> + <!-- Default target action for double tap of the power button gesture in multi target mode. 0: Launch camera 1: Launch wallet --> - <integer name="config_defaultDoubleTapPowerGestureAction">0</integer> + <integer name="config_doubleTapPowerGestureMultiTargetDefaultAction">0</integer> <!-- Allow the gesture to quick tap the power button multiple times to start the emergency sos experience while the device is non-interactive. --> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 2adb79118ed9..d6b8704a978b 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -608,12 +608,23 @@ <!-- Size of the feedback indicator for notifications --> <dimen name="notification_feedback_size">20dp</dimen> + <!-- Size of the (work) profile badge for notifications --> + <dimen name="notification_badge_size">12dp</dimen> + + <!-- Size of the (work) profile badge for notifications (2025 redesign version). + Scales with font size. Chosen to look good alongside notification_subtext_size text. --> + <dimen name="notification_2025_badge_size">14sp</dimen> + + <!-- Baseline for aligning icons in the top line (like the work profile icon or alerting icon) + to the text properly. This is equal to notification_2025_badge_size - 2sp. --> + <dimen name="notification_2025_badge_baseline">12sp</dimen> + + <!-- Spacing for the top line icons (e.g. the work profile badge). --> + <dimen name="notification_2025_badge_margin">4dp</dimen> + <!-- Size of the phishing alert for notifications --> <dimen name="notification_phishing_alert_size">@dimen/notification_badge_size</dimen> - <!-- Size of the profile badge for notifications --> - <dimen name="notification_badge_size">12dp</dimen> - <!-- Size of the alerted icon for notifications --> <dimen name="notification_alerted_size">@dimen/notification_badge_size</dimen> @@ -888,6 +899,8 @@ <dimen name="notification_progress_segSeg_gap">4dp</dimen> <!-- The gap between a segment and a point in the notification progress bar --> <dimen name="notification_progress_segPoint_gap">4dp</dimen> + <!-- The minimum required drawing width of the notification progress bar segments --> + <dimen name="notification_progress_segments_min_width">16dp</dimen> <!-- The height of the notification progress bar segments --> <dimen name="notification_progress_segments_height">6dp</dimen> <!-- The height of the notification progress bar faded segments --> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 6313054e47f5..debc5e9a0dce 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -853,6 +853,9 @@ <!-- Text shown when viewing channel settings for notifications related to vpn status --> <string name="notification_channel_vpn">VPN status</string> + <!-- Text shown when viewing channel settings for notifications related to system time --> + <string name="notification_channel_system_time">Time and time zones</string> + <!-- Notification channel name. This channel sends high-priority alerts from the user's IT admin for key updates about the user's work device or work profile. --> <string name="notification_channel_device_admin">Alerts from your IT admin</string> @@ -881,6 +884,10 @@ <string name="notification_channel_accessibility_magnification">Magnification</string> <!-- Text shown when viewing channel settings for notifications related to accessibility + hearing device. [CHAR_LIMIT=NONE]--> + <string name="notification_channel_accessibility_hearing_device">Hearing device</string> + + <!-- Text shown when viewing channel settings for notifications related to accessibility security policy. [CHAR_LIMIT=NONE]--> <string name="notification_channel_accessibility_security_policy">Accessibility usage</string> @@ -3875,6 +3882,12 @@ <string name="carrier_app_notification_title">New SIM inserted</string> <string name="carrier_app_notification_text">Tap to set it up</string> + <!-- Time zone notification strings --> + <!-- Title for time zone change notifications --> + <string name="time_zone_change_notification_title">Your time zone changed</string> + <!-- Body for time zone change notifications --> + <string name="time_zone_change_notification_body">You\'re now in <xliff:g id="time_zone_display_name">%1$s</xliff:g> (<xliff:g id="time_zone_offset">%2$s</xliff:g>)</string> + <!-- Date/Time picker dialogs strings --> <!-- The title of the time picker dialog. [CHAR LIMIT=NONE] --> @@ -4985,6 +4998,19 @@ <!-- Text used to describe system navigation features, shown within a UI allowing a user to assign system magnification features to the Accessibility button in the navigation bar. --> <string name="accessibility_magnification_chooser_text">Magnification</string> + <!-- Notification title for switching input to the phone's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_switch_phone_mic_notification_title">Switch to phone mic?</string> + <!-- Notification title for switching input to the hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_switch_hearing_mic_notification_title">Switch to hearing aid mic?</string> + <!-- Notification content for switching input to the phone's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_switch_phone_mic_notification_text">For better sound or if your hearing aid battery is low. This only switches your mic during the call.</string> + <!-- Notification content for switching input to the hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_switch_hearing_mic_notification_text">You can use your hearing aid microphone for hands-free calling. This only switches your mic during the call.</string> + <!-- Notification action button. Click it will switch the input between phone's microphone and hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_notification_switch_button">Switch</string> + <!-- Notification action button. Click it will open the bluetooth device details page for this hearing device. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_notification_settings_button">Settings</string> + <!-- Text spoken when the current user is switched if accessibility is enabled. [CHAR LIMIT=none] --> <string name="user_switched">Current user <xliff:g id="name" example="Bob">%1$s</xliff:g>.</string> <!-- Message shown when switching to a user [CHAR LIMIT=none] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index a18f923d625b..5f6619d4e4cc 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -575,6 +575,7 @@ <java-symbol type="dimen" name="notification_top_pad_large_text" /> <java-symbol type="dimen" name="notification_top_pad_large_text_narrow" /> <java-symbol type="dimen" name="notification_badge_size" /> + <java-symbol type="dimen" name="notification_2025_badge_size" /> <java-symbol type="dimen" name="immersive_mode_cling_width" /> <java-symbol type="dimen" name="accessibility_magnification_indicator_width" /> <java-symbol type="dimen" name="circular_display_mask_thickness" /> @@ -2329,6 +2330,7 @@ <java-symbol type="bool" name="config_dreamsEnabledOnBattery" /> <java-symbol type="bool" name="config_dreamsActivatedOnDockByDefault" /> <java-symbol type="bool" name="config_dreamsActivatedOnSleepByDefault" /> + <java-symbol type="bool" name="config_dreamsActivatedOnPosturedByDefault" /> <java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenPowered" /> <java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenNotPowered" /> <java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" /> @@ -2377,6 +2379,9 @@ <java-symbol type="string" name="config_secondaryLocationTimeZoneProviderPackageName" /> <java-symbol type="bool" name="config_enableTelephonyTimeZoneDetection" /> <java-symbol type="bool" name="config_supportTelephonyTimeZoneFallback" /> + <java-symbol type="bool" name="config_enableTimeZoneNotificationsSupported" /> + <java-symbol type="bool" name="config_enableTimeZoneNotificationsTrackingSupported" /> + <java-symbol type="bool" name="config_enableTimeZoneManualChangeTrackingSupported" /> <java-symbol type="bool" name="config_autoResetAirplaneMode" /> <java-symbol type="string" name="config_notificationAccessConfirmationActivity" /> <java-symbol type="bool" name="config_preventImeStartupUnlessTextEditor" /> @@ -3167,8 +3172,8 @@ <java-symbol type="integer" name="config_cameraLiftTriggerSensorType" /> <java-symbol type="string" name="config_cameraLiftTriggerSensorStringType" /> <java-symbol type="bool" name="config_cameraDoubleTapPowerGestureEnabled" /> - <java-symbol type="bool" name="config_doubleTapPowerGestureEnabled" /> - <java-symbol type="integer" name="config_defaultDoubleTapPowerGestureAction" /> + <java-symbol type="integer" name="config_doubleTapPowerGestureMode" /> + <java-symbol type="integer" name="config_doubleTapPowerGestureMultiTargetDefaultAction" /> <java-symbol type="bool" name="config_emergencyGestureEnabled" /> <java-symbol type="bool" name="config_defaultEmergencyGestureEnabled" /> <java-symbol type="bool" name="config_defaultEmergencyGestureSoundEnabled" /> @@ -3839,6 +3844,13 @@ <java-symbol type="string" name="reduce_bright_colors_feature_name" /> <java-symbol type="string" name="one_handed_mode_feature_name" /> + <java-symbol type="string" name="hearing_device_switch_phone_mic_notification_title" /> + <java-symbol type="string" name="hearing_device_switch_hearing_mic_notification_title" /> + <java-symbol type="string" name="hearing_device_switch_phone_mic_notification_text" /> + <java-symbol type="string" name="hearing_device_switch_hearing_mic_notification_text" /> + <java-symbol type="string" name="hearing_device_notification_switch_button" /> + <java-symbol type="string" name="hearing_device_notification_settings_button" /> + <!-- com.android.internal.widget.RecyclerView --> <java-symbol type="id" name="item_touch_helper_previous_elevation"/> <java-symbol type="dimen" name="item_touch_helper_max_drag_scroll_per_frame"/> @@ -3939,6 +3951,7 @@ <java-symbol type="dimen" name="notification_progress_tracker_height" /> <java-symbol type="dimen" name="notification_progress_segSeg_gap" /> <java-symbol type="dimen" name="notification_progress_segPoint_gap" /> + <java-symbol type="dimen" name="notification_progress_segments_min_width" /> <java-symbol type="dimen" name="notification_progress_segments_height" /> <java-symbol type="dimen" name="notification_progress_segments_faded_height" /> <java-symbol type="dimen" name="notification_progress_segments_corner_radius" /> @@ -4018,6 +4031,7 @@ <java-symbol type="string" name="notification_channel_network_available" /> <java-symbol type="array" name="config_defaultCloudSearchServices" /> <java-symbol type="string" name="notification_channel_vpn" /> + <java-symbol type="string" name="notification_channel_system_time" /> <java-symbol type="string" name="notification_channel_device_admin" /> <java-symbol type="string" name="notification_channel_alerts" /> <java-symbol type="string" name="notification_channel_retail_mode" /> @@ -4025,8 +4039,11 @@ <java-symbol type="string" name="notification_channel_heavy_weight_app" /> <java-symbol type="string" name="notification_channel_system_changes" /> <java-symbol type="string" name="notification_channel_accessibility_magnification" /> + <java-symbol type="string" name="notification_channel_accessibility_hearing_device" /> <java-symbol type="string" name="notification_channel_accessibility_security_policy" /> <java-symbol type="string" name="notification_channel_display" /> + <java-symbol type="string" name="time_zone_change_notification_title" /> + <java-symbol type="string" name="time_zone_change_notification_body" /> <java-symbol type="string" name="config_defaultAutofillService" /> <java-symbol type="string" name="config_defaultFieldClassificationService" /> <java-symbol type="string" name="config_defaultOnDeviceSpeechRecognitionService" /> diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java index 6538ce85457c..3d6e1225bd92 100644 --- a/core/tests/coretests/src/android/app/NotificationManagerTest.java +++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java @@ -16,6 +16,8 @@ package android.app; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -25,8 +27,12 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.ParceledListSlice; +import android.os.UserHandle; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; @@ -35,6 +41,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -42,6 +49,7 @@ import org.junit.runner.RunWith; import java.time.Instant; import java.time.InstantSource; +import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest @@ -50,14 +58,24 @@ public class NotificationManagerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private Context mContext; private NotificationManagerWithMockService mNotificationManager; private final FakeClock mClock = new FakeClock(); + private PackageTestableContext mContext; + @Before public void setUp() { - mContext = ApplicationProvider.getApplicationContext(); + mContext = new PackageTestableContext(ApplicationProvider.getApplicationContext()); mNotificationManager = new NotificationManagerWithMockService(mContext, mClock); + + // Caches must be in test mode in order to be used in tests. + PropertyInvalidatedCache.setTestMode(true); + mNotificationManager.setChannelCacheToTestMode(); + } + + @After + public void tearDown() { + PropertyInvalidatedCache.setTestMode(false); } @Test @@ -243,12 +261,161 @@ public class NotificationManagerTest { anyInt(), any(), anyInt()); } + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_cachedUntilInvalidated() throws Exception { + // Invalidate the cache first because the cache won't do anything until then + NotificationManager.invalidateNotificationChannelCache(); + + // It doesn't matter what the returned contents are, as long as we return a channel. + // This setup must set up getNotificationChannels(), as that's the method called. + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel()))); + + // ask for the same channel 100 times without invalidating the cache + for (int i = 0; i < 100; i++) { + NotificationChannel unused = mNotificationManager.getNotificationChannel("id"); + } + + // invalidate the cache; then ask again + NotificationManager.invalidateNotificationChannelCache(); + NotificationChannel unused = mNotificationManager.getNotificationChannel("id"); + + verify(mNotificationManager.mBackendService, times(2)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_sameApp_oneCall() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + + NotificationChannel c1 = new NotificationChannel("id1", "name1", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel c2 = new NotificationChannel("id2", "name2", + NotificationManager.IMPORTANCE_NONE); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(c1, c2))); + + assertThat(mNotificationManager.getNotificationChannel("id1")).isEqualTo(c1); + assertThat(mNotificationManager.getNotificationChannel("id2")).isEqualTo(c2); + assertThat(mNotificationManager.getNotificationChannel("id3")).isNull(); + + verify(mNotificationManager.mBackendService, times(1)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannels_cachedUntilInvalidated() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel()))); + + // ask for channels 100 times without invalidating the cache + for (int i = 0; i < 100; i++) { + List<NotificationChannel> unused = mNotificationManager.getNotificationChannels(); + } + + // invalidate the cache; then ask again + NotificationManager.invalidateNotificationChannelCache(); + List<NotificationChannel> res = mNotificationManager.getNotificationChannels(); + + verify(mNotificationManager.mBackendService, times(2)) + .getNotificationChannels(any(), any(), anyInt()); + assertThat(res).containsExactlyElementsIn(List.of(exampleChannel())); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_channelAndConversationLookup() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + + // Full list of channels: c1; conv1 = child of c1; c2 is unrelated + NotificationChannel c1 = new NotificationChannel("id", "name", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel conv1 = new NotificationChannel("", "name_conversation", + NotificationManager.IMPORTANCE_DEFAULT); + conv1.setConversationId("id", "id_conversation"); + NotificationChannel c2 = new NotificationChannel("other", "name2", + NotificationManager.IMPORTANCE_DEFAULT); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), anyInt())) + .thenReturn(new ParceledListSlice<>(List.of(c1, conv1, c2))); + + // Lookup for channel c1 and c2: returned as expected + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(c1); + assertThat(mNotificationManager.getNotificationChannel("other")).isEqualTo(c2); + + // Lookup for conv1 should return conv1 + assertThat(mNotificationManager.getNotificationChannel("id", "id_conversation")).isEqualTo( + conv1); + + // Lookup for a different conversation channel that doesn't exist, whose parent channel id + // is "id", should return c1 + assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isEqualTo(c1); + + // Lookup of a nonexistent channel is null + assertThat(mNotificationManager.getNotificationChannel("id3")).isNull(); + + // All of that should have been one call to getNotificationChannels() + verify(mNotificationManager.mBackendService, times(1)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_differentPackages() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + final String pkg1 = "one"; + final String pkg2 = "two"; + final int userId = 0; + final int userId1 = 1; + + // multiple channels with the same ID, but belonging to different packages/users + NotificationChannel channel1 = new NotificationChannel("id", "name1", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel channel2 = channel1.copy(); + channel2.setName("name2"); + NotificationChannel channel3 = channel1.copy(); + channel3.setName("name3"); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1), + eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel1))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg2), + eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel2))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1), + eq(userId1))).thenReturn(new ParceledListSlice<>(List.of(channel3))); + + // set our context to pretend to be from package 1 and userId 0 + mContext.setParameters(pkg1, pkg1, userId); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel1); + + // now package 2 + mContext.setParameters(pkg2, pkg2, userId); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel2); + + // now pkg1 for a different user + mContext.setParameters(pkg1, pkg1, userId1); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel3); + + // Those should have been three different calls + verify(mNotificationManager.mBackendService, times(3)) + .getNotificationChannels(any(), any(), anyInt()); + } + private Notification exampleNotification() { return new Notification.Builder(mContext, "channel") .setSmallIcon(android.R.drawable.star_big_on) .build(); } + private NotificationChannel exampleChannel() { + return new NotificationChannel("id", "channel_name", + NotificationManager.IMPORTANCE_DEFAULT); + } + private static class NotificationManagerWithMockService extends NotificationManager { private final INotificationManager mBackendService; @@ -264,6 +431,48 @@ public class NotificationManagerTest { } } + // Helper context wrapper class where we can control just the return values of getPackageName, + // getOpPackageName, and getUserId (used in getNotificationChannels). + private static class PackageTestableContext extends ContextWrapper { + private String mPackage; + private String mOpPackage; + private Integer mUserId; + + PackageTestableContext(Context base) { + super(base); + } + + void setParameters(String packageName, String opPackageName, int userId) { + mPackage = packageName; + mOpPackage = opPackageName; + mUserId = userId; + } + + @Override + public String getPackageName() { + if (mPackage != null) return mPackage; + return super.getPackageName(); + } + + @Override + public String getOpPackageName() { + if (mOpPackage != null) return mOpPackage; + return super.getOpPackageName(); + } + + @Override + public int getUserId() { + if (mUserId != null) return mUserId; + return super.getUserId(); + } + + @Override + public UserHandle getUser() { + if (mUserId != null) return UserHandle.of(mUserId); + return super.getUser(); + } + } + private static class FakeClock implements InstantSource { private long mNowMillis = 441644400000L; diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index ca6ad6fae46e..7be6950fb613 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -2504,6 +2504,21 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressSegments() { + final List<Notification.ProgressStyle.Segment> segments = List.of( + new Notification.ProgressStyle.Segment(100).setColor(Color.WHITE), + new Notification.ProgressStyle.Segment(50).setColor(Color.RED), + new Notification.ProgressStyle.Segment(50).setColor(Color.BLUE) + ); + + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressSegments(segments); + + assertThat(progressStyle1.getProgressSegments()).isEqualTo(segments); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_addProgressPoint_dropsNegativePoints() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2532,6 +2547,21 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressPoints() { + final List<Notification.ProgressStyle.Point> points = List.of( + new Notification.ProgressStyle.Point(0).setColor(Color.WHITE), + new Notification.ProgressStyle.Point(50).setColor(Color.RED), + new Notification.ProgressStyle.Point(100).setColor(Color.BLUE) + ); + + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressPoints(points); + + assertThat(progressStyle1.getProgressPoints()).isEqualTo(points); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_createProgressModel_ignoresPointsExceedingMax() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2673,11 +2703,58 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressIndeterminate() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressIndeterminate(true); + assertThat(progressStyle1.isProgressIndeterminate()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_styledByProgress_defaultValueTrue() { final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); assertThat(progressStyle1.isStyledByProgress()).isTrue(); } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setStyledByProgress() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setStyledByProgress(false); + assertThat(progressStyle1.isStyledByProgress()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_point() { + final int id = 1; + final int position = 10; + final int color = Color.RED; + + final Notification.ProgressStyle.Point point = + new Notification.ProgressStyle.Point(position).setId(id).setColor(color); + + assertEquals(id, point.getId()); + assertEquals(position, point.getPosition()); + assertEquals(color, point.getColor()); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_segment() { + final int id = 1; + final int length = 100; + final int color = Color.RED; + + final Notification.ProgressStyle.Segment segment = + new Notification.ProgressStyle.Segment(length).setId(id).setColor(color); + + assertEquals(id, segment.getId()); + assertEquals(length, segment.getLength()); + assertEquals(color, segment.getColor()); + } + private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java index 37ef6cba8814..939bf2ec4b0a 100644 --- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java +++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java @@ -207,7 +207,8 @@ public class RegisteredServicesCacheTest extends AndroidTestCase { final ComponentInfo info = new ComponentInfo(); info.applicationInfo = new ApplicationInfo(); info.applicationInfo.uid = uid; - return new RegisteredServicesCache.ServiceInfo<>(type, info, null); + return new RegisteredServicesCache.ServiceInfo<>(type, info, null /* componentName */, + 0 /* lastUpdateTime */); } private void assertNotEmptyFileCreated(TestServicesCache cache, int userId) { @@ -301,7 +302,7 @@ public class RegisteredServicesCacheTest extends AndroidTestCase { @Override protected ServiceInfo<TestServiceType> parseServiceInfo( - ResolveInfo resolveInfo) throws XmlPullParserException, IOException { + ResolveInfo resolveInfo, int userId) throws XmlPullParserException, IOException { int size = mServices.size(); for (int i = 0; i < size; i++) { Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.valueAt(i); diff --git a/core/tests/coretests/src/android/content/pm/SystemFeaturesCacheTest.java b/core/tests/coretests/src/android/content/pm/SystemFeaturesCacheTest.java new file mode 100644 index 000000000000..ce4aa42f39b6 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/SystemFeaturesCacheTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2025 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.content.pm; + +import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; +import static android.content.pm.PackageManager.FEATURE_WATCH; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import android.util.ArrayMap; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SystemFeaturesCacheTest { + + private SystemFeaturesCache mCache; + + @Test + public void testNoFeatures() throws Exception { + SystemFeaturesCache cache = new SystemFeaturesCache(new ArrayMap<String, FeatureInfo>()); + assertThat(cache.maybeHasFeature("", 0)).isNull(); + assertThat(cache.maybeHasFeature(FEATURE_WATCH, 0)).isFalse(); + assertThat(cache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0)).isFalse(); + assertThat(cache.maybeHasFeature("com.missing.feature", 0)).isNull(); + } + + @Test + public void testNonSdkFeature() throws Exception { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(); + features.put("custom.feature", createFeature("custom.feature", 0)); + SystemFeaturesCache cache = new SystemFeaturesCache(features); + + assertThat(cache.maybeHasFeature("custom.feature", 0)).isNull(); + } + + @Test + public void testSdkFeature() throws Exception { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(); + features.put(FEATURE_WATCH, createFeature(FEATURE_WATCH, 0)); + SystemFeaturesCache cache = new SystemFeaturesCache(features); + + assertThat(cache.maybeHasFeature(FEATURE_WATCH, 0)).isTrue(); + assertThat(cache.maybeHasFeature(FEATURE_WATCH, -1)).isTrue(); + assertThat(cache.maybeHasFeature(FEATURE_WATCH, 1)).isFalse(); + assertThat(cache.maybeHasFeature(FEATURE_WATCH, Integer.MIN_VALUE)).isTrue(); + assertThat(cache.maybeHasFeature(FEATURE_WATCH, Integer.MAX_VALUE)).isFalse(); + + // Other SDK-declared features should be reported as unavailable. + assertThat(cache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0)).isFalse(); + } + + @Test + public void testSdkFeatureHasMinVersion() throws Exception { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(); + features.put(FEATURE_WATCH, createFeature(FEATURE_WATCH, Integer.MIN_VALUE)); + SystemFeaturesCache cache = new SystemFeaturesCache(features); + + assertThat(cache.maybeHasFeature(FEATURE_WATCH, 0)).isFalse(); + + // If both the query and the feature version itself happen to use MIN_VALUE, we can't + // reliably indicate availability, so it should report an indeterminate result. + assertThat(cache.maybeHasFeature(FEATURE_WATCH, Integer.MIN_VALUE)).isNull(); + } + + @Test + public void testParcel() throws Exception { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(); + features.put(FEATURE_WATCH, createFeature(FEATURE_WATCH, 0)); + SystemFeaturesCache cache = new SystemFeaturesCache(features); + + Parcel parcel = Parcel.obtain(); + SystemFeaturesCache parceledCache; + try { + parcel.writeParcelable(cache, 0); + parcel.setDataPosition(0); + parceledCache = parcel.readParcelable(getClass().getClassLoader()); + } finally { + parcel.recycle(); + } + + assertThat(parceledCache.maybeHasFeature(FEATURE_WATCH, 0)) + .isEqualTo(cache.maybeHasFeature(FEATURE_WATCH, 0)); + assertThat(parceledCache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0)) + .isEqualTo(cache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0)); + assertThat(parceledCache.maybeHasFeature("custom.feature", 0)) + .isEqualTo(cache.maybeHasFeature("custom.feature", 0)); + } + + private static FeatureInfo createFeature(String name, int version) { + FeatureInfo fi = new FeatureInfo(); + fi.name = name; + fi.version = version; + return fi; + } +} diff --git a/core/tests/coretests/src/android/os/OWNERS b/core/tests/coretests/src/android/os/OWNERS index c45080fb5e26..5fd4ffc7329a 100644 --- a/core/tests/coretests/src/android/os/OWNERS +++ b/core/tests/coretests/src/android/os/OWNERS @@ -10,6 +10,9 @@ per-file PowerManager*.java = file:/services/core/java/com/android/server/power/ # PerformanceHintManager per-file PerformanceHintManagerTest.java = file:/ADPF_OWNERS +# SystemHealthManager +per-file SystemHealthManagerUnitTest.java = file:/ADPF_OWNERS + # Caching per-file IpcDataCache* = file:/PERFORMANCE_OWNERS diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java index da9d687ee2b0..3e6520106ab0 100644 --- a/core/tests/coretests/src/android/os/ParcelTest.java +++ b/core/tests/coretests/src/android/os/ParcelTest.java @@ -361,7 +361,11 @@ public class ParcelTest { p.setClassCookie(ParcelTest.class, "to_be_discarded_cookie"); p.recycle(); - assertThat(p.getClassCookie(ParcelTest.class)).isNull(); + + // cannot access Parcel after it's recycled! + // this test is equivalent to checking hasClassCookie false + // after obtaining above + // assertThat(p.getClassCookie(ParcelTest.class)).isNull(); } @Test diff --git a/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java index 0bf406c970f2..2bd3f4df9435 100644 --- a/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java +++ b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java @@ -17,6 +17,7 @@ package com.android.internal.notification; import static com.android.internal.notification.SystemNotificationChannels.ABUSIVE_BACKGROUND_APPS; +import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_HEARING_DEVICE; import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_MAGNIFICATION; import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_SECURITY_POLICY; import static com.android.internal.notification.SystemNotificationChannels.ACCOUNT; @@ -90,8 +91,8 @@ public class SystemNotificationChannelsTest { DEVELOPER_IMPORTANT, UPDATES, NETWORK_STATUS, NETWORK_ALERTS, NETWORK_AVAILABLE, VPN, DEVICE_ADMIN, ALERTS, RETAIL_MODE, USB, FOREGROUND_SERVICE, HEAVY_WEIGHT_APP, SYSTEM_CHANGES, - ACCESSIBILITY_MAGNIFICATION, ACCESSIBILITY_SECURITY_POLICY, - ABUSIVE_BACKGROUND_APPS); + ACCESSIBILITY_MAGNIFICATION, ACCESSIBILITY_HEARING_DEVICE, + ACCESSIBILITY_SECURITY_POLICY, ABUSIVE_BACKGROUND_APPS); } @Test diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java index d26bb35e5481..5df2c1279eb8 100644 --- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java @@ -20,12 +20,16 @@ import static com.google.common.truth.Truth.assertThat; import android.app.Notification.ProgressStyle; import android.graphics.Color; +import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.android.internal.widget.NotificationProgressDrawable.Part; -import com.android.internal.widget.NotificationProgressDrawable.Point; -import com.android.internal.widget.NotificationProgressDrawable.Segment; +import com.android.internal.widget.NotificationProgressBar.Part; +import com.android.internal.widget.NotificationProgressBar.Point; +import com.android.internal.widget.NotificationProgressBar.Segment; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePart; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint; +import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,183 +41,287 @@ import java.util.List; public class NotificationProgressBarTest { @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentsIsEmpty() { + public void processAndConvertToParts_segmentsIsEmpty() { List<ProgressStyle.Segment> segments = new ArrayList<>(); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentsLengthNotMatchingProgressMax() { + public void processAndConvertToParts_segmentsLengthNotMatchingProgressMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50)); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentLengthIsNegative() { + public void processAndConvertToParts_segmentLengthIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(-50)); segments.add(new ProgressStyle.Segment(150)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentLengthIsZero() { + public void processAndConvertToParts_segmentLengthIsZero() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(0)); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_progressIsNegative() { + public void processAndConvertToParts_progressIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = -50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test - public void processAndConvertToDrawableParts_progressIsZero() { + public void processAndConvertToParts_progressIsZero() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 0; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 300, Color.RED))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedRed = 0x66FF0000; + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 300, fadedRed, true))); - List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true))); - - assertThat(parts).isEqualTo(expected); + assertThat(p.second).isEqualTo(0); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_progressAtMax() { + public void processAndConvertToParts_progressAtMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 100; int progressMax = 100; - boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 300, Color.RED))); - List<Part> expected = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + assertThat(drawableParts).isEqualTo(expectedDrawableParts); - assertThat(parts).isEqualTo(expected); + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + assertThat(p.second).isEqualTo(300); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_progressAboveMax() { + public void processAndConvertToParts_progressAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 150; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_pointPositionIsNegative() { + public void processAndConvertToParts_pointPositionIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); points.add(new ProgressStyle.Point(-50).setColor(Color.RED)); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_pointPositionAboveMax() { + public void processAndConvertToParts_pointPositionAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); points.add(new ProgressStyle.Point(150).setColor(Color.RED)); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithoutPoints() { + public void processAndConvertToParts_multipleSegmentsWithoutPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 60; int progressMax = 100; - boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedGreen = 0x6600FF00; + expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 180, Color.GREEN), + new DrawableSegment(180, 300, fadedGreen, true))); + + assertThat(p.second).isEqualTo(180); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + @Test + public void processAndConvertToParts_multipleSegmentsWithoutPoints_noTracker() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); + segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); + List<ProgressStyle.Point> points = new ArrayList<>(); + int progress = 60; + int progressMax = 100; + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN))); - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.50f, Color.RED), - new Segment(0.10f, Color.GREEN), - new Segment(0.40f, fadedGreen, true))); + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = false; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(parts).isEqualTo(expected); + // Colors with 40% opacity + int fadedGreen = 0x6600FF00; + expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 176, Color.GREEN), + new DrawableSegment(180, 300, fadedGreen, true))); + + assertThat(p.second).isEqualTo(180); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_singleSegmentWithPoints() { + public void processAndConvertToParts_singleSegmentWithPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); List<ProgressStyle.Point> points = new ArrayList<>(); @@ -223,31 +331,68 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.BLUE), new Point(Color.RED), + new Segment(0.10f, Color.BLUE), new Point(Color.BLUE), + new Segment(0.35f, Color.BLUE), new Point(Color.BLUE), + new Segment(0.15f, Color.BLUE), new Point(Color.YELLOW), + new Segment(0.25f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.BLUE), + new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.BLUE), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 170, Color.BLUE), + new DrawablePoint(174, 186, Color.BLUE), + new DrawableSegment(190, 215, Color.BLUE), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = true; + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + // Colors with 40% opacity int fadedBlue = 0x660000FF; int fadedYellow = 0x66FFFF00; - - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.15f, Color.BLUE), - new Point(null, Color.RED), - new Segment(0.10f, Color.BLUE), - new Point(null, Color.BLUE), - new Segment(0.35f, Color.BLUE), - new Point(null, Color.BLUE), - new Segment(0.15f, fadedBlue, true), - new Point(null, fadedYellow, true), - new Segment(0.25f, fadedBlue, true))); - - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); - - assertThat(parts).isEqualTo(expected); + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 34.219177F, Color.BLUE), + new DrawablePoint(38.219177F, 50.219177F, Color.RED), + new DrawableSegment(54.219177F, 70.21918F, Color.BLUE), + new DrawablePoint(74.21918F, 86.21918F, Color.BLUE), + new DrawableSegment(90.21918F, 172.38356F, Color.BLUE), + new DrawablePoint(176.38356F, 188.38356F, Color.BLUE), + new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true), + new DrawablePoint(221.0137F, 233.0137F, fadedYellow), + new DrawableSegment(237.0137F, 300F, fadedBlue, true))); + + assertThat(p.second).isEqualTo(182.38356F); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithPoints() { + public void processAndConvertToParts_multipleSegmentsWithPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); @@ -258,32 +403,68 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.RED), new Point(Color.RED), + new Segment(0.10f, Color.RED), new Point(Color.BLUE), + new Segment(0.25f, Color.RED), new Segment(0.10f, Color.GREEN), + new Point(Color.BLUE), new Segment(0.15f, Color.GREEN), + new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.RED), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 146, Color.RED), + new DrawableSegment(150, 170, Color.GREEN), + new DrawablePoint(174, 186, Color.BLUE), + new DrawableSegment(190, 215, Color.GREEN), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedGreen = 0x6600FF00; int fadedYellow = 0x66FFFF00; - - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.15f, Color.RED), - new Point(null, Color.RED), - new Segment(0.10f, Color.RED), - new Point(null, Color.BLUE), - new Segment(0.25f, Color.RED), - new Segment(0.10f, Color.GREEN), - new Point(null, Color.BLUE), - new Segment(0.15f, fadedGreen, true), - new Point(null, fadedYellow, true), - new Segment(0.25f, fadedGreen, true))); - - assertThat(parts).isEqualTo(expected); + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 34.095238F, Color.RED), + new DrawablePoint(38.095238F, 50.095238F, Color.RED), + new DrawableSegment(54.095238F, 70.09524F, Color.RED), + new DrawablePoint(74.09524F, 86.09524F, Color.BLUE), + new DrawableSegment(90.09524F, 148.9524F, Color.RED), + new DrawableSegment(152.95238F, 172.7619F, Color.GREEN), + new DrawablePoint(176.7619F, 188.7619F, Color.BLUE), + new DrawableSegment(192.7619F, 217.33333F, fadedGreen, true), + new DrawablePoint(221.33333F, 233.33333F, fadedYellow), + new DrawableSegment(237.33333F, 299.99997F, fadedGreen, true))); + + assertThat(p.second).isEqualTo(182.7619F); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithPoints_notStyledByProgress() { + public void processAndConvertToParts_multipleSegmentsWithPoints_notStyledByProgress() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); @@ -293,21 +474,223 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.RED), new Point(Color.RED), + new Segment(0.10f, Color.RED), new Point(Color.BLUE), + new Segment(0.25f, Color.RED), new Segment(0.25f, Color.GREEN), + new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.RED), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 146, Color.RED), + new DrawableSegment(150, 215, Color.GREEN), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = false; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 34.296295F, Color.RED), + new DrawablePoint(38.296295F, 50.296295F, Color.RED), + new DrawableSegment(54.296295F, 70.296295F, Color.RED), + new DrawablePoint(74.296295F, 86.296295F, Color.BLUE), + new DrawableSegment(90.296295F, 149.62962F, Color.RED), + new DrawableSegment(153.62962F, 216.8148F, Color.GREEN), + new DrawablePoint(220.81482F, 232.81482F, Color.YELLOW), + new DrawableSegment(236.81482F, 300, Color.GREEN))); + + assertThat(p.second).isEqualTo(182.9037F); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + // The only difference from the `zeroWidthDrawableSegment` test below is the longer + // segmentMinWidth (= 16dp). + @Test + public void maybeStretchAndRescaleSegments_negativeWidthDrawableSegment() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 16, Color.BLUE), + new DrawableSegment(20, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 32, Color.BLUE), + new DrawableSegment(36, 69.41936F, Color.BLUE), + new DrawableSegment(73.41936F, 124.25807F, Color.BLUE), + new DrawableSegment(128.25807F, 200, Color.BLUE))); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + // The only difference from the `negativeWidthDrawableSegment` test above is the shorter + // segmentMinWidth (= 10dp). + @Test + public void maybeStretchAndRescaleSegments_zeroWidthDrawableSegment() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 16, Color.BLUE), + new DrawableSegment(20, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 10; + boolean isStyledByProgress = true; + + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 26, Color.BLUE), + new DrawableSegment(30, 64.169014F, Color.BLUE), + new DrawableSegment(68.169014F, 120.92958F, Color.BLUE), + new DrawableSegment(124.92958F, 200, Color.BLUE))); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + @Test + public void maybeStretchAndRescaleSegments_noStretchingNecessary() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.2f, Color.BLUE), + new Segment(0.1f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 36, Color.BLUE), + new DrawableSegment(40, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 10; + boolean isStyledByProgress = true; - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.15f, Color.RED), - new Point(null, Color.RED), - new Segment(0.10f, Color.RED), - new Point(null, Color.BLUE), - new Segment(0.25f, Color.RED), - new Segment(0.25f, Color.GREEN), - new Point(null, Color.YELLOW), - new Segment(0.25f, Color.GREEN))); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(parts).isEqualTo(expected); + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); } } diff --git a/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java b/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java index e368d2815855..cb8b5ce245b6 100644 --- a/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java +++ b/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java @@ -48,12 +48,14 @@ public class TimeZoneCapabilitiesTest { .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) - .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED); + .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED); TimeZoneCapabilities.Builder builder2 = new TimeZoneCapabilities.Builder(TEST_USER_HANDLE) .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) - .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED); + .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED); { TimeZoneCapabilities one = builder1.build(); TimeZoneCapabilities two = builder2.build(); @@ -115,6 +117,13 @@ public class TimeZoneCapabilitiesTest { TimeZoneCapabilities two = builder2.build(); assertEquals(one, two); } + + builder1.setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED); + { + TimeZoneCapabilities one = builder1.build(); + TimeZoneCapabilities two = builder2.build(); + assertNotEquals(one, two); + } } @Test @@ -123,7 +132,8 @@ public class TimeZoneCapabilitiesTest { .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) - .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED); + .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED); assertRoundTripParcelable(builder.build()); builder.setConfigureAutoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED); @@ -137,6 +147,9 @@ public class TimeZoneCapabilitiesTest { builder.setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED); assertRoundTripParcelable(builder.build()); + + builder.setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED); + assertRoundTripParcelable(builder.build()); } @Test @@ -151,6 +164,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED) .build(); TimeZoneConfiguration configChange = new TimeZoneConfiguration.Builder() @@ -175,6 +189,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); TimeZoneConfiguration configChange = new TimeZoneConfiguration.Builder() @@ -191,6 +206,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); { @@ -204,6 +220,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); @@ -221,6 +238,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(false) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); @@ -238,6 +256,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); @@ -255,6 +274,25 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) + .build(); + + assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); + } + + { + TimeZoneCapabilities updatedCapabilities = + new TimeZoneCapabilities.Builder(capabilities) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED) + .build(); + + TimeZoneCapabilities expectedCapabilities = + new TimeZoneCapabilities.Builder(TEST_USER_HANDLE) + .setConfigureAutoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) + .setUseLocationEnabled(true) + .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) + .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); diff --git a/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java b/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java index 4ad3e41383aa..345e91268253 100644 --- a/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java +++ b/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java @@ -43,9 +43,11 @@ public class TimeZoneConfigurationTest { TimeZoneConfiguration completeConfig = new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(true) .setGeoDetectionEnabled(true) + .setNotificationsEnabled(true) .build(); assertTrue(completeConfig.isComplete()); assertTrue(completeConfig.hasIsGeoDetectionEnabled()); + assertTrue(completeConfig.hasIsNotificationsEnabled()); } @Test diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 50c95a9fa882..3378cc11d565 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -16,9 +16,9 @@ package android.graphics; +import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; -import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API; import static com.android.text.flags.Flags.FLAG_VERTICAL_TEXT_LAYOUT; import android.annotation.ColorInt; @@ -34,7 +34,6 @@ import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; -import android.graphics.fonts.FontStyle; import android.graphics.fonts.FontVariationAxis; import android.graphics.text.TextRunShaper; import android.os.Build; @@ -2100,14 +2099,6 @@ public class Paint { } /** - * A change ID for new font variation settings management. - * @hide - */ - @ChangeId - @EnabledSince(targetSdkVersion = 36) - public static final long NEW_FONT_VARIATION_MANAGEMENT = 361260253L; - - /** * Sets TrueType or OpenType font variation settings. The settings string is constructed from * multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters * and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that @@ -2136,16 +2127,12 @@ public class Paint { * </li> * </ul> * - * <p>Note: If the application that targets API 35 or before, this function mutates the - * underlying typeface instance. - * * @param fontVariationSettings font variation settings. You can pass null or empty string as * no variation settings. * - * @return If the application that targets API 36 or later and is running on devices API 36 or - * later, this function always returns true. Otherwise, this function returns true if - * the given settings is effective to at least one font file underlying this typeface. - * This function also returns true for empty settings string. Otherwise returns false. + * @return true if the given settings is effective to at least one font file underlying this + * typeface. This function also returns true for empty settings string. Otherwise + * returns false * * @throws IllegalArgumentException If given string is not a valid font variation settings * format @@ -2154,39 +2141,6 @@ public class Paint { * @see FontVariationAxis */ public boolean setFontVariationSettings(String fontVariationSettings) { - return setFontVariationSettings(fontVariationSettings, 0 /* wght adjust */); - } - - /** - * Set font variation settings with weight adjustment - * @hide - */ - public boolean setFontVariationSettings(String fontVariationSettings, int wghtAdjust) { - final boolean useFontVariationStore = Flags.typefaceRedesignReadonly() - && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT); - if (useFontVariationStore) { - FontVariationAxis[] axes = - FontVariationAxis.fromFontVariationSettings(fontVariationSettings); - if (axes == null) { - nSetFontVariationOverride(mNativePaint, 0); - mFontVariationSettings = null; - return true; - } - - long builderPtr = nCreateFontVariationBuilder(axes.length); - for (int i = 0; i < axes.length; ++i) { - int tag = axes[i].getOpenTypeTagValue(); - float value = axes[i].getStyleValue(); - if (tag == 0x77676874 /* wght */) { - value = Math.clamp(value + wghtAdjust, - FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX); - } - nAddFontVariationToBuilder(builderPtr, tag, value); - } - nSetFontVariationOverride(mNativePaint, builderPtr); - mFontVariationSettings = fontVariationSettings; - return true; - } final String settings = TextUtils.nullIfEmpty(fontVariationSettings); if (settings == mFontVariationSettings || (settings != null && settings.equals(mFontVariationSettings))) { diff --git a/graphics/java/android/graphics/fonts/FontVariationAxis.java b/graphics/java/android/graphics/fonts/FontVariationAxis.java index d1fe2cdbcd77..30a248bb3e0e 100644 --- a/graphics/java/android/graphics/fonts/FontVariationAxis.java +++ b/graphics/java/android/graphics/fonts/FontVariationAxis.java @@ -23,6 +23,7 @@ import android.os.Build; import android.text.TextUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.regex.Pattern; @@ -139,9 +140,19 @@ public final class FontVariationAxis { */ public static @Nullable FontVariationAxis[] fromFontVariationSettings( @Nullable String settings) { - if (settings == null || settings.isEmpty()) { + List<FontVariationAxis> result = fromFontVariationSettingsForList(settings); + if (result.isEmpty()) { return null; } + return result.toArray(new FontVariationAxis[0]); + } + + /** @hide */ + public static @NonNull List<FontVariationAxis> fromFontVariationSettingsForList( + @Nullable String settings) { + if (settings == null || settings.isEmpty()) { + return Collections.emptyList(); + } final ArrayList<FontVariationAxis> axisList = new ArrayList<>(); final int length = settings.length(); for (int i = 0; i < length; i++) { @@ -172,9 +183,9 @@ public final class FontVariationAxis { i = endOfValueString; } if (axisList.isEmpty()) { - return null; + return Collections.emptyList(); } - return axisList.toArray(new FontVariationAxis[0]); + return axisList; } /** diff --git a/keystore/java/android/security/keystore/KeyStoreManager.java b/keystore/java/android/security/keystore/KeyStoreManager.java index 740ccb53a691..13f1a72469c2 100644 --- a/keystore/java/android/security/keystore/KeyStoreManager.java +++ b/keystore/java/android/security/keystore/KeyStoreManager.java @@ -312,9 +312,11 @@ public final class KeyStoreManager { * When passed into getSupplementaryAttestationInfo, getSupplementaryAttestationInfo returns the * DER-encoded structure corresponding to the `Modules` schema described in the KeyMint HAL's * KeyCreationResult.aidl. The SHA-256 hash of this encoded structure is what's included with - * the tag in attestations. + * the tag in attestations. To ensure the returned encoded structure is the one attested to, + * clients should verify its SHA-256 hash matches the one in the attestation. Note that the + * returned structure can vary between boots. */ - // TODO(b/369375199): Replace with Tag.MODULE_HASH when flagging is removed. + // TODO(b/380020528): Replace with Tag.MODULE_HASH when KeyMint V4 is frozen. public static final int MODULE_HASH = TagType.BYTES | 724; /** diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 4c75ea4777da..957d1b835ec2 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -26,8 +26,8 @@ package { java_library { name: "wm_shell_protolog-groups", srcs: [ - "src/com/android/wm/shell/protolog/ShellProtoLogGroup.java", ":protolog-common-src", + "src/com/android/wm/shell/protolog/ShellProtoLogGroup.java", ], } @@ -61,8 +61,8 @@ java_genrule { name: "wm_shell_protolog_src", srcs: [ ":protolog-impl", - ":wm_shell_protolog-groups", ":wm_shell-sources", + ":wm_shell_protolog-groups", ], tools: ["protologtool"], cmd: "$(location protologtool) transform-protolog-calls " + @@ -80,8 +80,8 @@ java_genrule { java_genrule { name: "generate-wm_shell_protolog.json", srcs: [ - ":wm_shell_protolog-groups", ":wm_shell-sources", + ":wm_shell_protolog-groups", ], tools: ["protologtool"], cmd: "$(location protologtool) generate-viewer-config " + @@ -97,8 +97,8 @@ java_genrule { java_genrule { name: "gen-wmshell.protolog.pb", srcs: [ - ":wm_shell_protolog-groups", ":wm_shell-sources", + ":wm_shell_protolog-groups", ], tools: ["protologtool"], cmd: "$(location protologtool) generate-viewer-config " + @@ -159,38 +159,39 @@ java_library { android_library { name: "WindowManager-Shell", srcs: [ - "src/com/android/wm/shell/EventLogTags.logtags", ":wm_shell_protolog_src", // TODO(b/168581922) protologtool do not support kotlin(*.kt) - ":wm_shell-sources-kt", + "src/com/android/wm/shell/EventLogTags.logtags", ":wm_shell-aidls", ":wm_shell-shared-aidls", + ":wm_shell-sources-kt", ], resource_dirs: [ "res", ], static_libs: [ + "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", + "//frameworks/libs/systemui:iconloader_base", + "//packages/apps/Car/SystemUI/aconfig:com_android_systemui_car_flags_lib", + "PlatformAnimationLib", + "WindowManager-Shell-lite-proto", + "WindowManager-Shell-proto", + "WindowManager-Shell-shared", + "androidx-constraintlayout_constraintlayout", "androidx.appcompat_appcompat", - "androidx.core_core-ktx", "androidx.arch.core_core-runtime", - "androidx.datastore_datastore", "androidx.compose.material3_material3", - "androidx-constraintlayout_constraintlayout", + "androidx.core_core-ktx", + "androidx.datastore_datastore", "androidx.dynamicanimation_dynamicanimation", "androidx.recyclerview_recyclerview", - "kotlinx-coroutines-android", - "kotlinx-coroutines-core", - "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", - "//frameworks/libs/systemui:iconloader_base", "com_android_launcher3_flags_lib", "com_android_wm_shell_flags_lib", - "PlatformAnimationLib", - "WindowManager-Shell-proto", - "WindowManager-Shell-lite-proto", - "WindowManager-Shell-shared", - "perfetto_trace_java_protos", "dagger2", "jsr330", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", + "perfetto_trace_java_protos", ], libs: [ // Soong fails to automatically add this dependency because all the diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 755f472ee22e..2fed1380b635 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -233,6 +233,16 @@ public class DesktopModeStatus { } /** + * Returns whether the multiple desktops feature is enabled for this device (both backend and + * frontend implementations). + */ + public static boolean enableMultipleDesktops(@NonNull Context context) { + return Flags.enableMultipleDesktopsBackend() + && Flags.enableMultipleDesktopsFrontend() + && canEnterDesktopMode(context); + } + + /** * @return {@code true} if this device is requesting to show the app handle despite non * necessarily enabling desktop mode */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java index 9f01316d5b5c..b098620fde2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -230,6 +230,11 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { return mDisplayAreasInfo.get(displayId); } + @Nullable + public SurfaceControl getDisplayAreaLeash(int displayId) { + return mLeashes.get(displayId); + } + /** * Applies the {@link DisplayAreaInfo} to the {@link DisplayAreaContext} specified by * {@link DisplayAreaInfo#displayId}. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java new file mode 100644 index 000000000000..5018fdb615da --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java @@ -0,0 +1,288 @@ +/* + * 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.wm.shell.animation; + +import static com.android.wm.shell.transition.DefaultSurfaceAnimator.setupValueAnimator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.view.Choreographer; +import android.view.SurfaceControl; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.ClipRectAnimation; +import android.view.animation.ScaleAnimation; +import android.view.animation.Transformation; +import android.view.animation.TranslateAnimation; + +import java.util.function.Consumer; + +/** + * Animation implementation for size-changing window container animations. Ported from + * {@link com.android.server.wm.WindowChangeAnimationSpec}. + * <p> + * This animation behaves slightly differently depending on whether the window is growing + * or shrinking: + * <ul> + * <li>If growing, it will do a clip-reveal after quicker fade-out/scale of the smaller (old) + * snapshot. + * <li>If shrinking, it will do an opposite clip-reveal on the old snapshot followed by a quicker + * fade-out of the bigger (old) snapshot while simultaneously shrinking the new window into + * place. + * </ul> + */ +public class SizeChangeAnimation { + private final Rect mTmpRect = new Rect(); + final Transformation mTmpTransform = new Transformation(); + final Matrix mTmpMatrix = new Matrix(); + final float[] mTmpFloats = new float[9]; + final float[] mTmpVecs = new float[4]; + + private final Animation mAnimation; + private final Animation mSnapshotAnim; + + private final ValueAnimator mAnimator = ValueAnimator.ofFloat(0f, 1f); + + /** + * The maximum of stretching applied to any surface during interpolation (since the animation + * is a combination of stretching/cropping/fading). + */ + private static final float SCALE_FACTOR = 0.7f; + + /** + * Since this animation is made of several sub-animations, we want to pre-arrange the + * sub-animations on a "virtual timeline" and then drive the overall progress in lock-step. + * + * To do this, we have a single value-animator which animates progress from 0-1 with an + * arbitrary duration and interpolator. Then we convert the progress to a frame in our virtual + * timeline to get the interpolated transforms. + * + * The APIs for arranging the sub-animations use integral frame numbers, so we need to pick + * an integral "duration" for our virtual timeline. That's what this constant specifies. It + * is effectively an animation "resolution" since it divides-up the 0-1 interpolation-space. + */ + private static final int ANIMATION_RESOLUTION = 1000; + + public SizeChangeAnimation(Rect startBounds, Rect endBounds) { + mAnimation = buildContainerAnimation(startBounds, endBounds); + mSnapshotAnim = buildSnapshotAnimation(startBounds, endBounds); + } + + /** + * Initialize a size-change animation for a container leash. + */ + public void initialize(SurfaceControl leash, SurfaceControl snapshot, + SurfaceControl.Transaction startT) { + startT.reparent(snapshot, leash); + startT.setPosition(snapshot, 0, 0); + startT.show(snapshot); + startT.show(leash); + apply(startT, leash, snapshot, 0.f); + } + + /** + * Initialize a size-change animation for a view containing the leash surface(s). + * + * Note that this **will** apply {@param startToApply}! + */ + public void initialize(View view, SurfaceControl leash, SurfaceControl snapshot, + SurfaceControl.Transaction startToApply) { + startToApply.reparent(snapshot, leash); + startToApply.setPosition(snapshot, 0, 0); + startToApply.show(snapshot); + startToApply.show(leash); + apply(view, startToApply, leash, snapshot, 0.f); + } + + private ValueAnimator buildAnimatorInner(ValueAnimator.AnimatorUpdateListener updater, + SurfaceControl leash, SurfaceControl snapshot, Consumer<Animator> onFinish, + SurfaceControl.Transaction transaction, @Nullable View view) { + return setupValueAnimator(mAnimator, updater, (anim) -> { + transaction.reparent(snapshot, null); + if (view != null) { + view.setClipBounds(null); + view.setAnimationMatrix(null); + transaction.setCrop(leash, null); + } + transaction.apply(); + transaction.close(); + onFinish.accept(anim); + }); + } + + /** + * Build an animator which works on a pair of surface controls (where the snapshot is assumed + * to be a child of the main leash). + * + * @param onFinish Called when animation finishes. This is called on the anim thread! + */ + public ValueAnimator buildAnimator(SurfaceControl leash, SurfaceControl snapshot, + Consumer<Animator> onFinish) { + final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + Choreographer choreographer = Choreographer.getInstance(); + return buildAnimatorInner(animator -> { + // The finish callback in buildSurfaceAnimation will ensure that the animation ends + // with fraction 1. + final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f); + apply(transaction, leash, snapshot, progress); + transaction.setFrameTimelineVsync(choreographer.getVsyncId()); + transaction.apply(); + }, leash, snapshot, onFinish, transaction, null /* view */); + } + + /** + * Build an animator which works on a view that contains a pair of surface controls (where + * the snapshot is assumed to be a child of the main leash). + * + * @param onFinish Called when animation finishes. This is called on the anim thread! + */ + public ValueAnimator buildViewAnimator(View view, SurfaceControl leash, + SurfaceControl snapshot, Consumer<Animator> onFinish) { + final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + return buildAnimatorInner(animator -> { + // The finish callback in buildSurfaceAnimation will ensure that the animation ends + // with fraction 1. + final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f); + apply(view, transaction, leash, snapshot, progress); + }, leash, snapshot, onFinish, transaction, view); + } + + /** Animation for the whole container (snapshot is inside this container). */ + private static AnimationSet buildContainerAnimation(Rect startBounds, Rect endBounds) { + final long duration = ANIMATION_RESOLUTION; + boolean growing = endBounds.width() - startBounds.width() + + endBounds.height() - startBounds.height() >= 0; + long scalePeriod = (long) (duration * SCALE_FACTOR); + float startScaleX = SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width() + + (1.f - SCALE_FACTOR); + float startScaleY = SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height() + + (1.f - SCALE_FACTOR); + final AnimationSet animSet = new AnimationSet(true); + + final Animation scaleAnim = new ScaleAnimation(startScaleX, 1, startScaleY, 1); + scaleAnim.setDuration(scalePeriod); + if (!growing) { + scaleAnim.setStartOffset(duration - scalePeriod); + } + animSet.addAnimation(scaleAnim); + final Animation translateAnim = new TranslateAnimation(startBounds.left, + endBounds.left, startBounds.top, endBounds.top); + translateAnim.setDuration(duration); + animSet.addAnimation(translateAnim); + Rect startClip = new Rect(startBounds); + Rect endClip = new Rect(endBounds); + startClip.offsetTo(0, 0); + endClip.offsetTo(0, 0); + final Animation clipAnim = new ClipRectAnimation(startClip, endClip); + clipAnim.setDuration(duration); + animSet.addAnimation(clipAnim); + animSet.initialize(startBounds.width(), startBounds.height(), + endBounds.width(), endBounds.height()); + return animSet; + } + + /** The snapshot surface is assumed to be a child of the container surface. */ + private static AnimationSet buildSnapshotAnimation(Rect startBounds, Rect endBounds) { + final long duration = ANIMATION_RESOLUTION; + boolean growing = endBounds.width() - startBounds.width() + + endBounds.height() - startBounds.height() >= 0; + long scalePeriod = (long) (duration * SCALE_FACTOR); + float endScaleX = 1.f / (SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width() + + (1.f - SCALE_FACTOR)); + float endScaleY = 1.f / (SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height() + + (1.f - SCALE_FACTOR)); + + AnimationSet snapAnimSet = new AnimationSet(true); + // Animation for the "old-state" snapshot that is atop the task. + final Animation snapAlphaAnim = new AlphaAnimation(1.f, 0.f); + snapAlphaAnim.setDuration(scalePeriod); + if (!growing) { + snapAlphaAnim.setStartOffset(duration - scalePeriod); + } + snapAnimSet.addAnimation(snapAlphaAnim); + final Animation snapScaleAnim = + new ScaleAnimation(endScaleX, endScaleX, endScaleY, endScaleY); + snapScaleAnim.setDuration(duration); + snapAnimSet.addAnimation(snapScaleAnim); + snapAnimSet.initialize(startBounds.width(), startBounds.height(), + endBounds.width(), endBounds.height()); + return snapAnimSet; + } + + private void calcCurrentClipBounds(Rect outClip, Transformation fromTransform) { + // The following applies an inverse scale to the clip-rect so that it crops "after" the + // scale instead of before. + mTmpVecs[1] = mTmpVecs[2] = 0; + mTmpVecs[0] = mTmpVecs[3] = 1; + fromTransform.getMatrix().mapVectors(mTmpVecs); + + mTmpVecs[0] = 1.f / mTmpVecs[0]; + mTmpVecs[3] = 1.f / mTmpVecs[3]; + final Rect clipRect = fromTransform.getClipRect(); + outClip.left = (int) (clipRect.left * mTmpVecs[0] + 0.5f); + outClip.right = (int) (clipRect.right * mTmpVecs[0] + 0.5f); + outClip.top = (int) (clipRect.top * mTmpVecs[3] + 0.5f); + outClip.bottom = (int) (clipRect.bottom * mTmpVecs[3] + 0.5f); + } + + private void apply(SurfaceControl.Transaction t, SurfaceControl leash, SurfaceControl snapshot, + float progress) { + long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress); + // update thumbnail surface + mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform); + t.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats); + t.setAlpha(snapshot, mTmpTransform.getAlpha()); + + // update container surface + mAnimation.getTransformation(currentPlayTime, mTmpTransform); + final Matrix matrix = mTmpTransform.getMatrix(); + t.setMatrix(leash, matrix, mTmpFloats); + + calcCurrentClipBounds(mTmpRect, mTmpTransform); + t.setCrop(leash, mTmpRect); + } + + private void apply(View view, SurfaceControl.Transaction tmpT, SurfaceControl leash, + SurfaceControl snapshot, float progress) { + long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress); + // update thumbnail surface + mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform); + tmpT.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats); + tmpT.setAlpha(snapshot, mTmpTransform.getAlpha()); + + // update container surface + mAnimation.getTransformation(currentPlayTime, mTmpTransform); + final Matrix matrix = mTmpTransform.getMatrix(); + mTmpMatrix.set(matrix); + // animationMatrix is applied after getTranslation, so "move" the translate to the end. + mTmpMatrix.preTranslate(-view.getTranslationX(), -view.getTranslationY()); + mTmpMatrix.postTranslate(view.getTranslationX(), view.getTranslationY()); + view.setAnimationMatrix(mTmpMatrix); + + calcCurrentClipBounds(mTmpRect, mTmpTransform); + tmpT.setCrop(leash, mTmpRect); + view.setClipBounds(mTmpRect); + + // this takes stuff out of mTmpT so mTmpT can be re-used immediately + view.getViewRootImpl().applyTransactionOnDraw(tmpT); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt index 4cc81a9e6f8f..ec3637aacf91 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt @@ -18,9 +18,11 @@ package com.android.wm.shell.apptoweb import android.app.ActivityManager.RunningTaskInfo import android.content.Context +import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.verify.domain.DomainVerificationManager import android.graphics.Bitmap import android.graphics.PixelFormat +import android.util.Slog import android.view.LayoutInflater import android.view.SurfaceControl import android.view.SurfaceControlViewHost @@ -160,8 +162,15 @@ internal class OpenByDefaultDialog( } private fun setDefaultLinkHandlingSetting() { - domainVerificationManager.setDomainVerificationLinkHandlingAllowed( - packageName, openInAppButton.isChecked) + try { + domainVerificationManager.setDomainVerificationLinkHandlingAllowed( + packageName, openInAppButton.isChecked) + } catch (e: NameNotFoundException) { + Slog.e( + TAG, + "Failed to change link handling policy due to the package name is not found: " + e + ) + } } private fun closeMenu() { @@ -203,4 +212,8 @@ internal class OpenByDefaultDialog( /** Called when open by default dialog view has been released. */ fun onDialogDismissed() } + + companion object { + private const val TAG = "OpenByDefaultDialog" + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java new file mode 100644 index 000000000000..9451374befe0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import com.android.wm.shell.shared.annotations.ExternalThread; + +/** + * Interface to engage with the app zoom out feature. + */ +@ExternalThread +public interface AppZoomOut { + + /** + * Called when the zoom out progress is updated, which is used to scale down the current app + * surface from fullscreen to the max pushback level we want to apply. {@param progress} ranges + * between [0,1], 0 when fullscreen, 1 when it's at the max pushback level. + */ + void setProgress(float progress); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java new file mode 100644 index 000000000000..8cd7b0f48003 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.Configuration; +import android.util.Slog; +import android.window.DisplayAreaInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayChangeController; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.RemoteCallable; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.shared.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; + +/** Class that manages the app zoom out UI and states. */ +public class AppZoomOutController implements RemoteCallable<AppZoomOutController>, + ShellTaskOrganizer.FocusListener, DisplayChangeController.OnDisplayChangingListener { + + private static final String TAG = "AppZoomOutController"; + + private final Context mContext; + private final ShellTaskOrganizer mTaskOrganizer; + private final DisplayController mDisplayController; + private final AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer; + private final ShellExecutor mMainExecutor; + private final AppZoomOutImpl mImpl = new AppZoomOutImpl(); + + private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener = + new DisplayController.OnDisplaysChangedListener() { + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + updateDisplayLayout(displayId); + } + + @Override + public void onDisplayAdded(int displayId) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + updateDisplayLayout(displayId); + } + }; + + + public static AppZoomOutController create(Context context, ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController, + DisplayLayout displayLayout, @ShellMainThread ShellExecutor mainExecutor) { + AppZoomOutDisplayAreaOrganizer displayAreaOrganizer = new AppZoomOutDisplayAreaOrganizer( + context, displayLayout, mainExecutor); + return new AppZoomOutController(context, shellInit, shellTaskOrganizer, displayController, + displayAreaOrganizer, mainExecutor); + } + + @VisibleForTesting + AppZoomOutController(Context context, ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController, + AppZoomOutDisplayAreaOrganizer displayAreaOrganizer, + @ShellMainThread ShellExecutor mainExecutor) { + mContext = context; + mTaskOrganizer = shellTaskOrganizer; + mDisplayController = displayController; + mDisplayAreaOrganizer = displayAreaOrganizer; + mMainExecutor = mainExecutor; + + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mTaskOrganizer.addFocusListener(this); + + mDisplayController.addDisplayWindowListener(mDisplaysChangedListener); + mDisplayController.addDisplayChangingController(this); + + mDisplayAreaOrganizer.registerOrganizer(); + } + + public AppZoomOut asAppZoomOut() { + return mImpl; + } + + public void setProgress(float progress) { + mDisplayAreaOrganizer.setProgress(progress); + } + + void updateDisplayLayout(int displayId) { + final DisplayLayout newDisplayLayout = mDisplayController.getDisplayLayout(displayId); + if (newDisplayLayout == null) { + Slog.w(TAG, "Failed to get new DisplayLayout."); + return; + } + mDisplayAreaOrganizer.setDisplayLayout(newDisplayLayout); + } + + @Override + public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo == null) { + return; + } + if (taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_HOME) { + mDisplayAreaOrganizer.setIsHomeTaskFocused(taskInfo.isFocused); + } + } + + @Override + public void onDisplayChange(int displayId, int fromRotation, int toRotation, + @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) { + // TODO: verify if there is synchronization issues. + mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation); + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + @ExternalThread + private class AppZoomOutImpl implements AppZoomOut { + @Override + public void setProgress(float progress) { + mMainExecutor.execute(() -> AppZoomOutController.this.setProgress(progress)); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java new file mode 100644 index 000000000000..1c37461b2d2b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import android.annotation.Nullable; +import android.content.Context; +import android.util.ArrayMap; +import android.view.SurfaceControl; +import android.window.DisplayAreaAppearedInfo; +import android.window.DisplayAreaInfo; +import android.window.DisplayAreaOrganizer; +import android.window.WindowContainerToken; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.common.DisplayLayout; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +/** Display area organizer that manages the app zoom out UI and states. */ +public class AppZoomOutDisplayAreaOrganizer extends DisplayAreaOrganizer { + + private static final float PUSHBACK_SCALE_FOR_LAUNCHER = 0.05f; + private static final float PUSHBACK_SCALE_FOR_APP = 0.025f; + private static final float INVALID_PROGRESS = -1; + + private final DisplayLayout mDisplayLayout = new DisplayLayout(); + private final Context mContext; + private final float mCornerRadius; + private final Map<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap = + new ArrayMap<>(); + + private float mProgress = INVALID_PROGRESS; + // Denote whether the home task is focused, null when it's not yet initialized. + @Nullable private Boolean mIsHomeTaskFocused; + + public AppZoomOutDisplayAreaOrganizer(Context context, + DisplayLayout displayLayout, Executor mainExecutor) { + super(mainExecutor); + mContext = context; + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); + setDisplayLayout(displayLayout); + } + + @Override + public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo, SurfaceControl leash) { + leash.setUnreleasedWarningCallSite( + "AppZoomOutDisplayAreaOrganizer.onDisplayAreaAppeared"); + mDisplayAreaTokenMap.put(displayAreaInfo.token, leash); + } + + @Override + public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) { + final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token); + if (leash != null) { + leash.release(); + } + mDisplayAreaTokenMap.remove(displayAreaInfo.token); + } + + public void registerOrganizer() { + final List<DisplayAreaAppearedInfo> displayAreaInfos = registerOrganizer( + AppZoomOutDisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT); + for (int i = 0; i < displayAreaInfos.size(); i++) { + final DisplayAreaAppearedInfo info = displayAreaInfos.get(i); + onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash()); + } + } + + @Override + public void unregisterOrganizer() { + super.unregisterOrganizer(); + reset(); + } + + void setProgress(float progress) { + if (mProgress == progress) { + return; + } + + mProgress = progress; + apply(); + } + + void setIsHomeTaskFocused(boolean isHomeTaskFocused) { + if (mIsHomeTaskFocused != null && mIsHomeTaskFocused == isHomeTaskFocused) { + return; + } + + mIsHomeTaskFocused = isHomeTaskFocused; + apply(); + } + + private void apply() { + if (mIsHomeTaskFocused == null || mProgress == INVALID_PROGRESS) { + return; + } + + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + float scale = mProgress * (mIsHomeTaskFocused + ? PUSHBACK_SCALE_FOR_LAUNCHER : PUSHBACK_SCALE_FOR_APP); + mDisplayAreaTokenMap.forEach((token, leash) -> updateSurface(tx, leash, scale)); + tx.apply(); + } + + void setDisplayLayout(DisplayLayout displayLayout) { + mDisplayLayout.set(displayLayout); + } + + private void reset() { + setProgress(0); + mProgress = INVALID_PROGRESS; + mIsHomeTaskFocused = null; + } + + private void updateSurface(SurfaceControl.Transaction tx, SurfaceControl leash, float scale) { + if (scale == 0) { + // Reset when scale is set back to 0. + tx + .setCrop(leash, null) + .setScale(leash, 1, 1) + .setPosition(leash, 0, 0) + .setCornerRadius(leash, 0); + return; + } + + tx + // Rounded corner can only be applied if a crop is set. + .setCrop(leash, 0, 0, mDisplayLayout.width(), mDisplayLayout.height()) + .setScale(leash, 1 - scale, 1 - scale) + .setPosition(leash, scale * mDisplayLayout.width() * 0.5f, + scale * mDisplayLayout.height() * 0.5f) + .setCornerRadius(leash, mCornerRadius * (1 - scale)); + } + + void onRotateDisplay(Context context, int toRotation) { + if (mDisplayLayout.rotation() == toRotation) { + return; + } + mDisplayLayout.rotateTo(context.getResources(), toRotation); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoShellModule.java new file mode 100644 index 000000000000..fc51c754e06b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoShellModule.java @@ -0,0 +1,30 @@ +/* + * 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.wm.shell.automotive; + +import com.android.wm.shell.dagger.WMSingleton; + +import dagger.Binds; +import dagger.Module; + + +@Module +public abstract class AutoShellModule { + @WMSingleton + @Binds + abstract AutoTaskStackController provideTaskStackController(AutoTaskStackControllerImpl impl); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStack.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStack.kt new file mode 100644 index 000000000000..caacdd355996 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStack.kt @@ -0,0 +1,62 @@ +/* + * 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.wm.shell.automotive + +import android.app.ActivityManager +import android.graphics.Rect +import android.view.SurfaceControl + +/** + * Represents an auto task stack, which is always in multi-window mode. + * + * @property id The ID of the task stack. + * @property displayId The ID of the display the task stack is on. + * @property leash The surface control leash of the task stack. + */ +interface AutoTaskStack { + val id: Int + val displayId: Int + var leash: SurfaceControl +} + +/** + * Data class representing the state of an auto task stack. + * + * @property bounds The bounds of the task stack. + * @property childrenTasksVisible Whether the child tasks of the stack are visible. + * @property layer The layer of the task stack. + */ +data class AutoTaskStackState( + val bounds: Rect = Rect(), + val childrenTasksVisible: Boolean, + val layer: Int +) + +/** + * Data class representing a root task stack. + * + * @property id The ID of the root task stack + * @property displayId The ID of the display the root task stack is on. + * @property leash The surface control leash of the root task stack. + * @property rootTaskInfo The running task info of the root task. + */ +data class RootTaskStack( + override val id: Int, + override val displayId: Int, + override var leash: SurfaceControl, + var rootTaskInfo: ActivityManager.RunningTaskInfo +) : AutoTaskStack diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackController.kt new file mode 100644 index 000000000000..15fedac62af3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackController.kt @@ -0,0 +1,229 @@ +/* + * 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.wm.shell.automotive + +import android.app.PendingIntent +import android.content.Intent +import android.os.Bundle +import android.os.IBinder +import android.view.SurfaceControl +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TransitionFinishCallback + +/** + * Delegate interface for handling auto task stack transitions. + */ +interface AutoTaskStackTransitionHandlerDelegate { + /** + * Handles a transition request. + * + * @param transition The transition identifier. + * @param request The transition request information. + * @return An [AutoTaskStackTransaction] to be applied for the transition, or null if the + * animation is not handled by this delegate. + */ + fun handleRequest( + transition: IBinder, request: TransitionRequestInfo + ): AutoTaskStackTransaction? + + /** + * See [Transitions.TransitionHandler.startAnimation] for more details. + * + * @param changedTaskStacks Contains the states of the task stacks that were changed as a + * result of this transition. The key is the [AutoTaskStack.id] and the value is the + * corresponding [AutoTaskStackState]. + */ + fun startAnimation( + transition: IBinder, + changedTaskStacks: Map<Int, AutoTaskStackState>, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: TransitionFinishCallback + ): Boolean + + /** + * See [Transitions.TransitionHandler.onTransitionConsumed] for more details. + * + * @param requestedTaskStacks contains the states of the task stacks that were requested in + * the transition. The key is the [AutoTaskStack.id] and the value is the corresponding + * [AutoTaskStackState]. + */ + fun onTransitionConsumed( + transition: IBinder, + requestedTaskStacks: Map<Int, AutoTaskStackState>, + aborted: Boolean, finishTransaction: SurfaceControl.Transaction? + ) + + /** + * See [Transitions.TransitionHandler.mergeAnimation] for more details. + * + * @param changedTaskStacks Contains the states of the task stacks that were changed as a + * result of this transition. The key is the [AutoTaskStack.id] and the value is the + * corresponding [AutoTaskStackState]. + */ + fun mergeAnimation( + transition: IBinder, + changedTaskStacks: Map<Int, AutoTaskStackState>, + info: TransitionInfo, + surfaceTransaction: SurfaceControl.Transaction, + mergeTarget: IBinder, + finishCallback: TransitionFinishCallback + ) +} + + +/** + * Controller for managing auto task stacks. + */ +interface AutoTaskStackController { + + var autoTransitionHandlerDelegate: AutoTaskStackTransitionHandlerDelegate? + set + + /** + * Map of task stack IDs to their states. + * + * This gets updated right before [AutoTaskStackTransitionHandlerDelegate.startAnimation] or + * [AutoTaskStackTransitionHandlerDelegate.onTransitionConsumed] is called. + */ + val taskStackStateMap: Map<Int, AutoTaskStackState> + get + + /** + * Creates a new multi-window root task. + * + * A root task stack is placed in the default TDA of the specified display by default. + * Once the root task is removed, the [AutoTaskStackController] no longer holds a reference to + * it. + * + * @param displayId The ID of the display to create the root task stack on. + * @param listener The listener for root task stack events. + */ + @ShellMainThread + fun createRootTaskStack(displayId: Int, listener: RootTaskStackListener) + + + /** + * Sets the default root task stack (launch root) on a display. Calling it again with a + * different [rootTaskStackId] will simply replace the default root task stack on the display. + * + * Note: This is helpful for passively routing tasks to a specified container. If a display + * doesn't have a default root task stack set, all tasks will open in fullscreen and cover + * the entire default TDA by default. + * + * @param displayId The ID of the display. + * @param rootTaskStackId The ID of the root task stack, or null to clear the default. + */ + @ShellMainThread + fun setDefaultRootTaskStackOnDisplay(displayId: Int, rootTaskStackId: Int?) + + /** + * Starts a transaction with the specified [transaction]. + * Returns the transition identifier. + */ + @ShellMainThread + fun startTransition(transaction: AutoTaskStackTransaction): IBinder? +} + +internal sealed class TaskStackOperation { + data class ReparentTask( + val taskId: Int, + val parentTaskStackId: Int, + val onTop: Boolean + ) : TaskStackOperation() + + data class SendPendingIntent( + val sender: PendingIntent, + val intent: Intent, + val options: Bundle? + ) : TaskStackOperation() + + data class SetTaskStackState( + val taskStackId: Int, + val state: AutoTaskStackState + ) : TaskStackOperation() +} + +data class AutoTaskStackTransaction internal constructor( + internal val operations: MutableList<TaskStackOperation> = mutableListOf() +) { + constructor() : this( + mutableListOf() + ) + + /** See [WindowContainerTransaction.reparent] for more details. */ + fun reparentTask( + taskId: Int, + parentTaskStackId: Int, + onTop: Boolean + ): AutoTaskStackTransaction { + operations.add(TaskStackOperation.ReparentTask(taskId, parentTaskStackId, onTop)) + return this + } + + /** See [WindowContainerTransaction.sendPendingIntent] for more details. */ + fun sendPendingIntent( + sender: PendingIntent, + intent: Intent, + options: Bundle? + ): AutoTaskStackTransaction { + operations.add(TaskStackOperation.SendPendingIntent(sender, intent, options)) + return this + } + + /** + * Adds a set task stack state operation to the transaction. + * + * If an operation with the same task stack ID already exists, it is replaced with the new one. + * + * @param taskStackId The ID of the task stack. + * @param state The new state of the task stack. + * @return The transaction with the added operation. + */ + fun setTaskStackState(taskStackId: Int, state: AutoTaskStackState): AutoTaskStackTransaction { + val existingOperation = operations.find { + it is TaskStackOperation.SetTaskStackState && it.taskStackId == taskStackId + } + if (existingOperation != null) { + val index = operations.indexOf(existingOperation) + operations[index] = TaskStackOperation.SetTaskStackState(taskStackId, state) + } else { + operations.add(TaskStackOperation.SetTaskStackState(taskStackId, state)) + } + return this + } + + /** + * Returns a map of task stack IDs to their states from the set task stack state operations. + * + * @return The map of task stack IDs to states. + */ + fun getTaskStackStates(): Map<Int, AutoTaskStackState> { + val states = mutableMapOf<Int, AutoTaskStackState>() + operations.forEach { operation -> + if (operation is TaskStackOperation.SetTaskStackState) { + states[operation.taskStackId] = operation.state + } + } + return states + } +} + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt new file mode 100644 index 000000000000..f8f284238a98 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt @@ -0,0 +1,534 @@ +/* + * 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.wm.shell.automotive + +import android.app.ActivityManager +import android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT +import android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.os.IBinder +import android.util.Log +import android.util.Slog +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import android.view.WindowManager +import android.view.WindowManager.TRANSIT_CHANGE +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.systemui.car.Flags.autoTaskStackWindowing +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.dagger.WMSingleton +import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TransitionFinishCallback +import javax.inject.Inject + +const val TAG = "AutoTaskStackController" + +@WMSingleton +class AutoTaskStackControllerImpl @Inject constructor( + val taskOrganizer: ShellTaskOrganizer, + @ShellMainThread private val shellMainThread: ShellExecutor, + val transitions: Transitions, + val shellInit: ShellInit, + val rootTdaOrganizer: RootTaskDisplayAreaOrganizer +) : AutoTaskStackController, Transitions.TransitionHandler { + override var autoTransitionHandlerDelegate: AutoTaskStackTransitionHandlerDelegate? = null + override val taskStackStateMap = mutableMapOf<Int, AutoTaskStackState>() + + private val DBG = Log.isLoggable(TAG, Log.DEBUG) + private val taskStackMap = mutableMapOf<Int, AutoTaskStack>() + private val pendingTransitions = ArrayList<PendingTransition>() + private val mTaskStackStateTranslator = TaskStackStateTranslator() + private val appTasksMap = mutableMapOf<Int, ActivityManager.RunningTaskInfo>() + private val defaultRootTaskPerDisplay = mutableMapOf<Int, Int>() + + init { + if (!autoTaskStackWindowing()) { + throw IllegalStateException("Failed to initialize" + + "AutoTaskStackController as the auto_task_stack_windowing TS flag is disabled.") + } else { + shellInit.addInitCallback(this::onInit, this); + } + } + + fun onInit() { + transitions.addHandler(this) + } + + /** Translates the [AutoTaskStackState] to relevant WM and surface transactions. */ + inner class TaskStackStateTranslator { + // TODO(b/384946072): Move to an interface with 2 implementations, one for root task and + // other for TDA + fun applyVisibilityAndBounds( + wct: WindowContainerTransaction, + taskStack: AutoTaskStack, + state: AutoTaskStackState + ) { + if (taskStack !is RootTaskStack) { + Slog.e(TAG, "Unsupported task stack, unable to convertToWct") + return + } + wct.setBounds(taskStack.rootTaskInfo.token, state.bounds) + wct.reorder(taskStack.rootTaskInfo.token, /* onTop= */ state.childrenTasksVisible) + } + + fun reorderLeash( + taskStack: AutoTaskStack, + state: AutoTaskStackState, + transaction: Transaction + ) { + if (taskStack !is RootTaskStack) { + Slog.e(TAG, "Unsupported task stack, unable to reorder leash") + return + } + Slog.d(TAG, "Setting the layer ${state.layer}") + transaction.setLayer(taskStack.leash, state.layer) + } + + fun restoreLeash(taskStack: AutoTaskStack, transaction: Transaction) { + if (taskStack !is RootTaskStack) { + Slog.e(TAG, "Unsupported task stack, unable to restore leash") + return + } + + val rootTdaInfo = rootTdaOrganizer.getDisplayAreaInfo(taskStack.displayId) + if (rootTdaInfo == null || + rootTdaInfo.featureId != taskStack.rootTaskInfo.displayAreaFeatureId + ) { + Slog.e(TAG, "Cannot find the rootTDA for the root task stack ${taskStack.id}") + return + } + if (DBG) { + Slog.d(TAG, "Reparenting ${taskStack.id} leash to DA ${rootTdaInfo.featureId}") + } + transaction.reparent( + taskStack.leash, + rootTdaOrganizer.getDisplayAreaLeash(taskStack.displayId) + ) + } + } + + inner class RootTaskStackListenerAdapter( + val rootTaskStackListener: RootTaskStackListener, + ) : ShellTaskOrganizer.TaskListener { + private var rootTaskStack: RootTaskStack? = null + + // TODO(b/384948029): Notify car service for all the children tasks' events + override fun onTaskAppeared( + taskInfo: ActivityManager.RunningTaskInfo?, + leash: SurfaceControl? + ) { + if (taskInfo == null) { + throw IllegalArgumentException("taskInfo can't be null in onTaskAppeared") + } + if (leash == null) { + throw IllegalArgumentException("leash can't be null in onTaskAppeared") + } + if (DBG) Slog.d(TAG, "onTaskAppeared = ${taskInfo.taskId}") + + if (rootTaskStack == null) { + val rootTask = + RootTaskStack(taskInfo.taskId, taskInfo.displayId, leash, taskInfo) + taskStackMap[rootTask.id] = rootTask + + rootTaskStack = rootTask; + rootTaskStackListener.onRootTaskStackCreated(rootTask); + return + } + appTasksMap[taskInfo.taskId] = taskInfo + rootTaskStackListener.onTaskAppeared(taskInfo, leash) + } + + override fun onTaskInfoChanged(taskInfo: ActivityManager.RunningTaskInfo?) { + if (taskInfo == null) { + throw IllegalArgumentException("taskInfo can't be null in onTaskInfoChanged") + } + if (DBG) Slog.d(TAG, "onTaskInfoChanged = ${taskInfo.taskId}") + var previousRootTaskStackInfo = rootTaskStack ?: run { + Slog.e(TAG, "Received onTaskInfoChanged, when root task stack is null") + return@onTaskInfoChanged + } + rootTaskStack?.let { + if (taskInfo.taskId == previousRootTaskStackInfo.id) { + previousRootTaskStackInfo = previousRootTaskStackInfo.copy(rootTaskInfo = taskInfo) + taskStackMap[previousRootTaskStackInfo.id] = previousRootTaskStackInfo + rootTaskStack = previousRootTaskStackInfo; + rootTaskStackListener.onRootTaskStackInfoChanged(it) + return + } + } + + appTasksMap[taskInfo.taskId] = taskInfo + rootTaskStackListener.onTaskInfoChanged(taskInfo) + } + + override fun onTaskVanished(taskInfo: ActivityManager.RunningTaskInfo?) { + if (taskInfo == null) { + throw IllegalArgumentException("taskInfo can't be null in onTaskVanished") + } + if (DBG) Slog.d(TAG, "onTaskVanished = ${taskInfo.taskId}") + var rootTask = rootTaskStack ?: run { + Slog.e(TAG, "Received onTaskVanished, when root task stack is null") + return@onTaskVanished + } + if (taskInfo.taskId == rootTask.id) { + rootTask = rootTask.copy(rootTaskInfo = taskInfo) + rootTaskStack = rootTask + rootTaskStackListener.onRootTaskStackDestroyed(rootTask) + taskStackMap.remove(rootTask.id) + taskStackStateMap.remove(rootTask.id) + rootTaskStack = null + return + } + appTasksMap.remove(taskInfo.taskId) + rootTaskStackListener.onTaskVanished(taskInfo) + } + + override fun onBackPressedOnTaskRoot(taskInfo: ActivityManager.RunningTaskInfo?) { + if (taskInfo == null) { + throw IllegalArgumentException("taskInfo can't be null in onBackPressedOnTaskRoot") + } + super.onBackPressedOnTaskRoot(taskInfo) + rootTaskStackListener.onBackPressedOnTaskRoot(taskInfo) + } + } + + override fun createRootTaskStack( + displayId: Int, + listener: RootTaskStackListener + ) { + if (!autoTaskStackWindowing()) { + Slog.e( + TAG, "Failed to create root task stack as the " + + "auto_task_stack_windowing TS flag is disabled." + ) + return + } + taskOrganizer.createRootTask( + displayId, + WINDOWING_MODE_MULTI_WINDOW, + RootTaskStackListenerAdapter(listener), + /* removeWithTaskOrganizer= */ true + ) + } + + override fun setDefaultRootTaskStackOnDisplay(displayId: Int, rootTaskStackId: Int?) { + if (!autoTaskStackWindowing()) { + Slog.e( + TAG, "Failed to set default root task stack as the " + + "auto_task_stack_windowing TS flag is disabled." + ) + return + } + var wct = WindowContainerTransaction() + + // Clear the default root task stack if already set + defaultRootTaskPerDisplay[displayId]?.let { existingDefaultRootTaskStackId -> + (taskStackMap[existingDefaultRootTaskStackId] as? RootTaskStack)?.let { rootTaskStack -> + wct.setLaunchRoot(rootTaskStack.rootTaskInfo.token, null, null) + } + } + + if (rootTaskStackId != null) { + var taskStack = + taskStackMap[rootTaskStackId] ?: run { return@setDefaultRootTaskStackOnDisplay } + if (DBG) Slog.d(TAG, "setting launch root for = ${taskStack.id}") + if (taskStack !is RootTaskStack) { + throw IllegalArgumentException( + "Cannot set a non root task stack as default root task " + + "stack" + ) + } + wct.setLaunchRoot( + taskStack.rootTaskInfo.token, + intArrayOf(WINDOWING_MODE_UNDEFINED), + intArrayOf( + ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_RECENTS, + + // TODO(b/386242708): Figure out if this flag will ever be used for automotive + // assistant. Based on output, remove it from here and fix the + // AssistantStackTests accordingly. + ACTIVITY_TYPE_ASSISTANT + ) + ) + } + + taskOrganizer.applyTransaction(wct) + } + + override fun startTransition(transaction: AutoTaskStackTransaction): IBinder? { + if (!autoTaskStackWindowing()) { + Slog.e( + TAG, "Failed to start transaction as the " + + "auto_task_stack_windowing TS flag is disabled." + ) + return null + } + if (transaction.operations.isEmpty()) { + Slog.e(TAG, "Operations empty, no transaction started") + return null + } + if (DBG) Slog.d(TAG, "startTransaction ${transaction.operations}") + + var wct = WindowContainerTransaction() + convertToWct(transaction, wct) + var pending = PendingTransition( + TRANSIT_CHANGE, + wct, + transaction, + ) + return startTransitionNow(pending) + } + + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo + ): WindowContainerTransaction? { + if (DBG) { + Slog.d(TAG, "handle request, id=${request.debugId}, type=${request.type}, " + + "triggertask = ${request.triggerTask ?: "null"}") + } + val ast = autoTransitionHandlerDelegate?.handleRequest(transition, request) + ?: run { return@handleRequest null } + + if (ast.operations.isEmpty()) { + return null + } + var wct = WindowContainerTransaction() + convertToWct(ast, wct) + + pendingTransitions.add( + PendingTransition(request.type, wct, ast).apply { isClaimed = transition } + ) + return wct + } + + fun updateTaskStackStates(taskStatStates: Map<Int, AutoTaskStackState>) { + taskStackStateMap.putAll(taskStatStates) + } + + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: Transaction, + finishTransaction: Transaction, + finishCallback: TransitionFinishCallback + ): Boolean { + if (DBG) Slog.d(TAG, " startAnimation, id=${info.debugId} = changes=" + info.changes) + val pending: PendingTransition? = findPending(transition) + if (pending != null) { + pendingTransitions.remove(pending) + updateTaskStackStates(pending.transaction.getTaskStackStates()) + } + + reorderLeashes(startTransaction) + reorderLeashes(finishTransaction) + + for (chg in info.changes) { + // TODO(b/384946072): handle the da stack similarly. The below implementation only + // handles the root task stack + + val taskInfo = chg.taskInfo ?: continue + val taskStack = taskStackMap[taskInfo.taskId] ?: continue + + // Restore the leashes for the task stacks to ensure correct z-order competition + if (taskStackMap.containsKey(taskInfo.taskId)) { + mTaskStackStateTranslator.restoreLeash( + taskStack, + startTransaction + ) + if (TransitionUtil.isOpeningMode(chg.mode)) { + // Clients can still manipulate the alpha, but this ensures that the default + // behavior is natural + startTransaction.setAlpha(chg.leash, 1f) + } + continue + } + } + + val isPlayedByDelegate = autoTransitionHandlerDelegate?.startAnimation( + transition, + pending?.transaction?.getTaskStackStates() ?: mapOf(), + info, + startTransaction, + finishTransaction, + { + shellMainThread.execute { + finishCallback.onTransitionFinished(it) + startNextTransition() + } + } + ) ?: false + + if (isPlayedByDelegate) { + if (DBG) Slog.d(TAG, "${info.debugId} played"); + return true; + } + + // If for an animation which is not played by the delegate, contains a change in a known + // task stack, it should be leveraged to correct the leashes. So, handle the animation in + // this case. + if (info.changes.any { taskStackMap.containsKey(it.taskInfo?.taskId) }) { + startTransaction.apply() + finishCallback.onTransitionFinished(null) + startNextTransition() + if (DBG) Slog.d(TAG, "${info.debugId} played"); + return true + } + return false; + } + + fun convertToWct(ast: AutoTaskStackTransaction, wct: WindowContainerTransaction) { + ast.operations.forEach { operation -> + when (operation) { + is TaskStackOperation.ReparentTask -> { + val appTask = appTasksMap[operation.taskId] + + if (appTask == null) { + Slog.e( + TAG, "task with id=$operation.taskId not found, failed to " + + "reparent." + ) + return@forEach + } + if (!taskStackMap.containsKey(operation.parentTaskStackId)) { + Slog.e( + TAG, "task stack with id=${operation.parentTaskStackId} not " + + "found, failed to reparent" + ) + return@forEach + } + // TODO(b/384946072): Handle a display area stack as well + wct.reparent( + appTask.token, + (taskStackMap[operation.parentTaskStackId] as RootTaskStack) + .rootTaskInfo.token, + operation.onTop + ) + } + + is TaskStackOperation.SendPendingIntent -> wct.sendPendingIntent( + operation.sender, + operation.intent, + operation.options + ) + + is TaskStackOperation.SetTaskStackState -> { + taskStackMap[operation.taskStackId]?.let { taskStack -> + mTaskStackStateTranslator.applyVisibilityAndBounds( + wct, + taskStack, + operation.state + ) + } + ?: Slog.w(TAG, "AutoTaskStack with id ${operation.taskStackId} " + + "not found.") + } + } + } + } + + override fun mergeAnimation( + transition: IBinder, + info: TransitionInfo, + surfaceTransaction: Transaction, + mergeTarget: IBinder, + finishCallback: TransitionFinishCallback + ) { + val pending: PendingTransition? = findPending(transition) + + autoTransitionHandlerDelegate?.mergeAnimation( + transition, + pending?.transaction?.getTaskStackStates() ?: mapOf(), + info, + surfaceTransaction, + mergeTarget, + /* finishCallback = */ { + shellMainThread.execute { + finishCallback.onTransitionFinished(it) + } + } + ) + } + + override fun onTransitionConsumed( + transition: IBinder, + aborted: Boolean, + finishTransaction: Transaction? + ) { + val pending: PendingTransition? = findPending(transition) + if (pending != null) { + pendingTransitions.remove(pending) + updateTaskStackStates(pending.transaction.getTaskStackStates()) + // Still update the surface order because this means wm didn't lead to any change + if (finishTransaction != null) { + reorderLeashes(finishTransaction) + } + } + autoTransitionHandlerDelegate?.onTransitionConsumed( + transition, + pending?.transaction?.getTaskStackStates() ?: mapOf(), + aborted, + finishTransaction + ) + } + + private fun reorderLeashes(transaction: SurfaceControl.Transaction) { + taskStackStateMap.forEach { (taskId, taskStackState) -> + taskStackMap[taskId]?.let { taskStack -> + mTaskStackStateTranslator.reorderLeash(taskStack, taskStackState, transaction) + } ?: Slog.w(TAG, "Warning: AutoTaskStack with id $taskId not found.") + } + } + + private fun findPending(claimed: IBinder) = pendingTransitions.find { it.isClaimed == claimed } + + private fun startTransitionNow(pending: PendingTransition): IBinder { + val claimedTransition = transitions.startTransition(pending.mType, pending.wct, this) + pending.isClaimed = claimedTransition + pendingTransitions.add(pending) + return claimedTransition + } + + fun startNextTransition() { + if (pendingTransitions.isEmpty()) return + val pending: PendingTransition = pendingTransitions[0] + if (pending.isClaimed != null) { + // Wait for this to start animating. + return + } + pending.isClaimed = transitions.startTransition(pending.mType, pending.wct, this) + } + + internal class PendingTransition( + @field:WindowManager.TransitionType @param:WindowManager.TransitionType val mType: Int, + val wct: WindowContainerTransaction, + val transaction: AutoTaskStackTransaction, + ) { + var isClaimed: IBinder? = null + } + +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/RootTaskStackListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/RootTaskStackListener.kt new file mode 100644 index 000000000000..9d121b144492 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/RootTaskStackListener.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.automotive + +import com.android.wm.shell.ShellTaskOrganizer + +/** + * A [TaskListener] which simplifies the interface when used for + * [ShellTaskOrganizer.createRootTask]. + * + * [onRootTaskStackCreated], [onRootTaskStackInfoChanged], [onRootTaskStackDestroyed] will be called + * for the underlying root task. + * The [onTaskAppeared], [onTaskInfoChanged], [onTaskVanished] are called for the children tasks. + */ +interface RootTaskStackListener : ShellTaskOrganizer.TaskListener { + fun onRootTaskStackCreated(rootTaskStack: RootTaskStack) + fun onRootTaskStackInfoChanged(rootTaskStack: RootTaskStack) + fun onRootTaskStackDestroyed(rootTaskStack: RootTaskStack) +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index e96bc02c1198..8dabd54a01ff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -28,7 +28,6 @@ import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; -import static com.android.window.flags.Flags.predictiveBackSystemAnims; import static com.android.window.flags.Flags.unifyBackNavigationTransition; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; @@ -40,23 +39,17 @@ import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; import android.app.TaskInfo; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.res.Configuration; -import android.database.ContentObserver; import android.graphics.Point; import android.graphics.Rect; import android.hardware.input.InputManager; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SystemClock; -import android.os.SystemProperties; -import android.os.UserHandle; -import android.provider.Settings.Global; import android.util.Log; import android.view.IRemoteAnimationRunner; import android.view.InputDevice; @@ -92,7 +85,6 @@ import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.TransitionUtil; -import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -102,7 +94,6 @@ import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; /** @@ -111,14 +102,7 @@ import java.util.function.Predicate; public class BackAnimationController implements RemoteCallable<BackAnimationController>, ConfigurationChangeListener { private static final String TAG = "ShellBackPreview"; - private static final int SETTING_VALUE_OFF = 0; - private static final int SETTING_VALUE_ON = 1; - public static final boolean IS_ENABLED = - SystemProperties.getInt("persist.wm.debug.predictive_back", - SETTING_VALUE_ON) == SETTING_VALUE_ON; - - /** Predictive back animation developer option */ - private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); + /** * Max duration to wait for an animation to finish before triggering the real back. */ @@ -148,11 +132,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private boolean mReceivedNullNavigationInfo = false; private final IActivityTaskManager mActivityTaskManager; private final Context mContext; - private final ContentResolver mContentResolver; private final ShellController mShellController; private final ShellCommandHandler mShellCommandHandler; private final ShellExecutor mShellExecutor; - private final Handler mBgHandler; private final WindowManager mWindowManager; private final Transitions mTransitions; @VisibleForTesting @@ -245,7 +227,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull ShellInit shellInit, @NonNull ShellController shellController, @NonNull @ShellMainThread ShellExecutor shellExecutor, - @NonNull @ShellBackgroundThread Handler backgroundHandler, Context context, @NonNull BackAnimationBackground backAnimationBackground, ShellBackAnimationRegistry shellBackAnimationRegistry, @@ -256,10 +237,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont shellInit, shellController, shellExecutor, - backgroundHandler, ActivityTaskManager.getService(), context, - context.getContentResolver(), backAnimationBackground, shellBackAnimationRegistry, shellCommandHandler, @@ -272,10 +251,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull ShellInit shellInit, @NonNull ShellController shellController, @NonNull @ShellMainThread ShellExecutor shellExecutor, - @NonNull @ShellBackgroundThread Handler bgHandler, @NonNull IActivityTaskManager activityTaskManager, Context context, - ContentResolver contentResolver, @NonNull BackAnimationBackground backAnimationBackground, ShellBackAnimationRegistry shellBackAnimationRegistry, ShellCommandHandler shellCommandHandler, @@ -285,10 +262,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellExecutor = shellExecutor; mActivityTaskManager = activityTaskManager; mContext = context; - mContentResolver = contentResolver; mRequirePointerPilfer = context.getResources().getBoolean(R.bool.config_backAnimationRequiresPointerPilfer); - mBgHandler = bgHandler; shellInit.addInitCallback(this::onInit, this); mAnimationBackground = backAnimationBackground; mShellBackAnimationRegistry = shellBackAnimationRegistry; @@ -305,8 +280,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private void onInit() { - setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler); - updateEnableAnimationFromFlags(); createAdapter(); mShellController.addExternalInterface(IBackAnimation.DESCRIPTOR, this::createExternalInterface, this); @@ -314,42 +287,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellController.addConfigurationChangeListener(this); } - private void setupAnimationDeveloperSettingsObserver( - @NonNull ContentResolver contentResolver, - @NonNull @ShellBackgroundThread final Handler backgroundHandler) { - if (predictiveBackSystemAnims()) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation aconfig flag is enabled, therefore " - + "developer settings flag is ignored and no content observer registered"); - return; - } - ContentObserver settingsObserver = new ContentObserver(backgroundHandler) { - @Override - public void onChange(boolean selfChange, Uri uri) { - updateEnableAnimationFromFlags(); - } - }; - contentResolver.registerContentObserver( - Global.getUriFor(Global.ENABLE_BACK_ANIMATION), - false, settingsObserver, UserHandle.USER_SYSTEM - ); - } - - /** - * Updates {@link BackAnimationController#mEnableAnimations} based on the current values of the - * aconfig flag and the developer settings flag - */ - @ShellBackgroundThread - private void updateEnableAnimationFromFlags() { - boolean isEnabled = predictiveBackSystemAnims() || isDeveloperSettingEnabled(); - mEnableAnimations.set(isEnabled); - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled); - } - - private boolean isDeveloperSettingEnabled() { - return Global.getInt(mContext.getContentResolver(), - Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF) == SETTING_VALUE_ON; - } - public BackAnimation getBackAnimationImpl() { return mBackAnimation; } @@ -617,14 +554,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void startBackNavigation(@NonNull BackTouchTracker touchTracker) { try { startLatencyTracking(); - final BackAnimationAdapter adapter = mEnableAnimations.get() - ? mBackAnimationAdapter : null; - if (adapter != null && mShellBackAnimationRegistry.hasSupportedAnimatorsChanged()) { - adapter.updateSupportedAnimators( + if (mBackAnimationAdapter != null + && mShellBackAnimationRegistry.hasSupportedAnimatorsChanged()) { + mBackAnimationAdapter.updateSupportedAnimators( mShellBackAnimationRegistry.getSupportedAnimators()); } mBackNavigationInfo = mActivityTaskManager.startBackNavigation( - mNavigationObserver, adapter); + mNavigationObserver, mBackAnimationAdapter); onBackNavigationInfoReceived(mBackNavigationInfo, touchTracker); } catch (RemoteException remoteException) { Log.e(TAG, "Failed to initAnimation", remoteException); @@ -696,9 +632,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private boolean shouldDispatchToAnimator() { - return mEnableAnimations.get() - && mBackNavigationInfo != null - && mBackNavigationInfo.isPrepareRemoteAnimation(); + return mBackNavigationInfo != null && mBackNavigationInfo.isPrepareRemoteAnimation(); } private void tryPilferPointers() { @@ -1093,7 +1027,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont () -> mShellExecutor.execute(this::onBackAnimationFinished)); if (mApps.length >= 1) { - mCurrentTracker.updateStartLocation(); BackMotionEvent startEvent = mCurrentTracker.createStartEvent(mApps[0]); dispatchOnBackStarted(mActiveCallback, startEvent); if (startEvent.getSwipeEdge() == EDGE_NONE) { @@ -1194,7 +1127,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont */ private void dump(PrintWriter pw, String prefix) { pw.println(prefix + "BackAnimationController state:"); - pw.println(prefix + " mEnableAnimations=" + mEnableAnimations.get()); pw.println(prefix + " mBackGestureStarted=" + mBackGestureStarted); pw.println(prefix + " mPostCommitAnimationInProgress=" + mPostCommitAnimationInProgress); pw.println(prefix + " mShouldStartOnNextMoveEvent=" + mShouldStartOnNextMoveEvent); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java index 4569cf31dab1..b9fccc1c4147 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java @@ -106,11 +106,12 @@ public class BackAnimationRunner { private Runnable mFinishedCallback; private RemoteAnimationTarget[] mApps; - private IRemoteAnimationFinishedCallback mRemoteCallback; + private RemoteAnimationFinishedStub mRemoteCallback; private static class RemoteAnimationFinishedStub extends IRemoteAnimationFinishedCallback.Stub { //the binder callback should not hold strong reference to it to avoid memory leak. - private WeakReference<BackAnimationRunner> mRunnerRef; + private final WeakReference<BackAnimationRunner> mRunnerRef; + private boolean mAbandoned; private RemoteAnimationFinishedStub(BackAnimationRunner runner) { mRunnerRef = new WeakReference<>(runner); @@ -118,23 +119,29 @@ public class BackAnimationRunner { @Override public void onAnimationFinished() { - BackAnimationRunner runner = mRunnerRef.get(); + synchronized (this) { + if (mAbandoned) { + return; + } + } + final BackAnimationRunner runner = mRunnerRef.get(); if (runner == null) { return; } - if (runner.shouldMonitorCUJ(runner.mApps)) { - InteractionJankMonitor.getInstance().end(runner.mCujType); - } + runner.onAnimationFinish(this); + } - runner.mFinishedCallback.run(); - for (int i = runner.mApps.length - 1; i >= 0; --i) { - SurfaceControl sc = runner.mApps[i].leash; - if (sc != null && sc.isValid()) { - sc.release(); - } + void abandon() { + synchronized (this) { + mAbandoned = true; + final BackAnimationRunner runner = mRunnerRef.get(); + if (runner == null) { + return; + } + if (runner.shouldMonitorCUJ(runner.mApps)) { + InteractionJankMonitor.getInstance().end(runner.mCujType); + } } - runner.mApps = null; - runner.mFinishedCallback = null; } } @@ -144,13 +151,16 @@ public class BackAnimationRunner { */ void startAnimation(RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, Runnable finishedCallback) { - InteractionJankMonitor interactionJankMonitor = InteractionJankMonitor.getInstance(); + if (mRemoteCallback != null) { + mRemoteCallback.abandon(); + mRemoteCallback = null; + } + mRemoteCallback = new RemoteAnimationFinishedStub(this); mFinishedCallback = finishedCallback; mApps = apps; - if (mRemoteCallback == null) mRemoteCallback = new RemoteAnimationFinishedStub(this); mWaitingAnimation = false; if (shouldMonitorCUJ(apps)) { - interactionJankMonitor.begin( + InteractionJankMonitor.getInstance().begin( apps[0].leash, mContext, mHandler, mCujType); } try { @@ -161,6 +171,28 @@ public class BackAnimationRunner { } } + void onAnimationFinish(RemoteAnimationFinishedStub finished) { + mHandler.post(() -> { + if (mRemoteCallback != null && finished != mRemoteCallback) { + return; + } + if (shouldMonitorCUJ(mApps)) { + InteractionJankMonitor.getInstance().end(mCujType); + } + + mFinishedCallback.run(); + for (int i = mApps.length - 1; i >= 0; --i) { + final SurfaceControl sc = mApps[i].leash; + if (sc != null && sc.isValid()) { + sc.release(); + } + } + mApps = null; + mFinishedCallback = null; + mRemoteCallback = null; + }); + } + @VisibleForTesting boolean shouldMonitorCUJ(RemoteAnimationTarget[] apps) { return apps.length > 0 && mCujType != NO_CUJ; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index b796b411dd1a..1323fe0fa9ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -20,7 +20,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; import android.content.ComponentName; import android.content.Context; @@ -60,7 +59,6 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; import dagger.Lazy; @@ -73,6 +71,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.IntPredicate; import java.util.function.Predicate; /** @@ -198,9 +197,6 @@ public class CompatUIController implements OnDisplaysChangedListener, private final CompatUIStatusManager mCompatUIStatusManager; @NonNull - private final FocusTransitionObserver mFocusTransitionObserver; - - @NonNull private final Optional<DesktopUserRepositories> mDesktopUserRepositories; public CompatUIController(@NonNull Context context, @@ -217,8 +213,7 @@ public class CompatUIController implements OnDisplaysChangedListener, @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler, @NonNull AccessibilityManager accessibilityManager, @NonNull CompatUIStatusManager compatUIStatusManager, - @NonNull Optional<DesktopUserRepositories> desktopUserRepositories, - @NonNull FocusTransitionObserver focusTransitionObserver) { + @NonNull Optional<DesktopUserRepositories> desktopUserRepositories) { mContext = context; mShellController = shellController; mDisplayController = displayController; @@ -235,7 +230,6 @@ public class CompatUIController implements OnDisplaysChangedListener, DISAPPEAR_DELAY_MS, flags); mCompatUIStatusManager = compatUIStatusManager; mDesktopUserRepositories = desktopUserRepositories; - mFocusTransitionObserver = focusTransitionObserver; shellInit.addInitCallback(this::onInit, this); } @@ -412,8 +406,7 @@ public class CompatUIController implements OnDisplaysChangedListener, // start tracking the buttons visibility for this task. if (mTopActivityTaskId != taskInfo.taskId && !taskInfo.isTopActivityTransparent - && taskInfo.isVisible - && mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)) { + && taskInfo.isVisible && taskInfo.isFocused) { mTopActivityTaskId = taskInfo.taskId; setHasShownUserAspectRatioSettingsButton(false); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java index c493aadd57b0..151dc438702d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java @@ -20,6 +20,7 @@ import android.os.HandlerThread; import androidx.annotation.Nullable; +import com.android.wm.shell.appzoomout.AppZoomOut; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.desktopmode.DesktopMode; @@ -112,4 +113,7 @@ public interface WMComponent { */ @WMSingleton Optional<DesktopMode> getDesktopMode(); + + @WMSingleton + Optional<AppZoomOut> getAppZoomOut(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 1408b6efc7f9..ab3c33ec7e43 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -91,6 +91,7 @@ import com.android.wm.shell.compatui.impl.DefaultComponentIdGenerator; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; import com.android.wm.shell.displayareahelper.DisplayAreaHelperController; import com.android.wm.shell.freeform.FreeformComponents; @@ -108,10 +109,11 @@ import com.android.wm.shell.recents.TaskStackTransitionObserver; import com.android.wm.shell.shared.ShellTransitions; import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.annotations.ShellAnimationThread; -import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.annotations.ShellSplashscreenThread; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.appzoomout.AppZoomOut; +import com.android.wm.shell.appzoomout.AppZoomOutController; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.startingsurface.StartingSurface; @@ -273,8 +275,7 @@ public abstract class WMShellBaseModule { @NonNull CompatUIState compatUIState, @NonNull CompatUIComponentIdGenerator componentIdGenerator, @NonNull CompatUIComponentFactory compatUIComponentFactory, - CompatUIStatusManager compatUIStatusManager, - @NonNull FocusTransitionObserver focusTransitionObserver) { + CompatUIStatusManager compatUIStatusManager) { if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) { return Optional.empty(); } @@ -299,8 +300,7 @@ public abstract class WMShellBaseModule { compatUIShellCommandHandler.get(), accessibilityManager.get(), compatUIStatusManager, - desktopUserRepositories, - focusTransitionObserver)); + desktopUserRepositories)); } @WMSingleton @@ -438,29 +438,24 @@ public abstract class WMShellBaseModule { ShellInit shellInit, ShellController shellController, @ShellMainThread ShellExecutor shellExecutor, - @ShellBackgroundThread Handler backgroundHandler, BackAnimationBackground backAnimationBackground, Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry, ShellCommandHandler shellCommandHandler, Transitions transitions, @ShellMainThread Handler handler ) { - if (BackAnimationController.IS_ENABLED) { return shellBackAnimationRegistry.map( (animations) -> new BackAnimationController( shellInit, shellController, shellExecutor, - backgroundHandler, context, backAnimationBackground, animations, shellCommandHandler, transitions, handler)); - } - return Optional.empty(); } @BindsOptionalOf @@ -1039,6 +1034,38 @@ public abstract class WMShellBaseModule { }); } + @WMSingleton + @Provides + static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() { + return new DesktopWallpaperActivityTokenProvider(); + } + + @WMSingleton + @Provides + static Optional<DesktopWallpaperActivityTokenProvider> + provideOptionalDesktopWallpaperActivityTokenProvider( + Context context, + DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { + return Optional.of(desktopWallpaperActivityTokenProvider); + } + return Optional.empty(); + } + + // + // App zoom out (optional feature) + // + + @WMSingleton + @Provides + static Optional<AppZoomOut> provideAppZoomOut( + Optional<AppZoomOutController> appZoomOutController) { + return appZoomOutController.map((controller) -> controller.asAppZoomOut()); + } + + @BindsOptionalOf + abstract AppZoomOutController optionalAppZoomOutController(); + // // Task Stack // @@ -1083,6 +1110,7 @@ public abstract class WMShellBaseModule { Optional<RecentTasksController> recentTasksOptional, Optional<RecentsTransitionHandler> recentsTransitionHandlerOptional, Optional<OneHandedController> oneHandedControllerOptional, + Optional<AppZoomOutController> appZoomOutControllerOptional, Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional, Optional<ActivityEmbeddingController> activityEmbeddingOptional, Optional<MixedTransitionHandler> mixedTransitionHandler, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 1916215dea74..e8add56619c4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -50,6 +50,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; import com.android.wm.shell.apptoweb.AssistContentRequester; +import com.android.wm.shell.appzoomout.AppZoomOutController; import com.android.wm.shell.back.BackAnimationController; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; @@ -945,7 +946,8 @@ public abstract class WMShellModule { FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - WindowDecorTaskResourceLoader taskResourceLoader + WindowDecorTaskResourceLoader taskResourceLoader, + RecentsTransitionHandler recentsTransitionHandler ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -961,7 +963,7 @@ public abstract class WMShellModule { desktopTasksLimiter, appHandleEducationController, appToWebEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader)); + taskResourceLoader, recentsTransitionHandler)); } @WMSingleton @@ -1312,10 +1314,21 @@ public abstract class WMShellModule { return new DesktopModeUiEventLogger(uiEventLogger, packageManager); } + // + // App zoom out + // + @WMSingleton @Provides - static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() { - return new DesktopWallpaperActivityTokenProvider(); + static AppZoomOutController provideAppZoomOutController( + Context context, + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + DisplayController displayController, + DisplayLayout displayLayout, + @ShellMainThread ShellExecutor mainExecutor) { + return AppZoomOutController.create(context, shellInit, shellTaskOrganizer, + displayController, displayLayout, mainExecutor); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 03f388c9f1c9..c8d0dab39837 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -41,6 +41,7 @@ import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip2.phone.PhonePipMenuController; import com.android.wm.shell.pip2.phone.PipController; import com.android.wm.shell.pip2.phone.PipMotionHelper; @@ -82,11 +83,14 @@ public abstract class Pip2Module { @NonNull PipTransitionState pipStackListenerController, @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull PipUiStateChangeController pipUiStateChangeController, - Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) { + Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener, pipScheduler, pipStackListenerController, pipDisplayLayoutState, - pipUiStateChangeController, desktopUserRepositoriesOptional); + pipUiStateChangeController, desktopUserRepositoriesOptional, + desktopWallpaperActivityTokenProviderOptional); } @WMSingleton @@ -117,6 +121,7 @@ public abstract class Pip2Module { PipTouchHandler pipTouchHandler, PipAppOpsListener pipAppOpsListener, PhonePipMenuController pipMenuController, + PipUiEventLogger pipUiEventLogger, @ShellMainThread ShellExecutor mainExecutor) { if (!PipUtils.isPip2ExperimentEnabled()) { return Optional.empty(); @@ -126,7 +131,7 @@ public abstract class Pip2Module { displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, pipTransitionState, pipTouchHandler, pipAppOpsListener, pipMenuController, - mainExecutor)); + pipUiEventLogger, mainExecutor)); } } @@ -137,9 +142,12 @@ public abstract class Pip2Module { @ShellMainThread ShellExecutor mainExecutor, PipTransitionState pipTransitionState, Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState, - desktopUserRepositoriesOptional, rootTaskDisplayAreaOrganizer); + desktopUserRepositoriesOptional, desktopWallpaperActivityTokenProviderOptional, + rootTaskDisplayAreaOrganizer); } @WMSingleton @@ -188,11 +196,11 @@ public abstract class Pip2Module { FloatingContentCoordinator floatingContentCoordinator, PipScheduler pipScheduler, Optional<PipPerfHintController> pipPerfHintControllerOptional, - PipBoundsAlgorithm pipBoundsAlgorithm, - PipTransitionState pipTransitionState) { + PipTransitionState pipTransitionState, + PipUiEventLogger pipUiEventLogger) { return new PipMotionHelper(context, pipBoundsState, menuController, pipSnapAlgorithm, floatingContentCoordinator, pipScheduler, pipPerfHintControllerOptional, - pipBoundsAlgorithm, pipTransitionState); + pipTransitionState, pipUiEventLogger); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index d3066645f32e..1a58363dab81 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -27,6 +27,7 @@ import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread @@ -56,6 +57,10 @@ class DesktopRepository( * @property topTransparentFullscreenTaskId the task id of any current top transparent * fullscreen task launched on top of Desktop Mode. Cleared when the transparent task is * closed or sent to back. (top is at index 0). + * @property pipTaskId the task id of PiP task entered while in Desktop Mode. + * @property pipShouldKeepDesktopActive whether an active PiP window should keep the Desktop + * Mode session active. Only false when we are explicitly exiting Desktop Mode (via user + * action) while there is an active PiP window. */ private data class DesktopTaskData( val activeTasks: ArraySet<Int> = ArraySet(), @@ -66,6 +71,8 @@ class DesktopRepository( val freeformTasksInZOrder: ArrayList<Int> = ArrayList(), var fullImmersiveTaskId: Int? = null, var topTransparentFullscreenTaskId: Int? = null, + var pipTaskId: Int? = null, + var pipShouldKeepDesktopActive: Boolean = true, ) { fun deepCopy(): DesktopTaskData = DesktopTaskData( @@ -76,6 +83,8 @@ class DesktopRepository( freeformTasksInZOrder = ArrayList(freeformTasksInZOrder), fullImmersiveTaskId = fullImmersiveTaskId, topTransparentFullscreenTaskId = topTransparentFullscreenTaskId, + pipTaskId = pipTaskId, + pipShouldKeepDesktopActive = pipShouldKeepDesktopActive, ) fun clear() { @@ -86,6 +95,8 @@ class DesktopRepository( freeformTasksInZOrder.clear() fullImmersiveTaskId = null topTransparentFullscreenTaskId = null + pipTaskId = null + pipShouldKeepDesktopActive = true } } @@ -104,6 +115,9 @@ class DesktopRepository( /* Tracks last bounds of task before toggled to immersive state. */ private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>() + /* Callback for when a pending PiP transition has been aborted. */ + private var onPipAbortedCallback: ((Int, Int) -> Unit)? = null + private var desktopGestureExclusionListener: Consumer<Region>? = null private var desktopGestureExclusionExecutor: Executor? = null @@ -302,6 +316,54 @@ class DesktopRepository( } } + /** Set whether the given task is the Desktop-entered PiP task in this display. */ + fun setTaskInPip(displayId: Int, taskId: Int, enterPip: Boolean) { + val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId) + if (enterPip) { + desktopData.pipTaskId = taskId + desktopData.pipShouldKeepDesktopActive = true + } else { + desktopData.pipTaskId = + if (desktopData.pipTaskId == taskId) null + else { + logW( + "setTaskInPip: taskId=$taskId did not match saved taskId=${desktopData.pipTaskId}" + ) + desktopData.pipTaskId + } + } + notifyVisibleTaskListeners(displayId, getVisibleTaskCount(displayId)) + } + + /** Returns whether there is a PiP that was entered/minimized from Desktop in this display. */ + fun isMinimizedPipPresentInDisplay(displayId: Int): Boolean = + desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId != null + + /** Returns whether the given task is the Desktop-entered PiP task in this display. */ + fun isTaskMinimizedPipInDisplay(displayId: Int, taskId: Int): Boolean = + desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId == taskId + + /** Returns whether Desktop session should be active in this display due to active PiP. */ + fun shouldDesktopBeActiveForPip(displayId: Int): Boolean = + Flags.enableDesktopWindowingPip() && + isMinimizedPipPresentInDisplay(displayId) && + desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive + + /** Saves whether a PiP window should keep Desktop session active in this display. */ + fun setPipShouldKeepDesktopActive(displayId: Int, keepActive: Boolean) { + desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive = keepActive + } + + /** Saves callback to handle a pending PiP transition being aborted. */ + fun setOnPipAbortedCallback(callbackIfPipAborted: ((Int, Int) -> Unit)?) { + onPipAbortedCallback = callbackIfPipAborted + } + + /** Invokes callback to handle a pending PiP transition with the given task id being aborted. */ + fun onPipAborted(displayId: Int, pipTaskId: Int) { + onPipAbortedCallback?.invoke(displayId, pipTaskId) + } + /** Set whether the given task is the full-immersive task in this display. */ fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) { val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId) @@ -338,8 +400,12 @@ class DesktopRepository( } private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) { + val visibleAndPipTasksCount = + if (shouldDesktopBeActiveForPip(displayId)) visibleTasksCount + 1 else visibleTasksCount visibleTasksListeners.forEach { (listener, executor) -> - executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) } + executor.execute { + listener.onTasksVisibilityChanged(displayId, visibleAndPipTasksCount) + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index ee817b34b24a..6013648c9806 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -225,7 +225,6 @@ class DesktopTasksController( // Launch cookie used to identify a drag and drop transition to fullscreen after it has begun. // Used to prevent handleRequest from moving the new fullscreen task to freeform. private var dragAndDropFullscreenCookie: Binder? = null - private var pendingPipTransitionAndTask: Pair<IBinder, Int>? = null init { desktopMode = DesktopModeImpl() @@ -310,24 +309,40 @@ class DesktopTasksController( transitions.startTransition(transitionType, wct, handler).also { t -> handler?.setTransition(t) } + + // launch from recent DesktopTaskView + desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( + FREEFORM_ANIMATION_DURATION + ) } /** Gets number of visible freeform tasks in [displayId]. */ fun visibleTaskCount(displayId: Int): Int = taskRepository.getVisibleTaskCount(displayId) /** - * Returns true if any freeform tasks are visible or if a transparent fullscreen task exists on - * top in Desktop Mode. + * Returns true if any of the following is true: + * - Any freeform tasks are visible + * - A transparent fullscreen task exists on top in Desktop Mode + * - PiP on Desktop Windowing is enabled, there is an active PiP window and the desktop + * wallpaper is visible. */ fun isDesktopModeShowing(displayId: Int): Boolean { + val hasVisibleTasks = visibleTaskCount(displayId) > 0 + val hasTopTransparentFullscreenTask = + taskRepository.getTopTransparentFullscreenTaskId(displayId) != null + val hasMinimizedPip = + Flags.enableDesktopWindowingPip() && + taskRepository.isMinimizedPipPresentInDisplay(displayId) && + desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible(displayId) if ( DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() ) { - return visibleTaskCount(displayId) > 0 || - taskRepository.getTopTransparentFullscreenTaskId(displayId) != null + return hasVisibleTasks || hasTopTransparentFullscreenTask || hasMinimizedPip + } else if (Flags.enableDesktopWindowingPip()) { + return hasVisibleTasks || hasMinimizedPip } - return visibleTaskCount(displayId) > 0 + return hasVisibleTasks } /** Moves focused task to desktop mode for given [displayId]. */ @@ -587,7 +602,7 @@ class DesktopTasksController( ): ((IBinder) -> Unit)? { val taskId = taskInfo.taskId desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId) - performDesktopExitCleanupIfNeeded(taskId, displayId, wct) + performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) taskRepository.addClosingTask(displayId, taskId) taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding(displayId, taskId) @@ -619,8 +634,12 @@ class DesktopTasksController( ) val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null) wct.merge(requestRes.second, true) - pendingPipTransitionAndTask = - freeformTaskTransitionStarter.startPipTransition(wct) to taskInfo.taskId + freeformTaskTransitionStarter.startPipTransition(wct) + taskRepository.setTaskInPip(taskInfo.displayId, taskInfo.taskId, enterPip = true) + taskRepository.setOnPipAbortedCallback { displayId, taskId -> + minimizeTaskInner(shellTaskOrganizer.getRunningTaskInfo(taskId)!!) + taskRepository.setTaskInPip(displayId, taskId, enterPip = false) + } return } @@ -631,7 +650,7 @@ class DesktopTasksController( val taskId = taskInfo.taskId val displayId = taskInfo.displayId val wct = WindowContainerTransaction() - performDesktopExitCleanupIfNeeded(taskId, displayId, wct) + performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) // Notify immersive handler as it might need to exit immersive state. val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( @@ -893,7 +912,12 @@ class DesktopTasksController( } if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct) + performDesktopExitCleanupIfNeeded( + task.taskId, + task.displayId, + wct, + forceToFullscreen = false, + ) } transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) @@ -1344,7 +1368,7 @@ class DesktopTasksController( private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) { logV("addWallpaperActivity") - if (Flags.enableDesktopWallpaperActivityOnSystemUser()) { + if (Flags.enableDesktopWallpaperActivityForSystemUser()) { val intent = Intent(context, DesktopWallpaperActivity::class.java) val options = ActivityOptions.makeBasic().apply { @@ -1393,7 +1417,7 @@ class DesktopTasksController( private fun removeWallpaperActivity(wct: WindowContainerTransaction) { desktopWallpaperActivityTokenProvider.getToken()?.let { token -> logV("removeWallpaperActivity") - if (Flags.enableDesktopWallpaperActivityOnSystemUser()) { + if (Flags.enableDesktopWallpaperActivityForSystemUser()) { wct.reorder(token, /* onTop= */ false) } else { wct.removeTask(token) @@ -1409,7 +1433,9 @@ class DesktopTasksController( taskId: Int, displayId: Int, wct: WindowContainerTransaction, + forceToFullscreen: Boolean, ) { + taskRepository.setPipShouldKeepDesktopActive(displayId, !forceToFullscreen) if (Flags.enablePerDisplayDesktopWallpaperActivity()) { if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) { return @@ -1417,6 +1443,12 @@ class DesktopTasksController( if (displayId != DEFAULT_DISPLAY) { return } + } else if ( + Flags.enableDesktopWindowingPip() && + taskRepository.isMinimizedPipPresentInDisplay(displayId) && + !forceToFullscreen + ) { + return } else { if (!taskRepository.isOnlyVisibleNonClosingTask(taskId)) { return @@ -1457,21 +1489,6 @@ class DesktopTasksController( return false } - override fun onTransitionConsumed( - transition: IBinder, - aborted: Boolean, - finishT: Transaction?, - ) { - pendingPipTransitionAndTask?.let { (pipTransition, taskId) -> - if (transition == pipTransition) { - if (aborted) { - shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { minimizeTaskInner(it) } - } - pendingPipTransitionAndTask = null - } - } - } - override fun handleRequest( transition: IBinder, request: TransitionRequestInfo, @@ -1921,7 +1938,12 @@ class DesktopTasksController( if (!isDesktopModeShowing(task.displayId)) return null val wct = WindowContainerTransaction() - performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct) + performDesktopExitCleanupIfNeeded( + task.taskId, + task.displayId, + wct, + forceToFullscreen = false, + ) if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { taskRepository.addClosingTask(task.displayId, task.taskId) @@ -2048,7 +2070,12 @@ class DesktopTasksController( wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } - performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct) + performDesktopExitCleanupIfNeeded( + taskInfo.taskId, + taskInfo.displayId, + wct, + forceToFullscreen = true, + ) } private fun cascadeWindow(bounds: Rect, displayLayout: DisplayLayout, displayId: Int) { @@ -2082,7 +2109,12 @@ class DesktopTasksController( // want it overridden in multi-window. wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) - performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct) + performDesktopExitCleanupIfNeeded( + taskInfo.taskId, + taskInfo.displayId, + wct, + forceToFullscreen = false, + ) } /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index e7a00776360e..d61ffdaf5cf8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -21,9 +21,11 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.os.IBinder import android.view.SurfaceControl -import android.view.WindowManager import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_BACK +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.DesktopModeFlags import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY import android.window.TransitionInfo @@ -39,6 +41,8 @@ import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP +import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP /** * A [Transitions.TransitionObserver] that observes shell transitions and updates the @@ -57,6 +61,8 @@ class DesktopTasksTransitionObserver( ) : Transitions.TransitionObserver { private var transitionToCloseWallpaper: IBinder? = null + /* Pending PiP transition and its associated display id and task id. */ + private var pendingPipTransitionAndPipTask: Triple<IBinder, Int, Int>? = null private var currentProfileId: Int init { @@ -90,6 +96,33 @@ class DesktopTasksTransitionObserver( removeTaskIfNeeded(info) } removeWallpaperOnLastTaskClosingIfNeeded(transition, info) + + val desktopRepository = desktopUserRepositories.getProfile(currentProfileId) + info.changes.forEach { change -> + change.taskInfo?.let { taskInfo -> + if ( + Flags.enableDesktopWindowingPip() && + desktopRepository.isTaskMinimizedPipInDisplay( + taskInfo.displayId, + taskInfo.taskId, + ) + ) { + when (info.type) { + TRANSIT_PIP -> + pendingPipTransitionAndPipTask = + Triple(transition, taskInfo.displayId, taskInfo.taskId) + + TRANSIT_EXIT_PIP, + TRANSIT_REMOVE_PIP -> + desktopRepository.setTaskInPip( + taskInfo.displayId, + taskInfo.taskId, + enterPip = false, + ) + } + } + } + } } private fun removeTaskIfNeeded(info: TransitionInfo) { @@ -236,7 +269,7 @@ class DesktopTasksTransitionObserver( if (transitionToCloseWallpaper == transition) { // TODO: b/362469671 - Handle merging the animation when desktop is also closing. desktopWallpaperActivityTokenProvider.getToken()?.let { wallpaperActivityToken -> - if (Flags.enableDesktopWallpaperActivityOnSystemUser()) { + if (Flags.enableDesktopWallpaperActivityForSystemUser()) { transitions.startTransition( TRANSIT_TO_BACK, WindowContainerTransaction() @@ -252,6 +285,18 @@ class DesktopTasksTransitionObserver( } } transitionToCloseWallpaper = null + } else if (pendingPipTransitionAndPipTask?.first == transition) { + val desktopRepository = desktopUserRepositories.getProfile(currentProfileId) + if (aborted) { + pendingPipTransitionAndPipTask?.let { + desktopRepository.onPipAborted( + /*displayId=*/ it.second, + /* taskId=*/ it.third, + ) + } + } + desktopRepository.setOnPipAbortedCallback(null) + pendingPipTransitionAndPipTask = null } } @@ -263,11 +308,15 @@ class DesktopTasksTransitionObserver( change.taskInfo?.let { taskInfo -> if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) { when (change.mode) { - WindowManager.TRANSIT_OPEN -> { + TRANSIT_OPEN -> { desktopWallpaperActivityTokenProvider.setToken( taskInfo.token, taskInfo.displayId, ) + desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible( + isVisible = true, + taskInfo.displayId, + ) // After the task for the wallpaper is created, set it non-trimmable. // This is important to prevent recents from trimming and removing the // task. @@ -278,6 +327,16 @@ class DesktopTasksTransitionObserver( } TRANSIT_CLOSE -> desktopWallpaperActivityTokenProvider.removeToken(taskInfo.displayId) + TRANSIT_TO_FRONT -> + desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible( + isVisible = true, + taskInfo.displayId, + ) + TRANSIT_TO_BACK -> + desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible( + isVisible = false, + taskInfo.displayId, + ) else -> {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt index a87004c07d43..2bd7a9873a5e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.desktopmode.desktopwallpaperactivity import android.util.SparseArray +import android.util.SparseBooleanArray import android.view.Display.DEFAULT_DISPLAY import android.window.WindowContainerToken @@ -24,6 +25,7 @@ import android.window.WindowContainerToken class DesktopWallpaperActivityTokenProvider { private val wallpaperActivityTokenByDisplayId = SparseArray<WindowContainerToken>() + private val wallpaperActivityVisByDisplayId = SparseBooleanArray() fun setToken(token: WindowContainerToken, displayId: Int = DEFAULT_DISPLAY) { wallpaperActivityTokenByDisplayId[displayId] = token @@ -36,4 +38,16 @@ class DesktopWallpaperActivityTokenProvider { fun removeToken(displayId: Int = DEFAULT_DISPLAY) { wallpaperActivityTokenByDisplayId.delete(displayId) } + + fun setWallpaperActivityIsVisible( + isVisible: Boolean = false, + displayId: Int = DEFAULT_DISPLAY, + ) { + wallpaperActivityVisByDisplayId.put(displayId, isVisible) + } + + fun isWallpaperActivityVisible(displayId: Int = DEFAULT_DISPLAY): Boolean { + return wallpaperActivityTokenByDisplayId[displayId] != null && + wallpaperActivityVisByDisplayId.get(displayId, false) + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index 491b577386d7..e24b2c5f0134 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -332,7 +332,9 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll dragSession = new DragSession(ActivityTaskManager.getInstance(), mDisplayController.getDisplayLayout(displayId), event.getClipData(), event.getDragFlags()); - dragSession.initialize(); + // Only update the running task for now to determine if we should defer to desktop to + // handle the drag + dragSession.updateRunningTask(); final ActivityManager.RunningTaskInfo taskInfo = dragSession.runningTaskInfo; // Desktop tasks will have their own drag handling. final boolean isDesktopDrag = taskInfo != null && taskInfo.isFreeform() @@ -340,7 +342,8 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll pd.isHandlingDrag = DragUtils.canHandleDrag(event) && !isDesktopDrag; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s flags=%s", - pd.isHandlingDrag, event.getClipData().getItemCount(), + pd.isHandlingDrag, + event.getClipData() != null ? event.getClipData().getItemCount() : -1, DragUtils.getMimeTypesConcatenated(description), DragUtils.dragFlagsToString(event.getDragFlags())); } @@ -355,6 +358,8 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll Slog.w(TAG, "Unexpected drag start during an active drag"); return false; } + // Only initialize the session after we've checked that we're handling the drag + dragSession.initialize(true /* skipUpdateRunningTask */); pd.dragSession = dragSession; pd.activeDragCount++; pd.dragLayout.prepare(pd.dragSession, mLogger.logStart(pd.dragSession)); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java index c4ff87d175a7..279452ee8b9b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java @@ -29,7 +29,6 @@ import android.content.ClipData; import android.content.ClipDescription; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.os.PersistableBundle; import androidx.annotation.Nullable; @@ -44,6 +43,7 @@ import java.util.List; */ public class DragSession { private final ActivityTaskManager mActivityTaskManager; + @Nullable private final ClipData mInitialDragData; private final int mInitialDragFlags; @@ -66,7 +66,7 @@ public class DragSession { @WindowConfiguration.ActivityType int runningTaskActType = ACTIVITY_TYPE_STANDARD; boolean dragItemSupportsSplitscreen; - int hideDragSourceTaskId = -1; + final int hideDragSourceTaskId; DragSession(ActivityTaskManager activityTaskManager, DisplayLayout dispLayout, ClipData data, int dragFlags) { @@ -83,7 +83,6 @@ public class DragSession { /** * Returns the clip description associated with the drag. - * @return */ ClipDescription getClipDescription() { return mInitialDragData.getDescription(); @@ -125,8 +124,10 @@ public class DragSession { /** * Updates the session data based on the current state of the system at the start of the drag. */ - void initialize() { - updateRunningTask(); + void initialize(boolean skipUpdateRunningTask) { + if (!skipUpdateRunningTask) { + updateRunningTask(); + } activityInfo = mInitialDragData.getItemAt(0).getActivityInfo(); // TODO: This should technically check & respect config_supportsNonResizableMultiWindow diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java index 248a1124cd86..4df9ae4b2ee3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java @@ -49,7 +49,7 @@ public class DragUtils { * Returns whether we can handle this particular drag. */ public static boolean canHandleDrag(DragEvent event) { - if (event.getClipData().getItemCount() <= 0) { + if (event.getClipData() == null || event.getClipData().getItemCount() <= 0) { // No clip data, ignore this drag return false; } @@ -107,8 +107,11 @@ public class DragUtils { /** * Returns a list of the mime types provided in the clip description. */ - public static String getMimeTypesConcatenated(ClipDescription description) { + public static String getMimeTypesConcatenated(@Nullable ClipDescription description) { String mimeTypes = ""; + if (description == null) { + return mimeTypes; + } for (int i = 0; i < description.getMimeTypeCount(); i++) { if (i > 0) { mimeTypes += ", "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index 8c6d5f5c6660..562b26014bf3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -59,6 +59,7 @@ import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -98,6 +99,7 @@ public class PipController implements ConfigurationChangeListener, private final PipTouchHandler mPipTouchHandler; private final PipAppOpsListener mPipAppOpsListener; private final PhonePipMenuController mPipMenuController; + private final PipUiEventLogger mPipUiEventLogger; private final ShellExecutor mMainExecutor; private final PipImpl mImpl; private final List<Consumer<Boolean>> mOnIsInPipStateChangedListeners = new ArrayList<>(); @@ -143,6 +145,7 @@ public class PipController implements ConfigurationChangeListener, PipTouchHandler pipTouchHandler, PipAppOpsListener pipAppOpsListener, PhonePipMenuController pipMenuController, + PipUiEventLogger pipUiEventLogger, ShellExecutor mainExecutor) { mContext = context; mShellCommandHandler = shellCommandHandler; @@ -160,6 +163,7 @@ public class PipController implements ConfigurationChangeListener, mPipTouchHandler = pipTouchHandler; mPipAppOpsListener = pipAppOpsListener; mPipMenuController = pipMenuController; + mPipUiEventLogger = pipUiEventLogger; mMainExecutor = mainExecutor; mImpl = new PipImpl(); @@ -187,6 +191,7 @@ public class PipController implements ConfigurationChangeListener, PipTouchHandler pipTouchHandler, PipAppOpsListener pipAppOpsListener, PhonePipMenuController pipMenuController, + PipUiEventLogger pipUiEventLogger, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -197,7 +202,7 @@ public class PipController implements ConfigurationChangeListener, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, pipTransitionState, pipTouchHandler, pipAppOpsListener, pipMenuController, - mainExecutor); + pipUiEventLogger, mainExecutor); } public PipImpl getPipImpl() { @@ -238,18 +243,6 @@ public class PipController implements ConfigurationChangeListener, }); mPipAppOpsListener.setCallback(mPipTouchHandler.getMotionHelper()); - mPipTransitionState.addPipTransitionStateChangedListener( - (oldState, newState, extra) -> { - if (newState == PipTransitionState.ENTERED_PIP) { - final TaskInfo taskInfo = mPipTransitionState.getPipTaskInfo(); - if (taskInfo != null && taskInfo.topActivity != null) { - mPipAppOpsListener.onActivityPinned( - taskInfo.topActivity.getPackageName()); - } - } else if (newState == PipTransitionState.EXITED_PIP) { - mPipAppOpsListener.onActivityUnpinned(); - } - }); } private ExternalInterfaceBinder createExternalInterface() { @@ -446,14 +439,25 @@ public class PipController implements ConfigurationChangeListener, mPipTransitionState.setSwipePipToHomeState(overlay, appBounds); break; case PipTransitionState.ENTERED_PIP: + final TaskInfo taskInfo = mPipTransitionState.getPipTaskInfo(); + if (taskInfo != null && taskInfo.topActivity != null) { + mPipAppOpsListener.onActivityPinned(taskInfo.topActivity.getPackageName()); + mPipUiEventLogger.setTaskInfo(taskInfo); + } if (mPipTransitionState.isInSwipePipToHomeTransition()) { + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_AUTO_ENTER); mPipTransitionState.resetSwipePipToHomeState(); + } else { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER); } for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) { listener.accept(true /* inPip */); } break; case PipTransitionState.EXITED_PIP: + mPipAppOpsListener.onActivityUnpinned(); + mPipUiEventLogger.setTaskInfo(null); for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) { listener.accept(false /* inPip */); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java index 37296531ee34..9babe9e9e4eb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -43,20 +43,20 @@ import com.android.wm.shell.R; import com.android.wm.shell.animation.FloatProperties; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.pip.PipAppOpsListener; -import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipPerfHintController; import com.android.wm.shell.common.pip.PipSnapAlgorithm; +import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.pip2.animation.PipResizeAnimator; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.PhysicsAnimator; import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; +import java.util.Optional; + import kotlin.Unit; import kotlin.jvm.functions.Function0; -import java.util.Optional; - /** * A helper to animate and manipulate the PiP. */ @@ -80,12 +80,12 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, private static final float DISMISS_CIRCLE_PERCENT = 0.85f; private final Context mContext; - private @NonNull PipBoundsState mPipBoundsState; - private @NonNull PipBoundsAlgorithm mPipBoundsAlgorithm; - private @NonNull PipScheduler mPipScheduler; - private @NonNull PipTransitionState mPipTransitionState; - private PhonePipMenuController mMenuController; - private PipSnapAlgorithm mSnapAlgorithm; + @NonNull private final PipBoundsState mPipBoundsState; + @NonNull private final PipScheduler mPipScheduler; + @NonNull private final PipTransitionState mPipTransitionState; + @NonNull private final PipUiEventLogger mPipUiEventLogger; + private final PhonePipMenuController mMenuController; + private final PipSnapAlgorithm mSnapAlgorithm; /** The region that all of PIP must stay within. */ private final Rect mFloatingAllowedArea = new Rect(); @@ -168,10 +168,9 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, PhonePipMenuController menuController, PipSnapAlgorithm snapAlgorithm, FloatingContentCoordinator floatingContentCoordinator, PipScheduler pipScheduler, Optional<PipPerfHintController> pipPerfHintControllerOptional, - PipBoundsAlgorithm pipBoundsAlgorithm, PipTransitionState pipTransitionState) { + PipTransitionState pipTransitionState, PipUiEventLogger pipUiEventLogger) { mContext = context; mPipBoundsState = pipBoundsState; - mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipScheduler = pipScheduler; mMenuController = menuController; mSnapAlgorithm = snapAlgorithm; @@ -185,6 +184,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, }; mPipTransitionState = pipTransitionState; mPipTransitionState.addPipTransitionStateChangedListener(this); + mPipUiEventLogger = pipUiEventLogger; } void init() { @@ -850,9 +850,11 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, if (mPipBoundsState.getBounds().left < 0 && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT) { mPipBoundsState.setStashed(STASH_TYPE_LEFT); + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_LEFT); } else if (mPipBoundsState.getBounds().left >= 0 && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) { mPipBoundsState.setStashed(STASH_TYPE_RIGHT); + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_RIGHT); } mMenuController.hideMenu(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 7f673d2efc68..ea8dac982703 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -40,6 +40,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; @@ -59,6 +60,8 @@ public class PipScheduler { private final ShellExecutor mMainExecutor; private final PipTransitionState mPipTransitionState; private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional; + private final Optional<DesktopWallpaperActivityTokenProvider> + mDesktopWallpaperActivityTokenProviderOptional; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private PipTransitionController mPipTransitionController; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory @@ -73,12 +76,16 @@ public class PipScheduler { ShellExecutor mainExecutor, PipTransitionState pipTransitionState, Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { mContext = context; mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; mPipTransitionState = pipTransitionState; mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional; + mDesktopWallpaperActivityTokenProviderOptional = + desktopWallpaperActivityTokenProviderOptional; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mSurfaceControlTransactionFactory = @@ -260,10 +267,18 @@ public class PipScheduler { /** Returns whether PiP is exiting while we're in desktop mode. */ private boolean isPipExitingToDesktopMode() { - return Flags.enableDesktopWindowingPip() && mDesktopUserRepositoriesOptional.isPresent() - && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount( - Objects.requireNonNull(mPipTransitionState.getPipTaskInfo()).displayId) > 0 - || isDisplayInFreeform()); + // Early return if PiP in Desktop Windowing is not supported. + if (!Flags.enableDesktopWindowingPip() || mDesktopUserRepositoriesOptional.isEmpty() + || mDesktopWallpaperActivityTokenProviderOptional.isEmpty()) { + return false; + } + final int displayId = Objects.requireNonNull( + mPipTransitionState.getPipTaskInfo()).displayId; + return mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(displayId) + > 0 + || mDesktopWallpaperActivityTokenProviderOptional.get().isWallpaperActivityVisible( + displayId) + || isDisplayInFreeform(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 8061ee9090b6..38015ca6d45f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -63,7 +63,9 @@ import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.split.SplitScreenUtils; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterAnimator; @@ -110,6 +112,8 @@ public class PipTransition extends PipTransitionController implements private final PipTransitionState mPipTransitionState; private final PipDisplayLayoutState mPipDisplayLayoutState; private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional; + private final Optional<DesktopWallpaperActivityTokenProvider> + mDesktopWallpaperActivityTokenProviderOptional; // // Transition caches @@ -145,7 +149,9 @@ public class PipTransition extends PipTransitionController implements PipTransitionState pipTransitionState, PipDisplayLayoutState pipDisplayLayoutState, PipUiStateChangeController pipUiStateChangeController, - Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) { + Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); @@ -157,6 +163,8 @@ public class PipTransition extends PipTransitionController implements mPipTransitionState.addPipTransitionStateChangedListener(this); mPipDisplayLayoutState = pipDisplayLayoutState; mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional; + mDesktopWallpaperActivityTokenProviderOptional = + desktopWallpaperActivityTokenProviderOptional; } @Override @@ -826,13 +834,14 @@ public class PipTransition extends PipTransitionController implements return false; } - // Since opening a new task while in Desktop Mode always first open in Fullscreen // until DesktopMode Shell code resolves it to Freeform, PipTransition will get a // possibility to handle it also. In this case return false to not have it enter PiP. final boolean isInDesktopSession = !mDesktopUserRepositoriesOptional.isEmpty() - && mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount( - pipTask.displayId) > 0; + && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount( + pipTask.displayId) > 0 + || mDesktopUserRepositoriesOptional.get().getCurrent() + .isMinimizedPipPresentInDisplay(pipTask.displayId)); if (isInDesktopSession) { return false; } @@ -968,6 +977,27 @@ public class PipTransition extends PipTransitionController implements "Unexpected bundle for " + mPipTransitionState); break; case PipTransitionState.EXITED_PIP: + final TaskInfo pipTask = mPipTransitionState.getPipTaskInfo(); + final boolean desktopPipEnabled = Flags.enableDesktopWindowingPip() + && mDesktopUserRepositoriesOptional.isPresent() + && mDesktopWallpaperActivityTokenProviderOptional.isPresent(); + if (desktopPipEnabled && pipTask != null) { + final DesktopRepository desktopRepository = + mDesktopUserRepositoriesOptional.get().getCurrent(); + final boolean wallpaperIsVisible = + mDesktopWallpaperActivityTokenProviderOptional.get() + .isWallpaperActivityVisible(pipTask.displayId); + if (desktopRepository.getVisibleTaskCount(pipTask.displayId) == 0 + && wallpaperIsVisible) { + mTransitions.startTransition( + TRANSIT_TO_BACK, + new WindowContainerTransaction().reorder( + mDesktopWallpaperActivityTokenProviderOptional.get() + .getToken(pipTask.displayId), /* onTop= */ false), + null + ); + } + } mPipTransitionState.setPinnedTaskLeash(null); mPipTransitionState.setPipTaskInfo(null); break; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl index 964e5fd62a5f..af1679f2d175 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl @@ -36,12 +36,6 @@ import com.android.internal.os.IResultReceiver; interface IRecentsAnimationController { /** - * Takes a screenshot of the task associated with the given {@param taskId}. Only valid for the - * current set of task ids provided to the handler. - */ - TaskSnapshot screenshotTask(int taskId); - - /** * Sets the final surface transaction on a Task. This is used by Launcher to notify the system * that animating Activity to PiP has completed and the associated task surface should be * updated accordingly. This should be called before `finish` diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl index 32c79a2d02de..8cdb8c4512a9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl @@ -17,9 +17,10 @@ package com.android.wm.shell.recents; import android.graphics.Rect; +import android.os.Bundle; import android.view.RemoteAnimationTarget; import android.window.TaskSnapshot; -import android.os.Bundle; +import android.window.TransitionInfo; import com.android.wm.shell.recents.IRecentsAnimationController; @@ -57,7 +58,8 @@ oneway interface IRecentsAnimationRunner { */ void onAnimationStart(in IRecentsAnimationController controller, in RemoteAnimationTarget[] apps, in RemoteAnimationTarget[] wallpapers, - in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras) = 2; + in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras, + in TransitionInfo info) = 2; /** * Called when the task of an activity that has been started while the recents animation diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 032dac9ff3a2..aeccd86e122c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -411,10 +411,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mInstanceId = System.identityHashCode(this); mListener = listener; mDeathHandler = () -> { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, - "[%d] RecentsController.DeathRecipient: binder died", mInstanceId); - finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */, - "deathRecipient"); + mExecutor.execute(() -> { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.DeathRecipient: binder died", mInstanceId); + finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */, + "deathRecipient"); + }); }; try { mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */); @@ -585,7 +587,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mListener.onAnimationStart(this, apps.toArray(new RemoteAnimationTarget[apps.size()]), new RemoteAnimationTarget[0], - new Rect(0, 0, 0, 0), new Rect(), new Bundle()); + new Rect(0, 0, 0, 0), new Rect(), new Bundle(), + null); for (int i = 0; i < mStateListeners.size(); i++) { mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING); } @@ -816,7 +819,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mListener.onAnimationStart(this, apps.toArray(new RemoteAnimationTarget[apps.size()]), wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]), - new Rect(0, 0, 0, 0), new Rect(), b); + new Rect(0, 0, 0, 0), new Rect(), b, info); for (int i = 0; i < mStateListeners.size(); i++) { mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING); } @@ -1227,19 +1230,6 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } @Override - public TaskSnapshot screenshotTask(int taskId) { - try { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, - "[%d] RecentsController.screenshotTask: taskId=%d", mInstanceId, taskId); - return ActivityTaskManager.getService().takeTaskSnapshot(taskId, - true /* updateCache */); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to screenshot task", e); - } - return null; - } - - @Override public void setInputConsumerEnabled(boolean enabled) { mExecutor.execute(() -> { if (mFinishCB == null || !enabled) { @@ -1286,6 +1276,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "requested")); } + /** + * @param runnerFinishCb The remote finish callback to run after finish is complete, this is + * not the same as mFinishCb which reports the transition is finished + * to WM. + */ private void finishInner(boolean toHome, boolean sendUserLeaveHint, IResultReceiver runnerFinishCb, String reason) { if (finishSyntheticTransition(runnerFinishCb, reason)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 90c59176b991..b6bd879c75eb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2166,7 +2166,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.setForceTranslucent(mRootTaskInfo.token, translucent); } - /** Callback when split roots visiblility changed. */ + /** Callback when split roots visiblility changed. + * NOTICE: This only be called on legacy transition. */ @Override public void onStageVisibilityChanged(StageTaskListener stageListener) { // If split didn't active, just ignore this callback because we should already did these @@ -2973,9 +2974,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final int transitType = info.getType(); TransitionInfo.Change pipChange = null; int closingSplitTaskId = -1; - // This array tracks if we are sending stages TO_BACK in this transition. - // TODO (b/349828130): Update for n apps - boolean[] stagesSentToBack = new boolean[2]; + // This array tracks where we are sending stages (TO_BACK/TO_FRONT) in this transition. + // TODO (b/349828130): Update for n apps (needs to handle different indices than 0/1). + // Also make sure having multiple changes per stage (2+ tasks in one stage) is being + // handled properly. + int[] stageChanges = new int[2]; for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); @@ -3039,18 +3042,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " with " + taskId + " before startAnimation()."); } } - if (isClosingType(change.getMode()) && - getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED) { - - // Record which stages are getting sent to back - if (change.getMode() == TRANSIT_TO_BACK) { - stagesSentToBack[getStageOfTask(taskId)] = true; - } + final int stageOfTaskId = getStageOfTask(taskId); + if (stageOfTaskId == STAGE_TYPE_UNDEFINED) { + continue; + } + if (isClosingType(change.getMode())) { // (For PiP transitions) If either one of the 2 stages is closing we're assuming // we'll break split closingSplitTaskId = taskId; } + if (transitType == WindowManager.TRANSIT_WAKE) { + // Record which stages are receiving which changes + if ((change.getMode() == TRANSIT_TO_BACK + || change.getMode() == TRANSIT_TO_FRONT) + && (stageOfTaskId == STAGE_TYPE_MAIN + || stageOfTaskId == STAGE_TYPE_SIDE)) { + stageChanges[stageOfTaskId] = change.getMode(); + } + } } if (pipChange != null) { @@ -3075,19 +3085,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return true; } - // If keyguard is active, check to see if we have our TO_BACK transitions in order. - // This array should either be all false (no split stages sent to back) or all true - // (all stages sent to back). In any other case (which can happen with SHOW_ABOVE_LOCKED - // apps) we should break split. - if (mKeyguardActive) { - boolean isFirstStageSentToBack = stagesSentToBack[0]; - for (boolean b : stagesSentToBack) { - // Compare each boolean to the first one. If any are different, break split. - if (b != isFirstStageSentToBack) { - dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); - break; - } - } + // If keyguard is active, check to see if we have all our stages showing. If one stage + // was moved but not the other (which can happen with SHOW_ABOVE_LOCKED apps), we should + // break split. + if (mKeyguardActive && stageChanges[STAGE_TYPE_MAIN] != stageChanges[STAGE_TYPE_SIDE]) { + dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); } final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index bfe74122c5c2..021f6595d984 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -240,20 +240,12 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { @Override @CallSuper public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, - "onTaskInfoChanged: taskId=%d vis=%b reqVis=%b baseAct=%s stageId=%s", - taskInfo.taskId, taskInfo.isVisible, taskInfo.isVisibleRequested, - taskInfo.baseActivity, stageTypeToString(mId)); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: taskId=%d taskAct=%s " + + "stageId=%s", + taskInfo.taskId, taskInfo.baseActivity, stageTypeToString(mId)); mWindowDecorViewModel.ifPresent(viewModel -> viewModel.onTaskInfoChanged(taskInfo)); if (mRootTaskInfo.taskId == taskInfo.taskId) { mRootTaskInfo = taskInfo; - boolean isVisible = taskInfo.isVisible && taskInfo.isVisibleRequested; - if (mVisible != isVisible) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: currentVis=%b newVis=%b", - mVisible, isVisible); - mVisible = isVisible; - mCallbacks.onStageVisibilityChanged(this); - } } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { if (!taskInfo.supportsMultiWindow || !ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType()) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java index d8884f6d8d38..f5aaaad93229 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java @@ -33,6 +33,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.TransactionPool; import java.util.ArrayList; +import java.util.function.Consumer; public class DefaultSurfaceAnimator { @@ -58,42 +59,12 @@ public class DefaultSurfaceAnimator { // Animation length is already expected to be scaled. va.overrideDurationScale(1.0f); va.setDuration(anim.computeDurationHint()); - va.addUpdateListener(updateListener); - va.addListener(new AnimatorListenerAdapter() { - // It is possible for the end/cancel to be called more than once, which may cause - // issues if the animating surface has already been released. Track the finished - // state here to skip duplicate callbacks. See b/252872225. - private boolean mFinished; - - @Override - public void onAnimationEnd(Animator animation) { - onFinish(); - } - - @Override - public void onAnimationCancel(Animator animation) { - onFinish(); - } - - private void onFinish() { - if (mFinished) return; - mFinished = true; - // Apply transformation of end state in case the animation is canceled. - if (va.getAnimatedFraction() < 1f) { - va.setCurrentFraction(1f); - } - - pool.release(transaction); - mainExecutor.execute(() -> { - animations.remove(va); - finishCallback.run(); - }); - // The update listener can continue to be called after the animation has ended if - // end() is called manually again before the finisher removes the animation. - // Remove it manually here to prevent animating a released surface. - // See b/252872225. - va.removeUpdateListener(updateListener); - } + setupValueAnimator(va, updateListener, (vanim) -> { + pool.release(transaction); + mainExecutor.execute(() -> { + animations.remove(vanim); + finishCallback.run(); + }); }); animations.add(va); } @@ -188,4 +159,50 @@ public class DefaultSurfaceAnimator { } } } + + /** + * Setup some callback logic on a value-animator. This helper ensures that a value animator + * finishes at its final fraction (1f) and that relevant callbacks are only called once. + */ + public static ValueAnimator setupValueAnimator(ValueAnimator animator, + ValueAnimator.AnimatorUpdateListener updateListener, + Consumer<ValueAnimator> afterFinish) { + animator.addUpdateListener(updateListener); + animator.addListener(new AnimatorListenerAdapter() { + // It is possible for the end/cancel to be called more than once, which may cause + // issues if the animating surface has already been released. Track the finished + // state here to skip duplicate callbacks. See b/252872225. + private boolean mFinished; + + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + onFinish(); + } + + @Override + public void onAnimationCancel(Animator animation) { + onFinish(); + } + + private void onFinish() { + if (mFinished) return; + mFinished = true; + // Apply transformation of end state in case the animation is canceled. + if (animator.getAnimatedFraction() < 1f) { + animator.setCurrentFraction(1f); + } + afterFinish.accept(animator); + // The update listener can continue to be called after the animation has ended if + // end() is called manually again before the finisher removes the animation. + // Remove it manually here to prevent animating a released surface. + // See b/252872225. + animator.removeUpdateListener(updateListener); + } + }); + return animator; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 1689bb5778ae..36c3e9711f5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -55,6 +55,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; +import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; @@ -69,6 +70,7 @@ import static com.android.wm.shell.transition.TransitionAnimationHelper.isCovere import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; import android.animation.Animator; +import android.animation.ValueAnimator; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; @@ -104,6 +106,7 @@ import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.ProtoLog; import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.animation.SizeChangeAnimation; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; @@ -422,6 +425,14 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { ROTATION_ANIMATION_ROTATE, 0 /* flags */, animations, onAnimFinish); continue; } + + if (Flags.portWindowSizeAnimation() && isTask + && TransitionInfo.isIndependent(change, info) + && change.getSnapshot() != null) { + startBoundsChangeAnimation(startTransaction, animations, change, onAnimFinish, + mMainExecutor); + continue; + } } // Hide the invisible surface directly without animating it if there is a display @@ -734,6 +745,21 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } } + private void startBoundsChangeAnimation(@NonNull SurfaceControl.Transaction startT, + @NonNull ArrayList<Animator> animations, @NonNull TransitionInfo.Change change, + @NonNull Runnable finishCb, @NonNull ShellExecutor mainExecutor) { + final SizeChangeAnimation sca = + new SizeChangeAnimation(change.getStartAbsBounds(), change.getEndAbsBounds()); + sca.initialize(change.getLeash(), change.getSnapshot(), startT); + final ValueAnimator va = sca.buildAnimator(change.getLeash(), change.getSnapshot(), + (animator) -> mainExecutor.execute(() -> { + animations.remove(animator); + finishCb.run(); + })); + va.setDuration(DEFAULT_APP_TRANSITION_DURATION); + animations.add(va); + } + @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 9fbda46bd2b7..429e0564dd2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -126,6 +126,8 @@ import com.android.wm.shell.desktopmode.common.ToggleTaskSizeUtilsKt; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppToWebEducationController; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.recents.RecentsTransitionHandler; +import com.android.wm.shell.recents.RecentsTransitionStateListener; import com.android.wm.shell.shared.FocusTransitionListener; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; @@ -157,8 +159,10 @@ import kotlinx.coroutines.MainCoroutineDispatcher; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; /** @@ -247,6 +251,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final DesktopModeEventLogger mDesktopModeEventLogger; private final DesktopModeUiEventLogger mDesktopModeUiEventLogger; private final WindowDecorTaskResourceLoader mTaskResourceLoader; + private final RecentsTransitionHandler mRecentsTransitionHandler; public DesktopModeWindowDecorViewModel( Context context, @@ -282,7 +287,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - WindowDecorTaskResourceLoader taskResourceLoader) { + WindowDecorTaskResourceLoader taskResourceLoader, + RecentsTransitionHandler recentsTransitionHandler) { this( context, shellExecutor, @@ -323,7 +329,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader); + taskResourceLoader, + recentsTransitionHandler); } @VisibleForTesting @@ -367,7 +374,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - WindowDecorTaskResourceLoader taskResourceLoader) { + WindowDecorTaskResourceLoader taskResourceLoader, + RecentsTransitionHandler recentsTransitionHandler) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -436,6 +444,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopModeEventLogger = desktopModeEventLogger; mDesktopModeUiEventLogger = desktopModeUiEventLogger; mTaskResourceLoader = taskResourceLoader; + mRecentsTransitionHandler = recentsTransitionHandler; shellInit.addInitCallback(this::onInit, this); } @@ -450,6 +459,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, new DesktopModeOnTaskResizeAnimationListener()); mDesktopTasksController.setOnTaskRepositionAnimationListener( new DesktopModeOnTaskRepositionAnimationListener()); + if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) { + mRecentsTransitionHandler.addTransitionStateListener( + new DesktopModeRecentsTransitionStateListener()); + } mDisplayController.addDisplayChangingController(mOnDisplayChangingListener); try { mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener, @@ -1859,6 +1872,38 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } } + private class DesktopModeRecentsTransitionStateListener + implements RecentsTransitionStateListener { + final Set<Integer> mAnimatingTaskIds = new HashSet<>(); + + @Override + public void onTransitionStateChanged(int state) { + switch (state) { + case RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED: + for (int n = 0; n < mWindowDecorByTaskId.size(); n++) { + int taskId = mWindowDecorByTaskId.keyAt(n); + mAnimatingTaskIds.add(taskId); + setIsRecentsTransitionRunningForTask(taskId, true); + } + return; + case RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING: + // No Recents transition running - clean up window decorations + for (int taskId : mAnimatingTaskIds) { + setIsRecentsTransitionRunningForTask(taskId, false); + } + mAnimatingTaskIds.clear(); + return; + default: + } + } + + private void setIsRecentsTransitionRunningForTask(int taskId, boolean isRecentsRunning) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) return; + decoration.setIsRecentsTransitionRunning(isRecentsRunning); + } + } + private class DragEventListenerImpl implements DragPositioningCallbackUtility.DragEventListener { @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 4ac89546c9c7..39a989ce7c7f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -204,6 +204,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final MultiInstanceHelper mMultiInstanceHelper; private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final DesktopUserRepositories mDesktopUserRepositories; + private boolean mIsRecentsTransitionRunning = false; private Runnable mLoadAppInfoRunnable; private Runnable mSetAppInfoRunnable; @@ -498,7 +499,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, - displayExclusionRegion); + displayExclusionRegion, mIsRecentsTransitionRunning); final WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -869,7 +870,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin boolean inFullImmersiveMode, @NonNull InsetsState displayInsetsState, boolean hasGlobalFocus, - @NonNull Region displayExclusionRegion) { + @NonNull Region displayExclusionRegion, + boolean shouldIgnoreCornerRadius) { final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); final boolean isAppHeader = captionLayoutId == R.layout.desktop_mode_app_header; @@ -1006,13 +1008,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mWindowDecorConfig = windowDecorConfig; if (DesktopModeStatus.useRoundedCorners()) { - relayoutParams.mCornerRadius = taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM - ? loadDimensionPixelSize(context.getResources(), - R.dimen.desktop_windowing_freeform_rounded_corner_radius) - : INVALID_CORNER_RADIUS; + relayoutParams.mCornerRadius = shouldIgnoreCornerRadius ? INVALID_CORNER_RADIUS : + getCornerRadius(context, relayoutParams.mLayoutResId); } } + private static int getCornerRadius(@NonNull Context context, int layoutResId) { + if (layoutResId == R.layout.desktop_mode_app_header) { + return loadDimensionPixelSize(context.getResources(), + R.dimen.desktop_windowing_freeform_rounded_corner_radius); + } + return INVALID_CORNER_RADIUS; + } + /** * If task has focused window decor, return the caption id of the fullscreen caption size * resource. Otherwise, return ID_NULL and caption width be set to task width. @@ -1740,6 +1748,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } /** + * Declares whether a Recents transition is currently active. + * + * <p> When a Recents transition is active we allow that transition to take ownership of the + * corner radius of its task surfaces, so each window decoration should stop updating the corner + * radius of its task surface during that time. + */ + void setIsRecentsTransitionRunning(boolean isRecentsTransitionRunning) { + mIsRecentsTransitionRunning = isRecentsTransitionRunning; + } + + /** * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button. */ void onMaximizeButtonHoverExit() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 5d1bedb85b5e..fa7183ad0fd8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -967,4 +967,4 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects), mFlags); } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index 3f65d9318692..1264c013faf5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -231,6 +231,7 @@ internal class AppHandleViewHolder( fun disposeStatusBarInputLayer() { if (!statusBarInputLayerExists) return statusBarInputLayerExists = false + statusBarInputLayer?.view?.setOnTouchListener(null) handler.post { statusBarInputLayer?.releaseView() statusBarInputLayer = null 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 636549fa0662..a6f8150ffc55 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 @@ -176,7 +176,6 @@ open class EnterPipToOtherOrientation(flicker: LegacyFlickerTest) : PipTransitio * transition */ @Ignore("TODO(b/356277166): enable the tablet test") - @Postsubmit @Test open fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() { assumeTrue(tapl.isTablet) diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt index 4987ab7b9344..d65f158e00d6 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt @@ -78,7 +78,6 @@ class BottomHalfEnterPipToOtherOrientation(flicker: LegacyFlickerTest) : } @Ignore("TODO(b/356277166): enable the tablet test") - @Presubmit @Test override fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() { // Test app and pip app should covers the entire screen on start. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java new file mode 100644 index 000000000000..e91a1238a390 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.testing.AndroidTestingRunner; +import android.view.Display; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class AppZoomOutControllerTest extends ShellTestCase { + + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Mock private DisplayController mDisplayController; + @Mock private AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer; + @Mock private ShellExecutor mExecutor; + @Mock private ActivityManager.RunningTaskInfo mRunningTaskInfo; + + private AppZoomOutController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Display display = mContext.getDisplay(); + DisplayLayout displayLayout = new DisplayLayout(mContext, display); + when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(displayLayout); + + ShellInit shellInit = spy(new ShellInit(mExecutor)); + mController = spy(new AppZoomOutController(mContext, shellInit, mTaskOrganizer, + mDisplayController, mDisplayAreaOrganizer, mExecutor)); + } + + @Test + public void isHomeTaskFocused_zoomOutForHome() { + mRunningTaskInfo.isFocused = true; + when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME); + mController.onFocusTaskChanged(mRunningTaskInfo); + + verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(true); + } + + @Test + public void isHomeTaskNotFocused_zoomOutForApp() { + mRunningTaskInfo.isFocused = false; + when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME); + mController.onFocusTaskChanged(mRunningTaskInfo); + + verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(false); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 47ee7bb20199..bbdb90f0a37c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -61,9 +61,7 @@ import android.os.RemoteCallback; import android.os.RemoteException; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; -import android.provider.Settings; import android.testing.AndroidTestingRunner; -import android.testing.TestableContentResolver; import android.testing.TestableLooper; import android.view.IRemoteAnimationRunner; import android.view.KeyEvent; @@ -84,7 +82,6 @@ import android.window.WindowContainerToken; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; -import com.android.internal.util.test.FakeSettingsProvider; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; @@ -109,7 +106,6 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) public class BackAnimationControllerTest extends ShellTestCase { - private static final String ANIMATION_ENABLED = "1"; private final TestShellExecutor mShellExecutor = new TestShellExecutor(); private ShellInit mShellInit; @@ -148,8 +144,6 @@ public class BackAnimationControllerTest extends ShellTestCase { private Transitions.TransitionHandler mTakeoverHandler; private BackAnimationController mController; - private TestableContentResolver mContentResolver; - private TestableLooper mTestableLooper; private DefaultCrossActivityBackAnimation mDefaultCrossActivityBackAnimation; private CrossTaskBackAnimation mCrossTaskBackAnimation; @@ -166,11 +160,6 @@ public class BackAnimationControllerTest extends ShellTestCase { MockitoAnnotations.initMocks(this); mContext.addMockSystemService(InputManager.class, mInputManager); mContext.getApplicationInfo().privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED; - mContentResolver = new TestableContentResolver(mContext); - mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); - Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, - ANIMATION_ENABLED); - mTestableLooper = TestableLooper.get(this); mShellInit = spy(new ShellInit(mShellExecutor)); mDefaultCrossActivityBackAnimation = new DefaultCrossActivityBackAnimation(mContext, mAnimationBackground, mRootTaskDisplayAreaOrganizer, mHandler); @@ -187,10 +176,8 @@ public class BackAnimationControllerTest extends ShellTestCase { mShellInit, mShellController, mShellExecutor, - new Handler(mTestableLooper.getLooper()), mActivityTaskManager, mContext, - mContentResolver, mAnimationBackground, mShellBackAnimationRegistry, mShellCommandHandler, @@ -342,47 +329,6 @@ public class BackAnimationControllerTest extends ShellTestCase { } @Test - public void animationDisabledFromSettings() throws RemoteException { - // Toggle the setting off - Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0"); - ShellInit shellInit = new ShellInit(mShellExecutor); - mController = - new BackAnimationController( - shellInit, - mShellController, - mShellExecutor, - new Handler(mTestableLooper.getLooper()), - mActivityTaskManager, - mContext, - mContentResolver, - mAnimationBackground, - mShellBackAnimationRegistry, - mShellCommandHandler, - mTransitions, - mHandler); - shellInit.init(); - registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); - - ArgumentCaptor<BackMotionEvent> backEventCaptor = - ArgumentCaptor.forClass(BackMotionEvent.class); - - createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, - /* enableAnimation = */ false, - /* isAnimationCallback = */ false); - - triggerBackGesture(); - releaseBackGesture(); - - verify(mAppCallback, times(1)).onBackInvoked(); - - verify(mAnimatorCallback, never()).onBackStarted(any()); - verify(mAnimatorCallback, never()).onBackProgressed(backEventCaptor.capture()); - verify(mAnimatorCallback, never()).onBackInvoked(); - verify(mBackAnimationRunner, never()).onAnimationStart( - anyInt(), any(), any(), any(), any()); - } - - @Test public void gestureQueued_WhenPreviousTransitionHasNotYetEnded() throws RemoteException { registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index 784e1907894d..b5c9fa151dac 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -62,11 +62,12 @@ import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; import dagger.Lazy; +import java.util.Optional; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -76,8 +77,6 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.Optional; - /** * Tests for {@link CompatUIController}. * @@ -128,8 +127,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { private DesktopUserRepositories mDesktopUserRepositories; @Mock private DesktopRepository mDesktopRepository; - @Mock - private FocusTransitionObserver mFocusTransitionObserver; @Captor ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; @@ -165,8 +162,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { mMockDisplayController, mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader, mCompatUIConfiguration, mCompatUIShellCommandHandler, mAccessibilityManager, - mCompatUIStatusManager, Optional.of(mDesktopUserRepositories), - mFocusTransitionObserver) { + mCompatUIStatusManager, Optional.of(mDesktopUserRepositories)) { @Override CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { @@ -284,7 +280,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { doReturn(false).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean()); TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); @@ -416,7 +411,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Verify button remains hidden while IME is showing. TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ false); @@ -449,7 +443,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Verify button remains hidden while keyguard is showing. TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ false); @@ -530,7 +523,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testRestartLayoutRecreatedIfNeeded() { final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); doReturn(true).when(mMockRestartDialogLayout) .needsToBeRecreated(any(TaskInfo.class), any(ShellTaskOrganizer.TaskListener.class)); @@ -546,7 +538,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testRestartLayoutNotRecreatedIfNotNeeded() { final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); doReturn(false).when(mMockRestartDialogLayout) .needsToBeRecreated(any(TaskInfo.class), any(ShellTaskOrganizer.TaskListener.class)); @@ -567,8 +558,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -584,8 +574,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_newTask_notVisibleOrFocused_notUpdated() { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate task being shown mController.updateActiveTaskInfo(taskInfo); @@ -603,8 +592,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create visible but NOT focused task final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); + /* isVisible */ true, /* isFocused */ false); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo1); @@ -616,8 +604,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create focused but NOT visible task final TaskInfo taskInfo2 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ false); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ false, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo2); @@ -629,8 +616,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create NOT focused but NOT visible task final TaskInfo taskInfo3 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ false); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); + /* isVisible */ false, /* isFocused */ false); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo3); @@ -646,8 +632,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_sameTask_notUpdated() { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -675,8 +660,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_transparentTask_notUpdated() { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -694,8 +678,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create transparent task final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ true, /* isTopActivityTransparent */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo1); @@ -711,7 +694,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() { TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(false); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); @@ -725,7 +707,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagEnabled() { TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); when(mDesktopUserRepositories.getCurrent().getVisibleTaskCount(DISPLAY_ID)).thenReturn(0); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); @@ -744,7 +725,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagDisabled() { when(mDesktopUserRepositories.getCurrent().getVisibleTaskCount(DISPLAY_ID)).thenReturn(0); TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); @@ -759,22 +739,23 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) { return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false, - /* isTopActivityTransparent */ false); + /* isFocused */ false, /* isTopActivityTransparent */ false); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, - boolean isVisible) { + boolean isVisible, boolean isFocused) { return createTaskInfo(displayId, taskId, hasSizeCompat, - isVisible, /* isTopActivityTransparent */ false); + isVisible, isFocused, /* isTopActivityTransparent */ false); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, - boolean isVisible, boolean isTopActivityTransparent) { + boolean isVisible, boolean isFocused, boolean isTopActivityTransparent) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; taskInfo.displayId = displayId; taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat); taskInfo.isVisible = isVisible; + taskInfo.isFocused = isFocused; taskInfo.isTopActivityTransparent = isTopActivityTransparent; taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(true); taskInfo.appCompatTaskInfo.setTopActivityLetterboxed(true); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt index c0ff2f0652b3..9b24c1c06cec 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt @@ -52,6 +52,7 @@ import org.junit.Test import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.whenever /** Tests for [DesktopModeEventLogger]. */ @@ -90,20 +91,12 @@ class DesktopModeEventLoggerTest : ShellTestCase() { val sessionId = desktopModeEventLogger.currentSessionId.get() assertThat(sessionId).isNotEqualTo(NO_SESSION_ID) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), - /* event */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER), - /* enter_reason */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER), - /* exit_reason */ - eq(0), - /* sessionId */ - eq(sessionId), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneUiChangedLogging( + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER, + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER, + 0, + sessionId, + ) verify { EventLogTags.writeWmShellEnterDesktopMode( eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason), @@ -122,20 +115,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { val sessionId = desktopModeEventLogger.currentSessionId.get() assertThat(sessionId).isNotEqualTo(NO_SESSION_ID) assertThat(sessionId).isNotEqualTo(previousSessionId) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), - /* event */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER), - /* enter_reason */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER), - /* exit_reason */ - eq(0), - /* sessionId */ - eq(sessionId), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneUiChangedLogging( + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER, + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER, + /* exit_reason */ + 0, + sessionId, + ) verify { EventLogTags.writeWmShellEnterDesktopMode( eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason), @@ -149,7 +135,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logSessionExit_noOngoingSession_doesNotLog() { desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -159,20 +145,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), - /* event */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT), - /* enter_reason */ - eq(0), - /* exit_reason */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT), - /* sessionId */ - eq(sessionId), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneUiChangedLogging( + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT, + /* enter_reason */ + 0, + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT, + sessionId, + ) verify { EventLogTags.writeWmShellExitDesktopMode( eq(ExitReason.DRAG_TO_EXIT.reason), @@ -187,7 +166,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskAdded_noOngoingSession_doesNotLog() { desktopModeEventLogger.logTaskAdded(TASK_UPDATE) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -197,32 +176,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logTaskAdded(TASK_UPDATE) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - eq(UNSET_MINIMIZE_REASON), - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED), @@ -245,7 +211,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskRemoved_noOngoingSession_doesNotLog() { desktopModeEventLogger.logTaskRemoved(TASK_UPDATE) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -255,32 +221,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logTaskRemoved(TASK_UPDATE) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - eq(UNSET_MINIMIZE_REASON), - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED), @@ -303,7 +256,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskInfoChanged_noOngoingSession_doesNotLog() { desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -313,35 +266,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED - ), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - eq(UNSET_MINIMIZE_REASON), - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq( @@ -371,37 +308,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskUpdate(minimizeReason = MinimizeReason.TASK_LIMIT) ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED - ), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - /* minimize_reason */ - eq(MinimizeReason.TASK_LIMIT.reason), - /* unminimize_reason */ - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + MinimizeReason.TASK_LIMIT.reason, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq( @@ -431,37 +350,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskUpdate(unminimizeReason = UnminimizeReason.TASKBAR_TAP) ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED - ), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - /* minimize_reason */ - eq(UNSET_MINIMIZE_REASON), - /* unminimize_reason */ - eq(UnminimizeReason.TASKBAR_TAP.reason), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UnminimizeReason.TASKBAR_TAP.reason, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq( @@ -491,7 +392,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskInfo(), ) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -509,39 +410,17 @@ class DesktopModeEventLoggerTest : ShellTestCase() { displayController, ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), - /* resize_trigger */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER - ), - /* resizing_stage */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE - ), - /* input_method */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD - ), - /* desktop_mode_session_id */ - eq(sessionId), - /* instance_id */ - eq(TASK_SIZE_UPDATE.instanceId), - /* uid */ - eq(TASK_SIZE_UPDATE.uid), - /* task_width */ - eq(TASK_SIZE_UPDATE.taskWidth), - /* task_height */ - eq(TASK_SIZE_UPDATE.taskHeight), - /* display_area */ - eq(DISPLAY_AREA), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskSizeUpdatedLogging( + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD, + sessionId, + TASK_SIZE_UPDATE.instanceId, + TASK_SIZE_UPDATE.uid, + TASK_SIZE_UPDATE.taskWidth, + TASK_SIZE_UPDATE.taskHeight, + DISPLAY_AREA, + ) } @Test @@ -552,7 +431,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskInfo(), ) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -568,39 +447,17 @@ class DesktopModeEventLoggerTest : ShellTestCase() { displayController = displayController, ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), - /* resize_trigger */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER - ), - /* resizing_stage */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE - ), - /* input_method */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD - ), - /* desktop_mode_session_id */ - eq(sessionId), - /* instance_id */ - eq(TASK_SIZE_UPDATE.instanceId), - /* uid */ - eq(TASK_SIZE_UPDATE.uid), - /* task_width */ - eq(TASK_SIZE_UPDATE.taskWidth), - /* task_height */ - eq(TASK_SIZE_UPDATE.taskHeight), - /* display_area */ - eq(DISPLAY_AREA), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskSizeUpdatedLogging( + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD, + sessionId, + TASK_SIZE_UPDATE.instanceId, + TASK_SIZE_UPDATE.uid, + TASK_SIZE_UPDATE.taskWidth, + TASK_SIZE_UPDATE.taskHeight, + DISPLAY_AREA, + ) } private fun startDesktopModeSession(): Int { @@ -652,6 +509,171 @@ class DesktopModeEventLoggerTest : ShellTestCase() { .build() } + private fun verifyNoLogging() { + verify( + { + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }, + never(), + ) + verify( + { + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }, + never(), + ) + verify( + { + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }, + never(), + ) + } + + private fun verifyOnlyOneUiChangedLogging( + event: Int, + enterReason: Int, + exitReason: Int, + sessionId: Int, + ) { + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), + eq(event), + eq(enterReason), + eq(exitReason), + eq(sessionId), + ) + }) + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }) + } + + private fun verifyOnlyOneTaskUpdateLogging( + taskEvent: Int, + instanceId: Int, + uid: Int, + taskHeight: Int, + taskWidth: Int, + taskX: Int, + taskY: Int, + sessionId: Int, + minimizeReason: Int, + unminimizeReason: Int, + visibleTaskCount: Int, + ) { + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + eq(taskEvent), + eq(instanceId), + eq(uid), + eq(taskHeight), + eq(taskWidth), + eq(taskX), + eq(taskY), + eq(sessionId), + eq(minimizeReason), + eq(unminimizeReason), + eq(visibleTaskCount), + ) + }) + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }) + } + + private fun verifyOnlyOneTaskSizeUpdatedLogging( + resizeTrigger: Int, + resizingStage: Int, + inputMethod: Int, + sessionId: Int, + instanceId: Int, + uid: Int, + taskWidth: Int, + taskHeight: Int, + displayArea: Int, + ) { + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), + eq(resizeTrigger), + eq(resizingStage), + eq(inputMethod), + eq(sessionId), + eq(instanceId), + eq(uid), + eq(taskWidth), + eq(taskHeight), + eq(displayArea), + ) + }) + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }) + } + private companion object { private const val TASK_ID = 1 private const val TASK_UID = 1 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 5629127b8c54..daecccef9344 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -25,6 +25,7 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.Display.INVALID_DISPLAY import androidx.test.filters.SmallTest import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.ShellExecutor @@ -1067,6 +1068,67 @@ class DesktopRepositoryTest : ShellTestCase() { assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2) } + @Test + fun setTaskInPip_savedAsMinimizedPipInDisplay() { + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() + } + + @Test + fun removeTaskInPip_removedAsMinimizedPipInDisplay() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false) + + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse() + } + + @Test + fun setTaskInPip_multipleDisplays_bothAreInPip() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + repo.setTaskInPip(DEFAULT_DESKTOP_ID + 1, taskId = 2, enterPip = true) + + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID + 1, taskId = 2)).isTrue() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun setPipShouldKeepDesktopActive_shouldKeepDesktopActive() { + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = true) + + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun setPipShouldNotKeepDesktopActive_shouldNotKeepDesktopActive() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue() + + repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = false) + + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun removeTaskInPip_shouldNotKeepDesktopActive() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false) + + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse() + } + class TestListener : DesktopRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index da27c08920dc..4bb743079861 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -82,6 +82,7 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT @@ -569,6 +570,38 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun isDesktopModeShowing_minimizedPipTask_wallpaperVisible_returnsTrue() { + val pipTask = setUpPipTask(autoEnterEnabled = true) + whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible()) + .thenReturn(true) + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isTrue() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun isDesktopModeShowing_minimizedPipTask_wallpaperNotVisible_returnsFalse() { + val pipTask = setUpPipTask(autoEnterEnabled = true) + whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible()) + .thenReturn(false) + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun isDesktopModeShowing_pipTaskNotMinimizedNorVisible_returnsFalse() { + setUpPipTask(autoEnterEnabled = true) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse() + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { val homeTask = setUpHomeTask(SECOND_DISPLAY) @@ -1497,7 +1530,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() { val task = setUpFreeformTask() assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) @@ -1530,7 +1563,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { val task = setUpFreeformTask() @@ -1965,7 +1998,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() { val task = setUpFreeformTask() @@ -2011,7 +2044,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun onDesktopWindowClose_multipleActiveTasks_isOnlyNonClosingTask() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -2025,7 +2058,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun onDesktopWindowClose_multipleActiveTasks_hasMinimized() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -2039,6 +2072,41 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun onDesktopWindowClose_minimizedPipPresent_doesNotExitDesktop() { + val freeformTask = setUpFreeformTask().apply { isFocused = true } + val pipTask = setUpPipTask(autoEnterEnabled = true) + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true) + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask) + + verifyExitDesktopWCTNotExecuted() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun onDesktopWindowClose_minimizedPipNotPresent_exitDesktop() { + val freeformTask = setUpFreeformTask() + val pipTask = setUpPipTask(autoEnterEnabled = true) + val handler = mock(TransitionHandler::class.java) + whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) + .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) + + controller.minimizeTask(pipTask) + verifyExitDesktopWCTNotExecuted() + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = false) + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask) + + // Remove wallpaper operation + wct.hierarchyOps.any { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = false) val transition = Binder() @@ -2055,10 +2123,9 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() { + fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() { val task = setUpPipTask(autoEnterEnabled = true) val handler = mock(TransitionHandler::class.java) - whenever(freeformTaskTransitionStarter.startPipTransition(any())).thenReturn(Binder()) whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) @@ -2069,7 +2136,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() { + fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() { val task = setUpPipTask(autoEnterEnabled = false) whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) .thenReturn(Binder()) @@ -2081,6 +2148,22 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun onPipTaskMinimize_doesntRemoveWallpaper() { + val task = setUpPipTask(autoEnterEnabled = true) + val handler = mock(TransitionHandler::class.java) + whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) + .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) + + controller.minimizeTask(task) + + val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(freeformTaskTransitionStarter).startPipTransition(captor.capture()) + captor.value.hierarchyOps.none { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = true) val transition = Binder() @@ -2095,7 +2178,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun onTaskMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() { val task = setUpFreeformTask() val transition = Binder() @@ -2147,7 +2230,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun onDesktopWindowMinimize_multipleActiveTasks_minimizesTheOnlyVisibleTask_removesWallpaper() { val task1 = setUpFreeformTask(active = true) val task2 = setUpFreeformTask(active = true) @@ -2808,7 +2891,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() { val task = setUpFreeformTask() @@ -2849,7 +2932,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, - Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun handleRequest_backTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -2867,7 +2950,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -2934,7 +3017,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_removesWallpaper() { val task = setUpFreeformTask() @@ -2974,7 +3057,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -2992,7 +3075,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -3084,7 +3167,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun moveFocusedTaskToFullscreen_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -3125,6 +3208,31 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun moveFocusedTaskToFullscreen_minimizedPipPresent_removeWallpaperActivity() { + val freeformTask = setUpFreeformTask() + val pipTask = setUpPipTask(autoEnterEnabled = true) + val handler = mock(TransitionHandler::class.java) + whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) + .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) + + controller.minimizeTask(pipTask) + verifyExitDesktopWCTNotExecuted() + + freeformTask.isFocused = true + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[freeformTask.token.asBinder()]) + assertThat(taskChange.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Remove wallpaper operation + wct.hierarchyOps.any { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeDesktop_multipleTasks_removesAll() { val task1 = setUpFreeformTask() @@ -3596,7 +3704,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun enterSplit_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -4851,7 +4959,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo { - return setUpFreeformTask().apply { + // active = false marks the task as non-visible; PiP window doesn't count as visible tasks + return setUpFreeformTask(active = false).apply { pictureInPictureParams = PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index 3cc30cb491b3..96ed214e7f88 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -22,6 +22,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.content.ComponentName import android.content.Context import android.content.Intent +import android.os.Binder import android.os.IBinder import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule @@ -29,7 +30,9 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_BACK +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.IWindowContainerToken import android.window.TransitionInfo import android.window.TransitionInfo.Change @@ -38,6 +41,7 @@ import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER import com.android.modules.utils.testing.ExtendedMockitoRule import com.android.window.flags.Flags +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.wm.shell.MockToken import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.back.BackAnimationController @@ -47,6 +51,8 @@ import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpape import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP +import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Before @@ -239,7 +245,7 @@ class DesktopTasksTransitionObserverTest { } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun closeLastTask_wallpaperTokenExists_wallpaperIsRemoved() { val mockTransition = Mockito.mock(IBinder::class.java) val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) @@ -300,6 +306,115 @@ class DesktopTasksTransitionObserverTest { verify(taskRepository).clearTopTransparentFullscreenTaskId(topTransparentTask.displayId) } + @Test + fun transitOpenWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createOpenChangeTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider) + .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId) + } + + @Test + fun transitToFrontWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createToFrontTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider) + .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId) + } + + @Test + fun transitToBackWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createToBackTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider) + .setWallpaperActivityIsVisible(isVisible = false, wallpaperTask.displayId) + } + + @Test + fun transitCloseWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createCloseTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider).removeToken(wallpaperTask.displayId) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun pendingPipTransitionAborted_taskRepositoryOnPipAbortedInvoked() { + val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + val pipTransition = Binder() + whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) + + transitionObserver.onTransitionReady( + transition = pipTransition, + info = createOpenChangeTransition(task, TRANSIT_PIP), + startTransaction = mock(), + finishTransaction = mock(), + ) + transitionObserver.onTransitionFinished(transition = pipTransition, aborted = true) + + verify(taskRepository).onPipAborted(task.displayId, task.taskId) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun exitPipTransition_taskRepositoryClearTaskInPip() { + val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) + + transitionObserver.onTransitionReady( + transition = mock(), + info = createOpenChangeTransition(task, type = TRANSIT_EXIT_PIP), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun removePipTransition_taskRepositoryClearTaskInPip() { + val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) + + transitionObserver.onTransitionReady( + transition = mock(), + info = createOpenChangeTransition(task, type = TRANSIT_REMOVE_PIP), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) + } + private fun createBackNavigationTransition( task: RunningTaskInfo?, type: Int = TRANSIT_TO_BACK, @@ -331,7 +446,7 @@ class DesktopTasksTransitionObserverTest { task: RunningTaskInfo?, type: Int = TRANSIT_OPEN, ): TransitionInfo { - return TransitionInfo(TRANSIT_OPEN, /* flags= */ 0).apply { + return TransitionInfo(type, /* flags= */ 0).apply { addChange( Change(mock(), mock()).apply { mode = TRANSIT_OPEN @@ -369,6 +484,19 @@ class DesktopTasksTransitionObserverTest { } } + private fun createToFrontTransition(task: RunningTaskInfo?): TransitionInfo { + return TransitionInfo(TRANSIT_TO_FRONT, 0 /* flags */).apply { + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_TO_FRONT + parent = null + taskInfo = task + flags = flags + } + ) + } + } + private fun getLatestWct( @WindowManager.TransitionType type: Int = TRANSIT_OPEN, handlerClass: Class<out Transitions.TransitionHandler>? = null, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java index e40bbad7adda..1b1a5a909220 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -150,4 +150,24 @@ public class DragAndDropControllerTest extends ShellTestCase { mController.onDrag(dragLayout, event); verify(mDragAndDropListener, never()).onDragStarted(); } + + @Test + public void testOnDragStarted_withNoClipDataOrDescription() { + final View dragLayout = mock(View.class); + final Display display = mock(Display.class); + doReturn(display).when(dragLayout).getDisplay(); + doReturn(DEFAULT_DISPLAY).when(display).getDisplayId(); + + final DragEvent event = mock(DragEvent.class); + doReturn(ACTION_DRAG_STARTED).when(event).getAction(); + doReturn(null).when(event).getClipData(); + doReturn(null).when(event).getClipDescription(); + + // Ensure there's a target so that onDrag will execute + mController.addDisplayDropTarget(0, mContext, mock(WindowManager.class), + mock(FrameLayout.class), mock(DragLayout.class)); + + // Verify the listener is called on a valid drag action. + mController.onDrag(dragLayout, event); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java index 0cf15baf30b0..a284663d9a38 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java @@ -220,7 +220,7 @@ public class SplitDragPolicyTest extends ShellTestCase { setRunningTask(mHomeTask); DragSession dragSession = new DragSession(mActivityTaskManager, mLandscapeDisplayLayout, data, 0 /* dragFlags */); - dragSession.initialize(); + dragSession.initialize(false /* skipUpdateRunningTask */); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); @@ -235,7 +235,7 @@ public class SplitDragPolicyTest extends ShellTestCase { setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, mLandscapeDisplayLayout, data, 0 /* dragFlags */); - dragSession.initialize(); + dragSession.initialize(false /* skipUpdateRunningTask */); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); @@ -255,7 +255,7 @@ public class SplitDragPolicyTest extends ShellTestCase { setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, mPortraitDisplayLayout, data, 0 /* dragFlags */); - dragSession.initialize(); + dragSession.initialize(false /* skipUpdateRunningTask */); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); @@ -276,7 +276,7 @@ public class SplitDragPolicyTest extends ShellTestCase { setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, mLandscapeDisplayLayout, mActivityClipData, 0 /* dragFlags */); - dragSession.initialize(); + dragSession.initialize(false /* skipUpdateRunningTask */); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = mPolicy.getTargets(mInsets); for (Target t : targets) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java index a8aa25700c7e..c42f6c35bcb0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java @@ -30,6 +30,7 @@ import static org.mockito.kotlin.MatchersKt.eq; import static org.mockito.kotlin.VerificationKt.times; import static org.mockito.kotlin.VerificationKt.verify; +import android.app.TaskInfo; import android.content.Context; import android.content.res.Resources; import android.graphics.Matrix; @@ -45,7 +46,9 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; @@ -56,6 +59,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Optional; @@ -83,7 +87,8 @@ public class PipSchedulerTest { @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; @Mock private SurfaceControl.Transaction mMockTransaction; @Mock private PipAlphaAnimator mMockAlphaAnimator; - @Mock private Optional<DesktopUserRepositories> mMockOptionalDesktopUserRepositories; + @Mock private DesktopUserRepositories mMockDesktopUserRepositories; + @Mock private DesktopWallpaperActivityTokenProvider mMockDesktopWallpaperActivityTokenProvider; @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; @@ -100,9 +105,13 @@ public class PipSchedulerTest { when(mMockFactory.getTransaction()).thenReturn(mMockTransaction); when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) .thenReturn(mMockTransaction); + when(mMockDesktopUserRepositories.getCurrent()) + .thenReturn(Mockito.mock(DesktopRepository.class)); + when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(Mockito.mock(TaskInfo.class)); mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor, - mMockPipTransitionState, mMockOptionalDesktopUserRepositories, + mMockPipTransitionState, Optional.of(mMockDesktopUserRepositories), + Optional.of(mMockDesktopWallpaperActivityTokenProvider), mRootTaskDisplayAreaOrganizer); mPipScheduler.setPipTransitionController(mMockPipTransitionController); mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java index 894d238b7e15..ab43119b14c0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -169,7 +169,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { final IResultReceiver finishCallback = mock(IResultReceiver.class); final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner); - verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any()); + verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any()); // Finish and verify no transition remains and that the provided finish callback is called mRecentsTransitionHandler.findController(transition).finish(true /* toHome */, @@ -184,7 +184,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class); final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner); - verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any()); + verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any()); mRecentsTransitionHandler.findController(transition).cancel("test"); mMainExecutor.flushAll(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index ffe8e7135513..79e9b9c8cd77 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -59,11 +59,12 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.window.flags.Flags import com.android.wm.shell.R -import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.DesktopImmersiveController import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition +import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction +import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.splitscreen.SplitScreenController @@ -539,7 +540,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest onLeftSnapClickListenerCaptor.value.invoke() verify(mockDesktopTasksController, never()) - .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT), + .snapToHalfScreen( + eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT), eq(ResizeTrigger.MAXIMIZE_BUTTON), eq(InputMethod.UNKNOWN_INPUT_METHOD), eq(decor), @@ -616,11 +618,12 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest onRightSnapClickListenerCaptor.value.invoke() verify(mockDesktopTasksController, never()) - .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT), + .snapToHalfScreen( + eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT), eq(ResizeTrigger.MAXIMIZE_BUTTON), eq(InputMethod.UNKNOWN_INPUT_METHOD), eq(decor), - ) + ) } @Test @@ -1223,6 +1226,49 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest verify(task2, never()).onExclusionRegionChanged(newRegion) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun testRecentsTransitionStateListener_requestedState_setsTransitionRunning() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration = setUpMockDecorationForTask(task) + onTaskOpening(task, SurfaceControl()) + + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED) + + verify(decoration).setIsRecentsTransitionRunning(true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun testRecentsTransitionStateListener_nonRunningState_setsTransitionNotRunning() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration = setUpMockDecorationForTask(task) + onTaskOpening(task, SurfaceControl()) + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED) + + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING) + + verify(decoration).setIsRecentsTransitionRunning(false) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + fun testRecentsTransitionStateListener_requestedAndAnimating_setsTransitionRunningOnce() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration = setUpMockDecorationForTask(task) + onTaskOpening(task, SurfaceControl()) + + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED) + desktopModeRecentsTransitionStateListener.onTransitionStateChanged( + RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING) + + verify(decoration, times(1)).setIsRecentsTransitionRunning(true) + } + private fun createOpenTaskDecoration( @WindowingMode windowingMode: Int, taskSurface: SurfaceControl = SurfaceControl(), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index b5e8cebc1277..8af8285d031c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -40,6 +40,7 @@ import android.view.SurfaceControl import android.view.WindowInsets.Type.statusBars import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.internal.jank.InteractionJankMonitor +import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase @@ -65,6 +66,8 @@ import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.desktopmode.education.AppHandleEducationController import com.android.wm.shell.desktopmode.education.AppToWebEducationController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter +import com.android.wm.shell.recents.RecentsTransitionHandler +import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController @@ -151,6 +154,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected val mockFocusTransitionObserver = mock<FocusTransitionObserver>() protected val mockCaptionHandleRepository = mock<WindowDecorCaptionHandleRepository>() protected val mockDesktopRepository: DesktopRepository = mock<DesktopRepository>() + protected val mockRecentsTransitionHandler = mock<RecentsTransitionHandler>() protected val motionEvent = mock<MotionEvent>() val displayLayout = mock<DisplayLayout>() protected lateinit var spyContext: TestableContext @@ -164,6 +168,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected lateinit var mockitoSession: StaticMockitoSession protected lateinit var shellInit: ShellInit internal lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener + protected lateinit var desktopModeRecentsTransitionStateListener: RecentsTransitionStateListener protected lateinit var displayChangingListener: DisplayChangeController.OnDisplayChangingListener internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener @@ -219,7 +224,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mockFocusTransitionObserver, desktopModeEventLogger, mock<DesktopModeUiEventLogger>(), - mock<WindowDecorTaskResourceLoader>() + mock<WindowDecorTaskResourceLoader>(), + mockRecentsTransitionHandler, ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -256,6 +262,13 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { verify(displayInsetsController) .addGlobalInsetsChangedListener(insetsChangedCaptor.capture()) desktopModeOnInsetsChangedListener = insetsChangedCaptor.firstValue + val recentsTransitionStateListenerCaptor = argumentCaptor<RecentsTransitionStateListener>() + if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) { + verify(mockRecentsTransitionHandler) + .addTransitionStateListener(recentsTransitionStateListenerCaptor.capture()) + desktopModeRecentsTransitionStateListener = + recentsTransitionStateListenerCaptor.firstValue + } val keyguardChangedCaptor = argumentCaptor<DesktopModeKeyguardChangeListener>() verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture()) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 6b02aeffd42a..9ea5fd6e1abe 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -169,6 +169,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final boolean DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED = false; private static final boolean DEFAULT_IS_IN_FULL_IMMERSIVE_MODE = false; private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true; + private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @@ -396,6 +397,31 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + public void updateRelayoutParams_shouldIgnoreCornerRadius_roundedCornersNotSet() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + fillRoundedCornersResources(/* fillValue= */ 30); + RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + mMockSplitScreenController, + DEFAULT_APPLY_START_TRANSACTION_ON_DRAW, + DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP, + DEFAULT_IS_STATUSBAR_VISIBLE, + DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, + DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + new InsetsState(), + DEFAULT_HAS_GLOBAL_FOCUS, + mExclusionRegion, + /* shouldIgnoreCornerRadius= */ true); + + assertThat(relayoutParams.mCornerRadius).isEqualTo(INVALID_CORNER_RADIUS); + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY) public void updateRelayoutParams_appHeader_usesTaskDensity() { final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources() @@ -634,7 +660,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, insetsState, DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); // Takes status bar inset as padding, ignores caption bar inset. assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50); @@ -659,7 +686,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsInsetSource).isFalse(); } @@ -683,7 +711,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); // Header is always shown because it's assumed the status bar is always visible. assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -707,7 +736,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); } @@ -730,7 +760,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -753,7 +784,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -777,7 +809,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -793,7 +826,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -817,7 +851,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* inFullImmersiveMode */ true, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -1480,7 +1515,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, - mExclusionRegion); + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); } private DesktopModeWindowDecoration createWindowDecoration( diff --git a/libs/androidfw/ZipUtils.cpp b/libs/androidfw/ZipUtils.cpp index a1385f2cf7b1..f7f62c51a25b 100644 --- a/libs/androidfw/ZipUtils.cpp +++ b/libs/androidfw/ZipUtils.cpp @@ -87,19 +87,29 @@ class BufferReader final : public zip_archive::Reader { } bool ReadAtOffset(uint8_t* buf, size_t len, off64_t offset) const override { - if (mInputSize < len || offset > mInputSize - len) { - return false; - } - - const incfs::map_ptr<uint8_t> pos = mInput.offset(offset); - if (!pos.verify(len)) { + auto in = AccessAtOffset(buf, len, offset); + if (!in) { return false; } - - memcpy(buf, pos.unsafe_ptr(), len); + memcpy(buf, in, len); return true; } + const uint8_t* AccessAtOffset(uint8_t*, size_t len, off64_t offset) const override { + if (offset > mInputSize - len) { + return nullptr; + } + const incfs::map_ptr<uint8_t> pos = mInput.offset(offset); + if (!pos.verify(len)) { + return nullptr; + } + return pos.unsafe_ptr(); + } + + bool IsZeroCopy() const override { + return true; + } + private: const incfs::map_ptr<uint8_t> mInput; const size_t mInputSize; @@ -107,7 +117,7 @@ class BufferReader final : public zip_archive::Reader { class BufferWriter final : public zip_archive::Writer { public: - BufferWriter(void* output, size_t outputSize) : Writer(), + BufferWriter(void* output, size_t outputSize) : mOutput(reinterpret_cast<uint8_t*>(output)), mOutputSize(outputSize), mBytesWritten(0) { } @@ -121,6 +131,12 @@ class BufferWriter final : public zip_archive::Writer { return true; } + Buffer GetBuffer(size_t length) override { + const auto remaining_size = mOutputSize - mBytesWritten; + return remaining_size >= length + ? Buffer(mOutput + mBytesWritten, remaining_size) : Buffer(); + } + private: uint8_t* const mOutput; const size_t mOutputSize; diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java index 9f3c34575b94..81d9d81c4f58 100644 --- a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java @@ -74,6 +74,7 @@ public abstract class AppFunctionService extends Service { /* context= */ this, /* onExecuteFunction= */ (platformRequest, callingPackage, + callingPackageSigningInfo, cancellationSignal, callback) -> { AppFunctionService.this.onExecuteFunction( @@ -105,15 +106,17 @@ public abstract class AppFunctionService extends Service { /** * Called by the system to execute a specific app function. * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. + * <p>This method is the entry point for handling all app function requests in an app. When the + * system needs your AppFunctionService to perform a function, it will invoke this method. * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * <p>Each function you've registered is identified by a unique identifier. This identifier + * doesn't need to be globally unique, but it must be unique within your app. For example, a + * function to order food could be identified as "orderFood". In most cases, this identifier is + * automatically generated by the AppFunctions SDK. + * + * <p>You can determine which function to execute by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. This allows your service to route the + * incoming request to the appropriate logic for handling the specific function. * * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker * thread and dispatch the result with the given callback. You should always report back the @@ -132,7 +135,5 @@ public abstract class AppFunctionService extends Service { @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, @NonNull CancellationSignal cancellationSignal, - @NonNull - OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> - callback); + @NonNull OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback); } diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 76ad2acccf89..5e71d3360f39 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -34,13 +34,6 @@ flag { } flag { - name: "high_contrast_text_luminance" - namespace: "accessibility" - description: "Use luminance to determine how to make text more high contrast, instead of RGB heuristic" - bug: "186567103" -} - -flag { name: "high_contrast_text_small_text_rect" namespace: "accessibility" description: "Draw a solid rectangle background behind text instead of a stroke outline" diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index e13e136550ca..e05c3d695463 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -34,9 +34,6 @@ namespace flags = com::android::graphics::hwui::flags; #else namespace flags { -constexpr bool high_contrast_text_luminance() { - return false; -} constexpr bool high_contrast_text_small_text_rect() { return false; } @@ -114,15 +111,10 @@ public: if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) { // high contrast draw path int color = paint.getColor(); - bool darken; - // This equation should match the one in core/java/android/text/Layout.java - if (flags::high_contrast_text_luminance()) { - uirenderer::Lab lab = uirenderer::sRGBToLab(color); - darken = lab.L <= 50; - } else { - int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color); - darken = channelSum < (128 * 3); - } + // LINT.IfChange(hct_darken) + uirenderer::Lab lab = uirenderer::sRGBToLab(color); + bool darken = lab.L <= 50; + // LINT.ThenChange(/core/java/android/text/Layout.java:hct_darken) // outline gDrawTextBlobMode = DrawTextBlobMode::HctOutline; diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp index 7b45070af312..290df997a8ed 100644 --- a/libs/hwui/hwui/MinikinUtils.cpp +++ b/libs/hwui/hwui/MinikinUtils.cpp @@ -36,7 +36,7 @@ minikin::MinikinPaint MinikinUtils::prepareMinikinPaint(const Paint* paint, const Typeface* resolvedFace = Typeface::resolveDefault(typeface); const SkFont& font = paint->getSkFont(); - minikin::MinikinPaint minikinPaint(resolvedFace->fFontCollection); + minikin::MinikinPaint minikinPaint(resolvedFace->getFontCollection()); /* Prepare minikin Paint */ minikinPaint.size = font.isLinearMetrics() ? font.getSize() : static_cast<int>(font.getSize()); @@ -46,9 +46,9 @@ minikin::MinikinPaint MinikinUtils::prepareMinikinPaint(const Paint* paint, minikinPaint.wordSpacing = paint->getWordSpacing(); minikinPaint.fontFlags = MinikinFontSkia::packFontFlags(font); minikinPaint.localeListId = paint->getMinikinLocaleListId(); - minikinPaint.fontStyle = resolvedFace->fStyle; + minikinPaint.fontStyle = resolvedFace->getFontStyle(); minikinPaint.fontFeatureSettings = paint->getFontFeatureSettings(); - if (!resolvedFace->fIsVariationInstance) { + if (!resolvedFace->isVariationInstance()) { // This is an optimization for direct private API use typically done by System UI. // In the public API surface, if Typeface is already configured for variation instance // (Target SDK <= 35) the font variation settings of Paint is not set. @@ -132,7 +132,7 @@ minikin::MinikinExtent MinikinUtils::getFontExtent(const Paint* paint, minikin:: bool MinikinUtils::hasVariationSelector(const Typeface* typeface, uint32_t codepoint, uint32_t vs) { const Typeface* resolvedFace = Typeface::resolveDefault(typeface); - return resolvedFace->fFontCollection->hasVariationSelector(codepoint, vs); + return resolvedFace->getFontCollection()->hasVariationSelector(codepoint, vs); } float MinikinUtils::xOffsetForTextAlign(Paint* paint, const minikin::Layout& layout) { diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp index 4dfe05377a48..a73aac632752 100644 --- a/libs/hwui/hwui/Typeface.cpp +++ b/libs/hwui/hwui/Typeface.cpp @@ -70,74 +70,45 @@ const Typeface* Typeface::resolveDefault(const Typeface* src) { Typeface* Typeface::createRelative(Typeface* src, Typeface::Style style) { const Typeface* resolvedFace = Typeface::resolveDefault(src); - Typeface* result = new Typeface; - if (result != nullptr) { - result->fFontCollection = resolvedFace->fFontCollection; - result->fBaseWeight = resolvedFace->fBaseWeight; - result->fAPIStyle = style; - result->fStyle = computeRelativeStyle(result->fBaseWeight, style); - result->fIsVariationInstance = resolvedFace->fIsVariationInstance; - } - return result; + return new Typeface(resolvedFace->getFontCollection(), + computeRelativeStyle(resolvedFace->getBaseWeight(), style), style, + resolvedFace->getBaseWeight(), resolvedFace->isVariationInstance()); } Typeface* Typeface::createAbsolute(Typeface* base, int weight, bool italic) { const Typeface* resolvedFace = Typeface::resolveDefault(base); - Typeface* result = new Typeface(); - if (result != nullptr) { - result->fFontCollection = resolvedFace->fFontCollection; - result->fBaseWeight = resolvedFace->fBaseWeight; - result->fAPIStyle = computeAPIStyle(weight, italic); - result->fStyle = computeMinikinStyle(weight, italic); - result->fIsVariationInstance = resolvedFace->fIsVariationInstance; - } - return result; + return new Typeface(resolvedFace->getFontCollection(), computeMinikinStyle(weight, italic), + computeAPIStyle(weight, italic), resolvedFace->getBaseWeight(), + resolvedFace->isVariationInstance()); } Typeface* Typeface::createFromTypefaceWithVariation(Typeface* src, const minikin::VariationSettings& variations) { const Typeface* resolvedFace = Typeface::resolveDefault(src); - Typeface* result = new Typeface(); - if (result != nullptr) { - result->fFontCollection = - resolvedFace->fFontCollection->createCollectionWithVariation(variations); - if (result->fFontCollection == nullptr) { + const std::shared_ptr<minikin::FontCollection>& fc = + resolvedFace->getFontCollection()->createCollectionWithVariation(variations); + return new Typeface( // None of passed axes are supported by this collection. // So we will reuse the same collection with incrementing reference count. - result->fFontCollection = resolvedFace->fFontCollection; - } - // Do not update styles. - // TODO: We may want to update base weight if the 'wght' is specified. - result->fBaseWeight = resolvedFace->fBaseWeight; - result->fAPIStyle = resolvedFace->fAPIStyle; - result->fStyle = resolvedFace->fStyle; - result->fIsVariationInstance = true; - } - return result; + fc ? fc : resolvedFace->getFontCollection(), + // Do not update styles. + // TODO: We may want to update base weight if the 'wght' is specified. + resolvedFace->fStyle, resolvedFace->getAPIStyle(), resolvedFace->getBaseWeight(), true); } Typeface* Typeface::createWithDifferentBaseWeight(Typeface* src, int weight) { const Typeface* resolvedFace = Typeface::resolveDefault(src); - Typeface* result = new Typeface; - if (result != nullptr) { - result->fFontCollection = resolvedFace->fFontCollection; - result->fBaseWeight = weight; - result->fAPIStyle = resolvedFace->fAPIStyle; - result->fStyle = computeRelativeStyle(weight, result->fAPIStyle); - result->fIsVariationInstance = resolvedFace->fIsVariationInstance; - } - return result; + return new Typeface(resolvedFace->getFontCollection(), + computeRelativeStyle(weight, resolvedFace->getAPIStyle()), + resolvedFace->getAPIStyle(), weight, resolvedFace->isVariationInstance()); } Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic, const Typeface* fallback) { - Typeface* result = new Typeface; - if (fallback == nullptr) { - result->fFontCollection = minikin::FontCollection::create(std::move(families)); - } else { - result->fFontCollection = - fallback->fFontCollection->createCollectionWithFamilies(std::move(families)); - } + const std::shared_ptr<minikin::FontCollection>& fc = + fallback ? fallback->getFontCollection()->createCollectionWithFamilies( + std::move(families)) + : minikin::FontCollection::create(std::move(families)); if (weight == RESOLVE_BY_FONT_TABLE || italic == RESOLVE_BY_FONT_TABLE) { int weightFromFont; @@ -171,11 +142,8 @@ Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::Font weight = SkFontStyle::kNormal_Weight; } - result->fBaseWeight = weight; - result->fAPIStyle = computeAPIStyle(weight, italic); - result->fStyle = computeMinikinStyle(weight, italic); - result->fIsVariationInstance = false; - return result; + return new Typeface(fc, computeMinikinStyle(weight, italic), computeAPIStyle(weight, italic), + weight, false); } void Typeface::setDefault(const Typeface* face) { @@ -205,11 +173,8 @@ void Typeface::setRobotoTypefaceForTest() { std::shared_ptr<minikin::FontCollection> collection = minikin::FontCollection::create(minikin::FontFamily::create(std::move(fonts))); - Typeface* hwTypeface = new Typeface(); - hwTypeface->fFontCollection = collection; - hwTypeface->fAPIStyle = Typeface::kNormal; - hwTypeface->fBaseWeight = SkFontStyle::kNormal_Weight; - hwTypeface->fStyle = minikin::FontStyle(); + Typeface* hwTypeface = new Typeface(collection, minikin::FontStyle(), Typeface::kNormal, + SkFontStyle::kNormal_Weight, false); Typeface::setDefault(hwTypeface); #endif diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h index 97d1bf4ef011..e8233a6bc6d8 100644 --- a/libs/hwui/hwui/Typeface.h +++ b/libs/hwui/hwui/Typeface.h @@ -32,21 +32,39 @@ constexpr int RESOLVE_BY_FONT_TABLE = -1; struct ANDROID_API Typeface { public: - std::shared_ptr<minikin::FontCollection> fFontCollection; + enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 }; + Typeface(const std::shared_ptr<minikin::FontCollection> fc, minikin::FontStyle style, + Style apiStyle, int baseWeight, bool isVariationInstance) + : fFontCollection(fc) + , fStyle(style) + , fAPIStyle(apiStyle) + , fBaseWeight(baseWeight) + , fIsVariationInstance(isVariationInstance) {} + + const std::shared_ptr<minikin::FontCollection>& getFontCollection() const { + return fFontCollection; + } // resolved style actually used for rendering - minikin::FontStyle fStyle; + minikin::FontStyle getFontStyle() const { return fStyle; } // style used in the API - enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 }; - Style fAPIStyle; + Style getAPIStyle() const { return fAPIStyle; } // base weight in CSS-style units, 1..1000 - int fBaseWeight; + int getBaseWeight() const { return fBaseWeight; } // True if the Typeface is already created for variation settings. - bool fIsVariationInstance; + bool isVariationInstance() const { return fIsVariationInstance; } +private: + std::shared_ptr<minikin::FontCollection> fFontCollection; + minikin::FontStyle fStyle; + Style fAPIStyle; + int fBaseWeight; + bool fIsVariationInstance = false; + +public: static const Typeface* resolveDefault(const Typeface* src); // The following three functions create new Typeface from an existing Typeface with a different diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp index 8d3a5eb2b4af..f6fdec1c82bc 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -609,7 +609,8 @@ namespace PaintGlue { SkFont* font = &paint->getSkFont(); const Typeface* typeface = paint->getAndroidTypeface(); typeface = Typeface::resolveDefault(typeface); - minikin::FakedFont baseFont = typeface->fFontCollection->baseFontFaked(typeface->fStyle); + minikin::FakedFont baseFont = + typeface->getFontCollection()->baseFontFaked(typeface->getFontStyle()); float saveSkewX = font->getSkewX(); bool savefakeBold = font->isEmbolden(); MinikinFontSkia::populateSkFont(font, baseFont.typeface().get(), baseFont.fakery); @@ -641,7 +642,7 @@ namespace PaintGlue { if (useLocale) { minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(paint, typeface); minikin::MinikinExtent extent = - typeface->fFontCollection->getReferenceExtentForLocale(minikinPaint); + typeface->getFontCollection()->getReferenceExtentForLocale(minikinPaint); metrics->fAscent = std::min(extent.ascent, metrics->fAscent); metrics->fDescent = std::max(extent.descent, metrics->fDescent); metrics->fTop = std::min(metrics->fAscent, metrics->fTop); diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp index 707577d6d075..63906de80745 100644 --- a/libs/hwui/jni/Typeface.cpp +++ b/libs/hwui/jni/Typeface.cpp @@ -99,17 +99,17 @@ static jlong Typeface_getReleaseFunc(CRITICAL_JNI_PARAMS) { // CriticalNative static jint Typeface_getStyle(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - return toTypeface(faceHandle)->fAPIStyle; + return toTypeface(faceHandle)->getAPIStyle(); } // CriticalNative static jint Typeface_getWeight(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - return toTypeface(faceHandle)->fStyle.weight(); + return toTypeface(faceHandle)->getFontStyle().weight(); } // Critical Native static jboolean Typeface_isVariationInstance(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - return toTypeface(faceHandle)->fIsVariationInstance; + return toTypeface(faceHandle)->isVariationInstance(); } static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray, @@ -128,18 +128,18 @@ static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArr // CriticalNative static void Typeface_setDefault(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { Typeface::setDefault(toTypeface(faceHandle)); - minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->fFontCollection); + minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->getFontCollection()); } static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle) { Typeface* face = toTypeface(faceHandle); - const size_t length = face->fFontCollection->getSupportedAxesCount(); + const size_t length = face->getFontCollection()->getSupportedAxesCount(); if (length == 0) { return nullptr; } std::vector<jint> tagVec(length); for (size_t i = 0; i < length; i++) { - tagVec[i] = face->fFontCollection->getSupportedAxisAt(i); + tagVec[i] = face->getFontCollection()->getSupportedAxisAt(i); } std::sort(tagVec.begin(), tagVec.end()); const jintArray result = env->NewIntArray(length); @@ -150,7 +150,7 @@ static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle) static void Typeface_registerGenericFamily(JNIEnv *env, jobject, jstring familyName, jlong ptr) { ScopedUtfChars familyNameChars(env, familyName); minikin::SystemFonts::registerFallback(familyNameChars.c_str(), - toTypeface(ptr)->fFontCollection); + toTypeface(ptr)->getFontCollection()); } #ifdef __ANDROID__ @@ -315,18 +315,19 @@ static jint Typeface_writeTypefaces(JNIEnv* env, jobject, jobject buffer, jint p std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections; std::unordered_map<std::shared_ptr<minikin::FontCollection>, size_t> fcToIndex; for (Typeface* typeface : typefaces) { - bool inserted = fcToIndex.emplace(typeface->fFontCollection, fontCollections.size()).second; + bool inserted = + fcToIndex.emplace(typeface->getFontCollection(), fontCollections.size()).second; if (inserted) { - fontCollections.push_back(typeface->fFontCollection); + fontCollections.push_back(typeface->getFontCollection()); } } minikin::FontCollection::writeVector(&writer, fontCollections); writer.write<uint32_t>(typefaces.size()); for (Typeface* typeface : typefaces) { - writer.write<uint32_t>(fcToIndex.find(typeface->fFontCollection)->second); - typeface->fStyle.writeTo(&writer); - writer.write<Typeface::Style>(typeface->fAPIStyle); - writer.write<int>(typeface->fBaseWeight); + writer.write<uint32_t>(fcToIndex.find(typeface->getFontCollection())->second); + typeface->getFontStyle().writeTo(&writer); + writer.write<Typeface::Style>(typeface->getAPIStyle()); + writer.write<int>(typeface->getBaseWeight()); } return static_cast<jint>(writer.size()); } @@ -349,11 +350,10 @@ static jlongArray Typeface_readTypefaces(JNIEnv* env, jobject, jobject buffer, j std::vector<jlong> faceHandles; faceHandles.reserve(typefaceCount); for (uint32_t i = 0; i < typefaceCount; i++) { - Typeface* typeface = new Typeface; - typeface->fFontCollection = fontCollections[reader.read<uint32_t>()]; - typeface->fStyle = minikin::FontStyle(&reader); - typeface->fAPIStyle = reader.read<Typeface::Style>(); - typeface->fBaseWeight = reader.read<int>(); + Typeface* typeface = + new Typeface(fontCollections[reader.read<uint32_t>()], minikin::FontStyle(&reader), + reader.read<Typeface::Style>(), reader.read<int>(), + false /* isVariationInstance */); faceHandles.push_back(toJLong(typeface)); } const jlongArray result = env->NewLongArray(typefaceCount); @@ -381,7 +381,8 @@ static void Typeface_warmUpCache(JNIEnv* env, jobject, jstring jFilePath) { // Critical Native static void Typeface_addFontCollection(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - std::shared_ptr<minikin::FontCollection> collection = toTypeface(faceHandle)->fFontCollection; + std::shared_ptr<minikin::FontCollection> collection = + toTypeface(faceHandle)->getFontCollection(); minikin::SystemFonts::addFontMap(std::move(collection)); } diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp index d1782b285b34..7a4ae8330de8 100644 --- a/libs/hwui/jni/text/TextShaper.cpp +++ b/libs/hwui/jni/text/TextShaper.cpp @@ -104,7 +104,7 @@ static jlong shapeTextRun(const uint16_t* text, int textSize, int start, int cou } else { fontId = fonts.size(); // This is new to us. Create new one. std::shared_ptr<minikin::Font> font; - if (resolvedFace->fIsVariationInstance) { + if (resolvedFace->isVariationInstance()) { // The optimization for target SDK 35 or before because the variation instance // is already created and no runtime variation resolution happens on such // environment. diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h index 859cc57dea9f..4c9656792dac 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.h +++ b/libs/hwui/renderthread/HintSessionWrapper.h @@ -20,6 +20,7 @@ #include <private/performance_hint_private.h> #include <future> +#include <memory> #include <optional> #include <vector> diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index 6571d92aeafa..a67aea466c1c 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -729,7 +729,7 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) { VkSemaphore semaphore; VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore); ALOGE_IF(VK_SUCCESS != err, - "VulkanManager::makeSwapSemaphore(): Failed to create semaphore"); + "VulkanManager::finishFrame(): Failed to create semaphore"); if (err == VK_SUCCESS) { sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore); @@ -777,7 +777,7 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) { int fenceFd = -1; VkResult err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd); - ALOGE_IF(VK_SUCCESS != err, "VulkanManager::swapBuffers(): Failed to get semaphore Fd"); + ALOGE_IF(VK_SUCCESS != err, "VulkanManager::finishFrame(): Failed to get semaphore Fd"); drawResult.presentFence.reset(fenceFd); } else { ALOGE("VulkanManager::finishFrame(): Semaphore submission failed"); diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp index 93118aeafaaf..b51414fd3c02 100644 --- a/libs/hwui/tests/common/TestUtils.cpp +++ b/libs/hwui/tests/common/TestUtils.cpp @@ -183,8 +183,11 @@ SkRect TestUtils::getLocalClipBounds(const SkCanvas* canvas) { } SkFont TestUtils::defaultFont() { - const std::shared_ptr<minikin::MinikinFont>& minikinFont = - Typeface::resolveDefault(nullptr)->fFontCollection->getFamilyAt(0)->getFont(0)->baseTypeface(); + const std::shared_ptr<minikin::MinikinFont>& minikinFont = Typeface::resolveDefault(nullptr) + ->getFontCollection() + ->getFamilyAt(0) + ->getFont(0) + ->baseTypeface(); SkTypeface* skTypeface = reinterpret_cast<const MinikinFontSkia*>(minikinFont.get())->GetSkTypeface(); LOG_ALWAYS_FATAL_IF(skTypeface == nullptr); return SkFont(sk_ref_sp(skTypeface)); diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp index c71c4d243a8b..7bcd937397b0 100644 --- a/libs/hwui/tests/unit/TypefaceTests.cpp +++ b/libs/hwui/tests/unit/TypefaceTests.cpp @@ -90,40 +90,40 @@ TEST(TypefaceTest, resolveDefault_and_setDefaultTest) { TEST(TypefaceTest, createWithDifferentBaseWeight) { std::unique_ptr<Typeface> bold(Typeface::createWithDifferentBaseWeight(nullptr, 700)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, bold->getAPIStyle()); std::unique_ptr<Typeface> light(Typeface::createWithDifferentBaseWeight(nullptr, 300)); - EXPECT_EQ(300, light->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, light->fAPIStyle); + EXPECT_EQ(300, light->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, light->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromRegular) { // In Java, Typeface.create(Typeface.DEFAULT, Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(nullptr, Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(nullptr, Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.DEFAULT, Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(nullptr, Typeface::kItalic)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic(Typeface::createRelative(nullptr, Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_BoldBase) { @@ -132,31 +132,31 @@ TEST(TypefaceTest, createRelativeTest_BoldBase) { // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(700, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(700, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(1000, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(1000, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(700, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(700, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(1000, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(1000, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_LightBase) { @@ -165,31 +165,31 @@ TEST(TypefaceTest, createRelativeTest_LightBase) { // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(300, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(300, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(600, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(600, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.ITLIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(300, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(300, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(600, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(600, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromBoldStyled) { @@ -198,32 +198,32 @@ TEST(TypefaceTest, createRelativeTest_fromBoldStyled) { // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromItalicStyled) { @@ -233,33 +233,33 @@ TEST(TypefaceTest, createRelativeTest_fromItalicStyled) { // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, // Typeface.ITALIC), Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC), // Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) { @@ -270,27 +270,27 @@ TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) { // .setWeight(700).setItalic(false).build(); // Typeface.create(typeface, Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif") // .setWeight(700).setItalic(false).build(); // Typeface.create(typeface, Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif") // .setWeight(700).setItalic(false).build(); // Typeface.create(typeface, Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif") @@ -298,9 +298,9 @@ TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) { // Typeface.create(typeface, Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createAbsolute) { @@ -309,45 +309,45 @@ TEST(TypefaceTest, createAbsolute) { // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(false) // .build(); std::unique_ptr<Typeface> regular(Typeface::createAbsolute(nullptr, 400, false)); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(false) // .build(); std::unique_ptr<Typeface> bold(Typeface::createAbsolute(nullptr, 700, false)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(true) // .build(); std::unique_ptr<Typeface> italic(Typeface::createAbsolute(nullptr, 400, true)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(true) // .build(); std::unique_ptr<Typeface> boldItalic(Typeface::createAbsolute(nullptr, 700, true)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(1100).setItalic(true) // .build(); std::unique_ptr<Typeface> over1000(Typeface::createAbsolute(nullptr, 1100, false)); - EXPECT_EQ(1000, over1000->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle); + EXPECT_EQ(1000, over1000->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle()); } TEST(TypefaceTest, createFromFamilies_Single) { @@ -355,43 +355,43 @@ TEST(TypefaceTest, createFromFamilies_Single) { // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(false).build(); std::unique_ptr<Typeface> regular(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 400, false, nullptr /* fallback */)); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle()); // In Java, new // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(false).build(); std::unique_ptr<Typeface> bold(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 700, false, nullptr /* fallback */)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, new // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(true).build(); std::unique_ptr<Typeface> italic(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 400, true, nullptr /* fallback */)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // new // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(true).build(); std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 700, true, nullptr /* fallback */)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // new // Typeface.Builder("Roboto-Regular.ttf").setWeight(1100).setItalic(false).build(); std::unique_ptr<Typeface> over1000(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 1100, false, nullptr /* fallback */)); - EXPECT_EQ(1000, over1000->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle); + EXPECT_EQ(1000, over1000->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle()); } TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) { @@ -399,33 +399,33 @@ TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) { std::unique_ptr<Typeface> regular( Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle()); // In Java, new Typeface.Builder("Family-Bold.ttf").build(); std::unique_ptr<Typeface> bold( Typeface::createFromFamilies(makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, new Typeface.Builder("Family-Italic.ttf").build(); std::unique_ptr<Typeface> italic( Typeface::createFromFamilies(makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, new Typeface.Builder("Family-BoldItalic.ttf").build(); std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies( makeSingleFamlyVector(kBoldItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); } TEST(TypefaceTest, createFromFamilies_Family) { @@ -435,8 +435,8 @@ TEST(TypefaceTest, createFromFamilies_Family) { std::unique_ptr<Typeface> typeface( Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(400, typeface->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant()); + EXPECT_EQ(400, typeface->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant()); } TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) { @@ -445,8 +445,8 @@ TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) { std::unique_ptr<Typeface> typeface( Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(700, typeface->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant()); + EXPECT_EQ(700, typeface->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant()); } TEST(TypefaceTest, createFromFamilies_Family_withFallback) { @@ -458,8 +458,8 @@ TEST(TypefaceTest, createFromFamilies_Family_withFallback) { std::unique_ptr<Typeface> regular( Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, fallback.get())); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); } } // namespace diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING index e52e0b16eca3..6a21496f1165 100644 --- a/media/TEST_MAPPING +++ b/media/TEST_MAPPING @@ -1,7 +1,10 @@ { "presubmit": [ { - "name": "CtsMediaBetterTogetherTestCases" + "name": "CtsMediaRouterTestCases" + }, + { + "name": "CtsMediaSessionTestCases" }, { "name": "mediaroutertest" diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 54a87ad7fd39..2a740f85aa72 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -32,6 +32,7 @@ import android.media.BluetoothProfileConnectionInfo; import android.media.FadeManagerConfiguration; import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; +import android.media.IAudioManagerNative; import android.media.IAudioModeDispatcher; import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; @@ -83,6 +84,7 @@ interface IAudioService { // When a method's argument list is changed, BpAudioManager's corresponding serialization code // (if any) in frameworks/native/services/audiomanager/IAudioManager.cpp must be updated. + IAudioManagerNative getNativeInterface(); int trackPlayer(in PlayerBase.PlayerIdCard pic); oneway void playerAttributes(in int piid, in AudioAttributes attr); diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index bbb03e77c8c9..88981eac9bb5 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -961,8 +961,7 @@ public final class MediaRoute2Info implements Parcelable { * * @hide */ - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) - public boolean isVisibleTo(String packageName) { + public boolean isVisibleTo(@NonNull String packageName) { return !mIsVisibilityRestricted || TextUtils.equals(getProviderPackageName(), packageName) || mAllowedPackages.contains(packageName); diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 3738312b762f..e57148fe5a6a 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -19,7 +19,6 @@ package android.media; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES; import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES; -import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME; import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL; import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2; import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING; @@ -1406,7 +1405,6 @@ public final class MediaRouter2 { requestCreateController(controller, route, managerRequestId); } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> getSortedRoutes( List<MediaRoute2Info> routes, List<String> packageOrder) { if (packageOrder.isEmpty()) { @@ -1427,7 +1425,6 @@ public final class MediaRouter2 { } @GuardedBy("mLock") - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> filterRoutesWithCompositePreferenceLocked( List<MediaRoute2Info> routes) { @@ -3654,7 +3651,6 @@ public final class MediaRouter2 { } } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) @Override public List<MediaRoute2Info> filterRoutesWithIndividualPreference( List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) { diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 3854747f46e0..3f18eef2f9aa 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -20,11 +20,9 @@ import static android.media.MediaRouter2.SCANNING_STATE_NOT_SCANNING; import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; -import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME; import android.Manifest; import android.annotation.CallbackExecutor; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -287,7 +285,6 @@ public final class MediaRouter2Manager { (route) -> sessionInfo.isSystemSession() ^ route.isSystemRoute()); } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> getSortedRoutes(RouteDiscoveryPreference preference) { if (!preference.shouldRemoveDuplicates()) { synchronized (mRoutesLock) { @@ -311,7 +308,6 @@ public final class MediaRouter2Manager { return routes; } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> getFilteredRoutes( @NonNull RoutingSessionInfo sessionInfo, boolean includeSelectedRoutes, diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 4398b261377b..c48b5f4e4aea 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -11,6 +11,16 @@ flag { } flag { + name: "disable_set_bluetooth_ad2p_on_calls" + namespace: "media_better_together" + description: "Prevents calls to AudioService.setBluetoothA2dpOn(), known to cause incorrect audio routing to the built-in speakers." + bug: "294968421" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_audio_input_device_routing_and_volume_control" namespace: "media_better_together" description: "Allows audio input devices routing and volume control via system settings." diff --git a/media/java/android/media/soundtrigger/SoundTriggerManager.java b/media/java/android/media/soundtrigger/SoundTriggerManager.java index 3d0c4069e782..213bc0673da6 100644 --- a/media/java/android/media/soundtrigger/SoundTriggerManager.java +++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java @@ -27,6 +27,7 @@ import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; +import android.annotation.WorkerThread; import android.app.ActivityThread; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; @@ -475,6 +476,7 @@ public final class SoundTriggerManager { @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @UnsupportedAppUsage @FlaggedApi(Flags.FLAG_MANAGER_API) + @WorkerThread public int loadSoundModel(@NonNull SoundModel soundModel) { if (mSoundTriggerSession == null) { throw new IllegalStateException("No underlying SoundTriggerModule available"); @@ -518,6 +520,7 @@ public final class SoundTriggerManager { @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @UnsupportedAppUsage @FlaggedApi(Flags.FLAG_MANAGER_API) + @WorkerThread public int startRecognition(@NonNull UUID soundModelId, @Nullable Bundle params, @NonNull ComponentName detectionService, @NonNull RecognitionConfig config) { Objects.requireNonNull(soundModelId); @@ -544,6 +547,7 @@ public final class SoundTriggerManager { @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @UnsupportedAppUsage @FlaggedApi(Flags.FLAG_MANAGER_API) + @WorkerThread public int stopRecognition(@NonNull UUID soundModelId) { if (mSoundTriggerSession == null) { throw new IllegalStateException("No underlying SoundTriggerModule available"); @@ -568,6 +572,7 @@ public final class SoundTriggerManager { @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @UnsupportedAppUsage @FlaggedApi(Flags.FLAG_MANAGER_API) + @WorkerThread public int unloadSoundModel(@NonNull UUID soundModelId) { if (mSoundTriggerSession == null) { throw new IllegalStateException("No underlying SoundTriggerModule available"); @@ -587,6 +592,7 @@ public final class SoundTriggerManager { @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @UnsupportedAppUsage @FlaggedApi(Flags.FLAG_MANAGER_API) + @WorkerThread public boolean isRecognitionActive(@NonNull UUID soundModelId) { if (soundModelId == null || mSoundTriggerSession == null) { return false; @@ -624,6 +630,7 @@ public final class SoundTriggerManager { @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @UnsupportedAppUsage @FlaggedApi(Flags.FLAG_MANAGER_API) + @WorkerThread public int getModelState(@NonNull UUID soundModelId) { if (mSoundTriggerSession == null) { throw new IllegalStateException("No underlying SoundTriggerModule available"); diff --git a/mime/Android.bp b/mime/Android.bp index 20110f1dfb47..b609548fcbab 100644 --- a/mime/Android.bp +++ b/mime/Android.bp @@ -49,6 +49,17 @@ java_library { ], } +java_library { + name: "mimemap-testing-alt", + defaults: ["mimemap-defaults"], + static_libs: ["mimemap-testing-alt-res.jar"], + jarjar_rules: "jarjar-rules-alt.txt", + visibility: [ + "//cts/tests/tests/mimemap:__subpackages__", + "//frameworks/base:__subpackages__", + ], +} + // The mimemap-res.jar and mimemap-testing-res.jar genrules produce a .jar that // has the resource file in a subdirectory res/ and testres/, respectively. // They need to be in different paths because one of them ends up in a @@ -86,6 +97,19 @@ java_genrule { cmd: "mkdir $(genDir)/testres/ && cp $(in) $(genDir)/testres/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres/", } +// The same as mimemap-testing-res.jar except that the resources are placed in a different directory. +// They get bundled with CTS so that CTS can compare a device's MimeMap implementation vs. +// the stock Android one from when CTS was built. +java_genrule { + name: "mimemap-testing-alt-res.jar", + tools: [ + "soong_zip", + ], + srcs: [":mime.types.minimized-alt"], + out: ["mimemap-testing-alt-res.jar"], + cmd: "mkdir $(genDir)/testres-alt/ && cp $(in) $(genDir)/testres-alt/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres-alt/", +} + // Combination of all *mime.types.minimized resources. filegroup { name: "mime.types.minimized", @@ -99,6 +123,19 @@ filegroup { ], } +// Combination of all *mime.types.minimized resources. +filegroup { + name: "mime.types.minimized-alt", + visibility: [ + "//visibility:private", + ], + device_common_srcs: [ + ":debian.mime.types.minimized-alt", + ":android.mime.types.minimized", + ":vendor.mime.types.minimized", + ], +} + java_genrule { name: "android.mime.types.minimized", visibility: [ diff --git a/mime/jarjar-rules-alt.txt b/mime/jarjar-rules-alt.txt new file mode 100644 index 000000000000..9a7644325336 --- /dev/null +++ b/mime/jarjar-rules-alt.txt @@ -0,0 +1 @@ +rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidAltMimeMapFactory diff --git a/mime/jarjar-rules.txt b/mime/jarjar-rules.txt index 145d1dbf3d11..e1ea8e10314c 100644 --- a/mime/jarjar-rules.txt +++ b/mime/jarjar-rules.txt @@ -1 +1 @@ -rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory
\ No newline at end of file +rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory diff --git a/native/android/tests/system_health/OWNERS b/native/android/tests/system_health/OWNERS new file mode 100644 index 000000000000..e3bbee92057d --- /dev/null +++ b/native/android/tests/system_health/OWNERS @@ -0,0 +1 @@ +include /ADPF_OWNERS diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml index afece5fac0fb..40a786ed560b 100644 --- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml +++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml @@ -81,7 +81,9 @@ <androidx.recyclerview.widget.RecyclerView android:id="@+id/device_list" android:layout_width="match_parent" - android:layout_height="200dp" + android:layout_height="wrap_content" + app:layout_constraintHeight_max="220dp" + app:layout_constraintHeight_min="200dp" android:scrollbars="vertical" android:visibility="gone" /> diff --git a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java index 068074ae1b89..8e52a00fe545 100644 --- a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java +++ b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java @@ -38,6 +38,7 @@ import android.location.provider.LocationProviderBase; import android.location.provider.ProviderProperties; import android.location.provider.ProviderRequest; import android.os.Bundle; +import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -301,8 +302,13 @@ public class FusedLocationProvider extends LocationProviderBase { .setWorkSource(mRequest.getWorkSource()) .setHiddenFromAppOps(true) .build(); - mLocationManager.requestLocationUpdates(mProvider, request, - mContext.getMainExecutor(), this); + + try { + mLocationManager.requestLocationUpdates( + mProvider, request, mContext.getMainExecutor(), this); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed to request location updates"); + } } } } @@ -311,7 +317,11 @@ public class FusedLocationProvider extends LocationProviderBase { synchronized (mLock) { int requestCode = mNextFlushCode++; mPendingFlushes.put(requestCode, callback); - mLocationManager.requestFlush(mProvider, this, requestCode); + try { + mLocationManager.requestFlush(mProvider, this, requestCode); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed to request flush"); + } } } diff --git a/packages/PrintSpooler/Android.bp b/packages/PrintSpooler/Android.bp index 6af3c6624f62..000e20fb4280 100644 --- a/packages/PrintSpooler/Android.bp +++ b/packages/PrintSpooler/Android.bp @@ -59,6 +59,21 @@ android_library { "android-support-core-ui", "android-support-fragment", "android-support-annotations", + "printspooler_aconfig_flags_java_lib", ], manifest: "AndroidManifest.xml", } + +aconfig_declarations { + name: "printspooler_aconfig_declarations", + package: "com.android.printspooler.flags", + container: "system", + srcs: [ + "flags/flags.aconfig", + ], +} + +java_aconfig_library { + name: "printspooler_aconfig_flags_java_lib", + aconfig_declarations: "printspooler_aconfig_declarations", +} diff --git a/packages/PrintSpooler/flags/flags.aconfig b/packages/PrintSpooler/flags/flags.aconfig new file mode 100644 index 000000000000..4a76dff405d0 --- /dev/null +++ b/packages/PrintSpooler/flags/flags.aconfig @@ -0,0 +1,9 @@ +package: "com.android.printspooler.flags" +container: "system" + +flag { + name: "log_print_jobs" + namespace: "printing" + description: "Log print job creation and state transitions." + bug: "385340868" +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java index bba57d5fe0a2..1a9309c13bd7 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java @@ -68,6 +68,7 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.dump.DualDumpOutputStream; import com.android.internal.util.function.pooled.PooledLambda; import com.android.printspooler.R; +import com.android.printspooler.flags.Flags; import com.android.printspooler.util.ApprovedPrintServices; import libcore.io.IoUtils; @@ -493,7 +494,7 @@ public final class PrintSpoolerService extends Service { keepAwakeLocked(); } - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[ADD] " + printJob); } } @@ -506,7 +507,7 @@ public final class PrintSpoolerService extends Service { PrintJobInfo printJob = mPrintJobs.get(i); if (isObsoleteState(printJob.getState())) { mPrintJobs.remove(i); - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[REMOVE] " + printJob.getId().flattenToString()); } removePrintJobFileLocked(printJob.getId()); @@ -568,7 +569,7 @@ public final class PrintSpoolerService extends Service { checkIfStillKeepAwakeLocked(); } - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[STATE CHANGED] " + printJob); } diff --git a/packages/SettingsLib/DataStore/OWNERS b/packages/SettingsLib/DataStore/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/DataStore/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst diff --git a/packages/SettingsLib/Graph/OWNERS b/packages/SettingsLib/Graph/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/Graph/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt index 1ed814a2ae20..51813a1c9aab 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt @@ -69,7 +69,6 @@ constructor( val visitedScreens: Set<String> = setOf(), val locale: Locale? = null, val flags: Int = PreferenceGetterFlags.ALL, - val includeValue: Boolean = true, // TODO: clean up val includeValueDescriptor: Boolean = true, ) diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt index 2fac54557bef..6fc6b5405eb2 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt @@ -22,6 +22,7 @@ import com.android.settingslib.graph.proto.PreferenceProto import com.android.settingslib.ipc.ApiDescriptor import com.android.settingslib.ipc.ApiHandler import com.android.settingslib.ipc.ApiPermissionChecker +import com.android.settingslib.metadata.PreferenceCoordinate import com.android.settingslib.metadata.PreferenceHierarchyNode import com.android.settingslib.metadata.PreferenceScreenRegistry diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt index ff14eb5aae55..70ce62c8383c 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.os.Parcel import com.android.settingslib.graph.proto.PreferenceProto import com.android.settingslib.ipc.MessageCodec +import com.android.settingslib.metadata.PreferenceCoordinate import java.util.Arrays /** Message codec for [PreferenceGetterRequest]. */ diff --git a/packages/SettingsLib/Ipc/OWNERS b/packages/SettingsLib/Ipc/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/Ipc/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst diff --git a/packages/SettingsLib/Metadata/OWNERS b/packages/SettingsLib/Metadata/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/Metadata/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt index 68aa2d258295..2dd736ae6083 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 The Android Open Source Project + * Copyright (C) 2025 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,7 +14,7 @@ * limitations under the License. */ -package com.android.settingslib.graph +package com.android.settingslib.metadata import android.os.Parcel import android.os.Parcelable diff --git a/packages/SettingsLib/OWNERS_catalyst b/packages/SettingsLib/OWNERS_catalyst new file mode 100644 index 000000000000..d44ac68585a2 --- /dev/null +++ b/packages/SettingsLib/OWNERS_catalyst @@ -0,0 +1,9 @@ +# OWNERS of Catalyst libraries (DataStore, Metadata, etc.) + +# Main developers +jiannan@google.com +cechkahn@google.com +sunnyshao@google.com + +# Emergency only +cipson@google.com diff --git a/packages/SettingsLib/Preference/OWNERS b/packages/SettingsLib/Preference/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/Preference/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst diff --git a/packages/SettingsLib/Service/OWNERS b/packages/SettingsLib/Service/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/Service/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst 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 e1e1ee5a8feb..78d6c31ac783 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 @@ -88,6 +88,7 @@ class AppListRepositoryImpl( matchAnyUserForAdmin: Boolean, ): List<ApplicationInfo> = try { coroutineScope { + // TODO(b/382016780): to be removed after flag cleanup. val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() } val hideWhenDisabledPackagesDeferred = async { context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames) @@ -95,6 +96,7 @@ class AppListRepositoryImpl( val installedApplicationsAsUser = getInstalledApplications(userId, matchAnyUserForAdmin) + // TODO(b/382016780): to be removed after flag cleanup. val hiddenSystemModules = hiddenSystemModulesDeferred.await() val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await() installedApplicationsAsUser.filter { app -> @@ -206,6 +208,7 @@ class AppListRepositoryImpl( private fun isSystemApp(app: ApplicationInfo, homeOrLauncherPackages: Set<String>): Boolean = app.isSystemApp && !app.isUpdatedSystemApp && app.packageName !in homeOrLauncherPackages + // TODO(b/382016780): to be removed after flag cleanup. private fun PackageManager.getHiddenSystemModules(): Set<String> { val moduleInfos = getInstalledModules(0).filter { it.isHidden } val hiddenApps = moduleInfos.mapNotNull { it.packageName }.toMutableSet() @@ -218,13 +221,14 @@ class AppListRepositoryImpl( companion object { private const val TAG = "AppListRepository" + // TODO(b/382016780): to be removed after flag cleanup. private fun ApplicationInfo.isInAppList( showInstantApps: Boolean, hiddenSystemModules: Set<String>, hideWhenDisabledPackages: Array<String>, ) = when { !showInstantApps && isInstantApp -> false - packageName in hiddenSystemModules -> false + !Flags.removeHiddenModuleUsage() && (packageName in hiddenSystemModules) -> false packageName in hideWhenDisabledPackages -> enabled && !isDisabledUntilUsed enabled -> true else -> enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER 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 b1baa8601f28..fd4b189c51ff 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 @@ -281,6 +281,23 @@ class AppListRepositoryTest { ) } + @EnableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE) + @Test + fun loadApps_shouldIncludeAllSystemModuleApps() = runTest { + 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, HIDDEN_MODULE_APP) + } + + @DisableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE) @EnableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX) @Test fun loadApps_hasApkInApexInfo_shouldNotIncludeAllHiddenApps() = runTest { @@ -297,7 +314,7 @@ class AppListRepositoryTest { assertThat(appList).containsExactly(NORMAL_APP) } - @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX) + @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX, Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE) @Test fun loadApps_noApkInApexInfo_shouldNotIncludeHiddenSystemModule() = runTest { packageManager.stub { @@ -456,6 +473,7 @@ class AppListRepositoryTest { isArchived = true } + // TODO(b/382016780): to be removed after flag cleanup. val HIDDEN_APEX_APP = ApplicationInfo().apply { packageName = "hidden.apex.package" } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java index c4829951d61a..3390296ef6fc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java @@ -137,6 +137,7 @@ public class AppUtils { /** * Returns a boolean indicating whether the given package is a hidden system module + * TODO(b/382016780): to be removed after flag cleanup. */ public static boolean isHiddenSystemModule(Context context, String packageName) { return ApplicationsState.getInstance((Application) context.getApplicationContext()) diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index fd9a008ee078..4110d536da61 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -157,6 +157,7 @@ public class ApplicationsState { int mCurComputingSizeUserId; boolean mSessionsChanged; // Maps all installed modules on the system to whether they're hidden or not. + // TODO(b/382016780): to be removed after flag cleanup. final HashMap<String, Boolean> mSystemModules = new HashMap<>(); // Temporary for dispatching session callbacks. Only touched by main thread. @@ -226,12 +227,14 @@ public class ApplicationsState { mRetrieveFlags = PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; - 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()); + if (!Flags.removeHiddenModuleUsage()) { + 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()); + } } } } @@ -336,7 +339,7 @@ public class ApplicationsState { } mHaveDisabledApps = true; } - if (isHiddenModule(info.packageName)) { + if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) { mApplications.remove(i--); continue; } @@ -453,6 +456,7 @@ public class ApplicationsState { return mHaveInstantApps; } + // TODO(b/382016780): to be removed after flag cleanup. boolean isHiddenModule(String packageName) { Boolean isHidden = mSystemModules.get(packageName); if (isHidden == null) { @@ -462,6 +466,7 @@ public class ApplicationsState { return isHidden; } + // TODO(b/382016780): to be removed after flag cleanup. boolean isSystemModule(String packageName) { return mSystemModules.containsKey(packageName); } @@ -755,7 +760,7 @@ public class ApplicationsState { Log.i(TAG, "Looking up entry of pkg " + info.packageName + ": " + entry); } if (entry == null) { - if (isHiddenModule(info.packageName)) { + if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) { if (DEBUG) { Log.i(TAG, "No AppEntry for " + info.packageName + " (hidden module)"); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 145b62cd12b5..68e9fe703090 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -73,6 +73,10 @@ public class BluetoothUtils { private static final Set<Integer> SA_PROFILES = ImmutableSet.of( BluetoothProfile.A2DP, BluetoothProfile.LE_AUDIO, BluetoothProfile.HEARING_AID); + private static final List<Integer> BLUETOOTH_DEVICE_CLASS_HEADSET = + List.of( + BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES, + BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET); private static final String TEMP_BOND_TYPE = "TEMP_BOND_TYPE"; private static final String TEMP_BOND_DEVICE_METADATA_VALUE = "le_audio_sharing"; @@ -390,6 +394,19 @@ public class BluetoothUtils { return false; } + /** Checks whether the bluetooth device is a headset. */ + public static boolean isHeadset(@NonNull BluetoothDevice bluetoothDevice) { + String deviceType = + BluetoothUtils.getStringMetaData( + bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE); + if (!TextUtils.isEmpty(deviceType)) { + return BluetoothDevice.DEVICE_TYPE_HEADSET.equals(deviceType) + || BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.equals(deviceType); + } + BluetoothClass btClass = bluetoothDevice.getBluetoothClass(); + return btClass != null && BLUETOOTH_DEVICE_CLASS_HEADSET.contains(btClass.getDeviceClass()); + } + /** Create an Icon pointing to a drawable. */ public static IconCompat createIconWithDrawable(Drawable drawable) { Bitmap bitmap; diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java index b2c279466ee4..e05f0a1bcde0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -483,14 +483,18 @@ public class HearingAidDeviceManager { void onActiveDeviceChanged(CachedBluetoothDevice device) { if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) { - if (device.isConnectedHearingAidDevice()) { + if (device.isConnectedHearingAidDevice() + && (device.isActiveDevice(BluetoothProfile.HEARING_AID) + || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) { setAudioRoutingConfig(device); } else { clearAudioRoutingConfig(); } } if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { - if (device.isConnectedHearingAidDevice()) { + if (device.isConnectedHearingAidDevice() + && (device.isActiveDevice(BluetoothProfile.HEARING_AID) + || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) { setMicrophoneForCalls(device); } else { clearMicrophoneForCalls(); diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java index dc40304ba24a..51259e2f311d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java @@ -16,6 +16,8 @@ package com.android.settingslib.dream; +import static android.service.dreams.Flags.allowDreamWhenPostured; + import android.annotation.IntDef; import android.content.ComponentName; import android.content.Context; @@ -78,14 +80,21 @@ public class DreamBackend { } @Retention(RetentionPolicy.SOURCE) - @IntDef({WHILE_CHARGING, WHILE_DOCKED, EITHER, NEVER}) + @IntDef({ + WHILE_CHARGING, + WHILE_DOCKED, + WHILE_POSTURED, + WHILE_CHARGING_OR_DOCKED, + NEVER + }) public @interface WhenToDream { } public static final int WHILE_CHARGING = 0; public static final int WHILE_DOCKED = 1; - public static final int EITHER = 2; - public static final int NEVER = 3; + public static final int WHILE_POSTURED = 2; + public static final int WHILE_CHARGING_OR_DOCKED = 3; + public static final int NEVER = 4; /** * The type of dream complications which can be provided by a @@ -134,6 +143,8 @@ public class DreamBackend { .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_CHARGING_ONLY; private static final int WHEN_TO_DREAM_DOCKED = FrameworkStatsLog .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_DOCKED_ONLY; + private static final int WHEN_TO_DREAM_POSTURED = FrameworkStatsLog + .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_POSTURED_ONLY; private static final int WHEN_TO_DREAM_CHARGING_OR_DOCKED = FrameworkStatsLog .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_EITHER_CHARGING_OR_DOCKED; @@ -143,6 +154,7 @@ public class DreamBackend { private final boolean mDreamsEnabledByDefault; private final boolean mDreamsActivatedOnSleepByDefault; private final boolean mDreamsActivatedOnDockByDefault; + private final boolean mDreamsActivatedOnPosturedByDefault; private final Set<ComponentName> mDisabledDreams; private final List<String> mLoggableDreamPrefixes; private Set<Integer> mSupportedComplications; @@ -168,6 +180,8 @@ public class DreamBackend { com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault); mDreamsActivatedOnDockByDefault = resources.getBoolean( com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault); + mDreamsActivatedOnPosturedByDefault = resources.getBoolean( + com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault); mDisabledDreams = Arrays.stream(resources.getStringArray( com.android.internal.R.array.config_disabledDreamComponents)) .map(ComponentName::unflattenFromString) @@ -280,10 +294,11 @@ public class DreamBackend { @WhenToDream public int getWhenToDreamSetting() { - return isActivatedOnDock() && isActivatedOnSleep() ? EITHER + return isActivatedOnDock() && isActivatedOnSleep() ? WHILE_CHARGING_OR_DOCKED : isActivatedOnDock() ? WHILE_DOCKED - : isActivatedOnSleep() ? WHILE_CHARGING - : NEVER; + : isActivatedOnPostured() ? WHILE_POSTURED + : isActivatedOnSleep() ? WHILE_CHARGING + : NEVER; } public void setWhenToDream(@WhenToDream int whenToDream) { @@ -293,16 +308,25 @@ public class DreamBackend { case WHILE_CHARGING: setActivatedOnDock(false); setActivatedOnSleep(true); + setActivatedOnPostured(false); break; case WHILE_DOCKED: setActivatedOnDock(true); setActivatedOnSleep(false); + setActivatedOnPostured(false); break; - case EITHER: + case WHILE_CHARGING_OR_DOCKED: setActivatedOnDock(true); setActivatedOnSleep(true); + setActivatedOnPostured(false); + break; + + case WHILE_POSTURED: + setActivatedOnPostured(true); + setActivatedOnSleep(false); + setActivatedOnDock(false); break; case NEVER: @@ -407,6 +431,22 @@ public class DreamBackend { setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, value); } + public boolean isActivatedOnPostured() { + return allowDreamWhenPostured() + && getBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + mDreamsActivatedOnPosturedByDefault); + } + + /** + * Sets whether dreams should be activated when the device is postured (stationary and upright) + */ + public void setActivatedOnPostured(boolean value) { + if (allowDreamWhenPostured()) { + logd("setActivatedOnPostured(%s)", value); + setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, value); + } + } + private boolean getBoolean(String key, boolean def) { return Settings.Secure.getInt(mContext.getContentResolver(), key, def ? 1 : 0) == 1; } @@ -548,7 +588,9 @@ public class DreamBackend { return WHEN_TO_DREAM_CHARGING; case WHILE_DOCKED: return WHEN_TO_DREAM_DOCKED; - case EITHER: + case WHILE_POSTURED: + return WHEN_TO_DREAM_POSTURED; + case WHILE_CHARGING_OR_DOCKED: return WHEN_TO_DREAM_CHARGING_OR_DOCKED; case NEVER: default: diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java index 7516d2e6ab1b..e3d7902f34b2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java +++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java @@ -22,6 +22,7 @@ import static com.android.settingslib.enterprise.ActionDisabledLearnMoreButtonLa import static com.android.settingslib.enterprise.ManagedDeviceActionDisabledByAdminController.DEFAULT_FOREGROUND_USER_CHECKER; import android.app.admin.DevicePolicyManager; +import android.app.supervision.SupervisionManager; import android.content.ComponentName; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; @@ -59,12 +60,18 @@ public final class ActionDisabledByAdminControllerFactory { } private static boolean isSupervisedDevice(Context context) { - DevicePolicyManager devicePolicyManager = - context.getSystemService(DevicePolicyManager.class); - ComponentName supervisionComponent = - devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent( - new UserHandle(UserHandle.myUserId())); - return supervisionComponent != null; + if (android.app.supervision.flags.Flags.deprecateDpmSupervisionApis()) { + SupervisionManager supervisionManager = + context.getSystemService(SupervisionManager.class); + return supervisionManager.isSupervisionEnabledForUser(UserHandle.myUserId()); + } else { + DevicePolicyManager devicePolicyManager = + context.getSystemService(DevicePolicyManager.class); + ComponentName supervisionComponent = + devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent( + new UserHandle(UserHandle.myUserId())); + return supervisionComponent != null; + } } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 496c3e6c74cc..9aaefe47fda2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -37,7 +37,8 @@ class FakeZenModeRepository : ZenModeRepository { override val globalZenMode: StateFlow<Int> get() = mutableZenMode.asStateFlow() - private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf()) + private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = + MutableStateFlow(listOf(TestModeBuilder.MANUAL_DND)) override val modes: Flow<List<ZenMode>> get() = mutableModesFlow.asStateFlow() @@ -65,8 +66,11 @@ class FakeZenModeRepository : ZenModeRepository { mutableModesFlow.value += mode } - fun addMode(id: String, @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN, - active: Boolean = false) { + fun addMode( + id: String, + @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN, + active: Boolean = false, + ) { mutableModesFlow.value += newMode(id, type, active) } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index abc163867248..64a2de5025de 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -31,7 +31,6 @@ import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Random; @@ -44,22 +43,7 @@ public class TestModeBuilder { private boolean mIsManualDnd; public static final ZenMode EXAMPLE = new TestModeBuilder().build(); - - public static final ZenMode MANUAL_DND_ACTIVE = manualDnd( - INTERRUPTION_FILTER_PRIORITY, true); - - public static final ZenMode MANUAL_DND_INACTIVE = manualDnd( - INTERRUPTION_FILTER_PRIORITY, false); - - @NonNull - public static ZenMode manualDnd(@NotificationManager.InterruptionFilter int filter, - boolean isActive) { - return new TestModeBuilder() - .makeManualDnd() - .setInterruptionFilter(filter) - .setActive(isActive) - .build(); - } + public static final ZenMode MANUAL_DND = new TestModeBuilder().makeManualDnd().build(); public TestModeBuilder() { // Reasonable defaults 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 3b18aa310c91..4e821ca50dce 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_REMOVE_HIDDEN_MODULE_USAGE; 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; @@ -59,6 +60,8 @@ import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.text.TextUtils; import android.util.IconDrawableFactory; @@ -204,6 +207,7 @@ public class ApplicationsStateRoboTest { info.setPackageName(packageName); info.setApkInApexPackageNames(Collections.singletonList(apexPackageName)); // will treat any app with package name that contains "hidden" as hidden module + // TODO(b/382016780): to be removed after flag cleanup. info.setHidden(!TextUtils.isEmpty(packageName) && packageName.contains("hidden")); return info; } @@ -414,6 +418,7 @@ public class ApplicationsStateRoboTest { } @Test + @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) public void onResume_shouldNotIncludeSystemHiddenModule() { mSession.onResume(); @@ -424,6 +429,18 @@ public class ApplicationsStateRoboTest { } @Test + @EnableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) + public void onResume_shouldIncludeSystemModule() { + mSession.onResume(); + + final List<ApplicationInfo> mApplications = mApplicationsState.mApplications; + assertThat(mApplications).hasSize(3); + assertThat(mApplications.get(0).packageName).isEqualTo("test.package.1"); + assertThat(mApplications.get(1).packageName).isEqualTo("test.hidden.module.2"); + assertThat(mApplications.get(2).packageName).isEqualTo("test.package.3"); + } + + @Test public void removeAndInstall_noWorkprofile_doResumeIfNeededLocked_shouldClearEntries() throws RemoteException { // scenario: only owner user @@ -832,6 +849,7 @@ public class ApplicationsStateRoboTest { mApplicationsState.mEntriesMap.clear(); ApplicationInfo appInfo = createApplicationInfo(PKG_1, /* uid= */ 0); mApplicationsState.mApplications.add(appInfo); + // TODO(b/382016780): to be removed after flag cleanup. mApplicationsState.mSystemModules.put(PKG_1, /* value= */ false); assertThat(mApplicationsState.getEntry(PKG_1, /* userId= */ 0).info.packageName) @@ -839,6 +857,7 @@ public class ApplicationsStateRoboTest { } @Test + @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) public void isHiddenModule_hasApkInApexInfo_shouldSupportHiddenApexPackage() { mSetFlagsRule.enableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX); ApplicationsState.sInstance = null; @@ -853,6 +872,7 @@ public class ApplicationsStateRoboTest { } @Test + @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) public void isHiddenModule_noApkInApexInfo_onlySupportHiddenModule() { mSetFlagsRule.disableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX); ApplicationsState.sInstance = null; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index d49447f05011..cafe19ff9a9b 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -80,7 +80,9 @@ public class BluetoothUtilsTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private CachedBluetoothDevice mCachedBluetoothDevice; - @Mock private BluetoothDevice mBluetoothDevice; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private BluetoothDevice mBluetoothDevice; + @Mock private AudioManager mAudioManager; @Mock private PackageManager mPackageManager; @Mock private LeAudioProfile mA2dpProfile; @@ -399,6 +401,38 @@ public class BluetoothUtilsTest { } @Test + public void isHeadset_metadataMatched_returnTrue() { + when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE)) + .thenReturn(BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.getBytes()); + + assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isTrue(); + } + + @Test + public void isHeadset_metadataNotMatched_returnFalse() { + when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE)) + .thenReturn(BluetoothDevice.DEVICE_TYPE_CARKIT.getBytes()); + + assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isFalse(); + } + + @Test + public void isHeadset_btClassMatched_returnTrue() { + when(mBluetoothDevice.getBluetoothClass().getDeviceClass()) + .thenReturn(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES); + + assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isTrue(); + } + + @Test + public void isHeadset_btClassNotMatched_returnFalse() { + when(mBluetoothDevice.getBluetoothClass().getDeviceClass()) + .thenReturn(BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER); + + assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isFalse(); + } + + @Test public void isAvailableMediaBluetoothDevice_isConnectedLeAudioDevice_returnTrue() { when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java index 21dde1fd9411..a215464f66c2 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java @@ -50,6 +50,9 @@ import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.audiopolicy.AudioProductStrategy; import android.os.Parcel; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.FeatureFlagUtils; import androidx.test.core.app.ApplicationProvider; @@ -72,6 +75,8 @@ import java.util.List; public class HearingAidDeviceManagerTest { @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final long HISYNCID1 = 10; private static final long HISYNCID2 = 11; @@ -736,6 +741,7 @@ public class HearingAidDeviceManagerTest { @Test public void onActiveDeviceChanged_connected_callSetStrategies() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true); when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn( mHearingDeviceAttribute); when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); @@ -750,6 +756,7 @@ public class HearingAidDeviceManagerTest { @Test public void onActiveDeviceChanged_disconnected_callSetStrategiesWithAutoValue() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(false); when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn( mHearingDeviceAttribute); when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false); @@ -952,6 +959,38 @@ public class HearingAidDeviceManagerTest { ConnectionStatus.CONNECTED); } + @Test + @RequiresFlagsEnabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL) + public void onActiveDeviceChanged_activeHearingAidProfile_callSetInputDeviceForCalls() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); + when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true); + doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(), + anyInt()); + + mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1); + + verify(mHelper).setPreferredInputDeviceForCalls( + eq(mCachedDevice1), eq(HearingAidAudioRoutingConstants.RoutingValue.AUTO)); + + } + + @Test + @RequiresFlagsEnabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL) + public void onActiveDeviceChanged_notActiveHearingAidProfile_callClearInputDeviceForCalls() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false); + when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true); + doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(), + anyInt()); + + mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1); + + verify(mHelper).clearPreferredInputDeviceForCalls(); + } + private HearingAidInfo getLeftAshaHearingAidInfo(long hiSyncId) { return new HearingAidInfo.Builder() .setAshaDeviceSide(HearingAidInfo.DeviceSide.SIDE_LEFT) diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java index d08d91d18b27..6b30f159129e 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java @@ -87,7 +87,7 @@ public class ZenModeTest { @Test public void testBasicMethods_manualDnd() { - ZenMode manualMode = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualMode = TestModeBuilder.MANUAL_DND; assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID); assertThat(manualMode.isManualDnd()).isTrue(); @@ -271,7 +271,7 @@ public class ZenModeTest { @Test public void setInterruptionFilter_manualDnd_throws() { - ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualDnd = TestModeBuilder.MANUAL_DND; assertThrows(IllegalStateException.class, () -> manualDnd.setInterruptionFilter(INTERRUPTION_FILTER_ALL)); @@ -280,24 +280,46 @@ public class ZenModeTest { @Test public void canEditPolicy_onlyFalseForSpecialDnd() { assertThat(TestModeBuilder.EXAMPLE.canEditPolicy()).isTrue(); - assertThat(TestModeBuilder.MANUAL_DND_ACTIVE.canEditPolicy()).isTrue(); - assertThat(TestModeBuilder.MANUAL_DND_INACTIVE.canEditPolicy()).isTrue(); - ZenMode dndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true); + ZenMode inactiveDnd = new TestModeBuilder().makeManualDnd().setActive(false).build(); + assertThat(inactiveDnd.canEditPolicy()).isTrue(); + + ZenMode activeDnd = new TestModeBuilder().makeManualDnd().setActive(true).build(); + assertThat(activeDnd.canEditPolicy()).isTrue(); + + ZenMode dndWithAlarms = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(true) + .build(); assertThat(dndWithAlarms.canEditPolicy()).isFalse(); - ZenMode dndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, true); + + ZenMode dndWithNone = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(true) + .build(); assertThat(dndWithNone.canEditPolicy()).isFalse(); // Note: Backend will never return an inactive manual mode with custom filter. - ZenMode badDndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, false); + ZenMode badDndWithAlarms = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(false) + .build(); assertThat(badDndWithAlarms.canEditPolicy()).isFalse(); - ZenMode badDndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, false); + + ZenMode badDndWithNone = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(false) + .build(); assertThat(badDndWithNone.canEditPolicy()).isFalse(); } @Test public void canEditPolicy_whenTrue_allowsSettingPolicyAndEffects() { - ZenMode normalDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_PRIORITY, true); + ZenMode normalDnd = new TestModeBuilder().makeManualDnd().setActive(true).build(); assertThat(normalDnd.canEditPolicy()).isTrue(); @@ -313,7 +335,11 @@ public class ZenModeTest { @Test public void canEditPolicy_whenFalse_preventsSettingFilterPolicyOrEffects() { - ZenMode specialDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true); + ZenMode specialDnd = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(true) + .build(); assertThat(specialDnd.canEditPolicy()).isFalse(); assertThrows(IllegalStateException.class, @@ -324,7 +350,7 @@ public class ZenModeTest { @Test public void comparator_prioritizes() { - ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualDnd = TestModeBuilder.MANUAL_DND; ZenMode driving1 = new TestModeBuilder().setName("b1").setType(TYPE_DRIVING).build(); ZenMode driving2 = new TestModeBuilder().setName("b2").setType(TYPE_DRIVING).build(); ZenMode bedtime1 = new TestModeBuilder().setName("c1").setType(TYPE_BEDTIME).build(); @@ -403,7 +429,7 @@ public class ZenModeTest { @Test public void getIconKey_manualDnd_isDndIcon() { - ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND_INACTIVE.getIconKey(); + ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND.getIconKey(); assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 5ddf005d9468..dafcc729b8f1 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -322,9 +322,6 @@ <!-- Whether vibrate icon is shown in the status bar by default. --> <integer name="def_statusBarVibrateIconEnabled">0</integer> - <!-- Whether predictive back animation is enabled by default. --> - <bool name="def_enable_back_animation">false</bool> - <!-- Whether wifi is always requested by default. --> <bool name="def_enable_wifi_always_requested">false</bool> diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java index 4125a81f9bbc..fc61b1e875f3 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java @@ -46,6 +46,7 @@ public class GlobalSettings { Settings.Global.APP_AUTO_RESTRICTION_ENABLED, Settings.Global.AUTO_TIME, Settings.Global.AUTO_TIME_ZONE, + Settings.Global.TIME_ZONE_NOTIFICATIONS, Settings.Global.POWER_SOUNDS_ENABLED, Settings.Global.DOCK_SOUNDS_ENABLED, Settings.Global.CHARGING_SOUNDS_ENABLED, diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 1fc1f05ae149..7b4a2ca5de39 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -91,6 +91,7 @@ public class SecureSettings { Settings.Secure.KEY_REPEAT_TIMEOUT_MS, Settings.Secure.KEY_REPEAT_DELAY_MS, Settings.Secure.CAMERA_GESTURE_DISABLED, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE, Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY, Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON, @@ -154,6 +155,7 @@ public class SecureSettings { Settings.Secure.SCREENSAVER_COMPONENTS, Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION, Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index 5b4ee8bdb339..1f56f10cca7d 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -109,6 +109,7 @@ public class SystemSettings { Settings.System.LOCALE_PREFERENCES, Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING, Settings.System.MOUSE_SCROLLING_ACCELERATION, + Settings.System.MOUSE_SCROLLING_SPEED, Settings.System.MOUSE_SWAP_PRIMARY_BUTTON, Settings.System.MOUSE_POINTER_ACCELERATION_ENABLED, Settings.System.TOUCHPAD_POINTER_SPEED, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index 32d4580f67ec..c0e266fa269f 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -102,6 +102,7 @@ public class GlobalSettingsValidators { }); VALIDATORS.put(Global.AUTO_TIME, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.AUTO_TIME_ZONE, BOOLEAN_VALIDATOR); + VALIDATORS.put(Global.TIME_ZONE_NOTIFICATIONS, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.POWER_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.DOCK_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.CHARGING_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index d0e88d5d6a3c..b0309a8fa5a5 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -140,6 +140,8 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.KEY_REPEAT_TIMEOUT_MS, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.KEY_REPEAT_DELAY_MS, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.CAMERA_GESTURE_DISABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put( + Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_AUTOCLICK_DELAY, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_LARGE_POINTER_ICON, BOOLEAN_VALIDATOR); @@ -228,6 +230,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.SCREENSAVER_COMPONENTS, COMMA_SEPARATED_COMPONENT_LIST_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_DOCK, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, NON_NEGATIVE_INTEGER_VALIDATOR); diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index 0432eeacec4d..4d98a11bdfe7 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -227,6 +227,7 @@ public class SystemSettingsValidators { VALIDATORS.put(System.MOUSE_SWAP_PRIMARY_BUTTON, BOOLEAN_VALIDATOR); VALIDATORS.put(System.MOUSE_SCROLLING_ACCELERATION, BOOLEAN_VALIDATOR); VALIDATORS.put(System.MOUSE_POINTER_ACCELERATION_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(System.MOUSE_SCROLLING_SPEED, new InclusiveIntegerRangeValidator(-7, 7)); VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7)); VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR); VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java index a2cc008843a4..c1c3e04d46fd 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java @@ -193,6 +193,7 @@ public class SettingsBackupAgent extends BackupAgentHelper { "power_button_instantly_locks"; private static final String KEY_LOCK_SETTINGS_PIN_ENHANCED_PRIVACY = "pin_enhanced_privacy"; + private static final int NUM_LOCK_SETTINGS = 5; // Error messages for logging metrics. private static final String ERROR_COULD_NOT_READ_FROM_CURSOR = @@ -208,6 +209,11 @@ public class SettingsBackupAgent extends BackupAgentHelper { private static final String ERROR_SKIPPED_DUE_TO_LARGE_SCREEN = "skipped_due_to_large_screen"; private static final String ERROR_DID_NOT_PASS_VALIDATION = "did_not_pass_validation"; + private static final String ERROR_IO_EXCEPTION = "io_exception"; + private static final String ERROR_FAILED_TO_RESTORE_SOFTAP_CONFIG = + "failed_to_restore_softap_config"; + private static final String ERROR_FAILED_TO_RESTORE_WIFI_CONFIG = + "failed_to_restore_wifi_config"; // Name of the temporary file we use during full backup/restore. This is @@ -794,29 +800,44 @@ public class SettingsBackupAgent extends BackupAgentHelper { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(baos); + int backedUpSettingsCount = 0; try { out.writeUTF(KEY_LOCK_SETTINGS_OWNER_INFO_ENABLED); out.writeUTF(ownerInfoEnabled ? "1" : "0"); + backedUpSettingsCount++; if (ownerInfo != null) { out.writeUTF(KEY_LOCK_SETTINGS_OWNER_INFO); out.writeUTF(ownerInfo != null ? ownerInfo : ""); + backedUpSettingsCount++; } if (lockPatternUtils.isVisiblePatternEverChosen(userId)) { out.writeUTF(KEY_LOCK_SETTINGS_VISIBLE_PATTERN_ENABLED); out.writeUTF(visiblePatternEnabled ? "1" : "0"); + backedUpSettingsCount++; } if (lockPatternUtils.isPowerButtonInstantlyLocksEverChosen(userId)) { out.writeUTF(KEY_LOCK_SETTINGS_POWER_BUTTON_INSTANTLY_LOCKS); out.writeUTF(powerButtonInstantlyLocks ? "1" : "0"); + backedUpSettingsCount++; } if (lockPatternUtils.isPinEnhancedPrivacyEverChosen(userId)) { out.writeUTF(KEY_LOCK_SETTINGS_PIN_ENHANCED_PRIVACY); out.writeUTF(lockPatternUtils.isPinEnhancedPrivacyEnabled(userId) ? "1" : "0"); + backedUpSettingsCount++; } // End marker out.writeUTF(""); out.flush(); + if (areAgentMetricsEnabled) { + numberOfSettingsPerKey.put(KEY_LOCK_SETTINGS, backedUpSettingsCount); + } } catch (IOException ioe) { + if (areAgentMetricsEnabled) { + mBackupRestoreEventLogger.logItemsBackupFailed( + KEY_LOCK_SETTINGS, + NUM_LOCK_SETTINGS - backedUpSettingsCount, + ERROR_IO_EXCEPTION); + } } return baos.toByteArray(); } @@ -1162,6 +1183,7 @@ public class SettingsBackupAgent extends BackupAgentHelper { ByteArrayInputStream bais = new ByteArrayInputStream(buffer, 0, nBytes); DataInputStream in = new DataInputStream(bais); + int restoredLockSettingsCount = 0; try { String key; // Read until empty string marker @@ -1187,9 +1209,20 @@ public class SettingsBackupAgent extends BackupAgentHelper { lockPatternUtils.setPinEnhancedPrivacyEnabled("1".equals(value), userId); break; } + if (areAgentMetricsEnabled) { + mBackupRestoreEventLogger.logItemsRestored(KEY_LOCK_SETTINGS, /* count= */ 1); + restoredLockSettingsCount++; + } + } in.close(); } catch (IOException ioe) { + if (areAgentMetricsEnabled) { + mBackupRestoreEventLogger.logItemsRestoreFailed( + KEY_LOCK_SETTINGS, + NUM_LOCK_SETTINGS - restoredLockSettingsCount, + ERROR_IO_EXCEPTION); + } } } @@ -1309,12 +1342,31 @@ public class SettingsBackupAgent extends BackupAgentHelper { mWifiManager.restoreSupplicantBackupData(supplicant_bytes, ipconfig_bytes); } - private byte[] getSoftAPConfiguration() { - return mWifiManager.retrieveSoftApBackupData(); + @VisibleForTesting + byte[] getSoftAPConfiguration() { + byte[] data = mWifiManager.retrieveSoftApBackupData(); + if (areAgentMetricsEnabled) { + // We're unable to determine how many settings this includes, so we'll just log 1. + numberOfSettingsPerKey.put(KEY_SOFTAP_CONFIG, 1); + } + return data; } - private void restoreSoftApConfiguration(byte[] data) { - SoftApConfiguration configInCloud = mWifiManager.restoreSoftApBackupData(data); + @VisibleForTesting + void restoreSoftApConfiguration(byte[] data) { + SoftApConfiguration configInCloud; + if (areAgentMetricsEnabled) { + try { + configInCloud = mWifiManager.restoreSoftApBackupData(data); + mBackupRestoreEventLogger.logItemsRestored(KEY_SOFTAP_CONFIG, /* count= */ 1); + } catch (Exception e) { + configInCloud = null; + mBackupRestoreEventLogger.logItemsRestoreFailed( + KEY_SOFTAP_CONFIG, /* count= */ 1, ERROR_FAILED_TO_RESTORE_SOFTAP_CONFIG); + } + } else { + configInCloud = mWifiManager.restoreSoftApBackupData(data); + } if (configInCloud != null) { if (DEBUG) Log.d(TAG, "Successfully unMarshaled SoftApConfiguration "); // Depending on device hardware, we may need to notify the user of a setting change @@ -1405,8 +1457,14 @@ public class SettingsBackupAgent extends BackupAgentHelper { return baos.toByteArray(); } - private byte[] getNewWifiConfigData() { - return mWifiManager.retrieveBackupData(); + @VisibleForTesting + byte[] getNewWifiConfigData() { + byte[] data = mWifiManager.retrieveBackupData(); + if (areAgentMetricsEnabled) { + // We're unable to determine how many settings this includes, so we'll just log 1. + numberOfSettingsPerKey.put(KEY_WIFI_NEW_CONFIG, 1); + } + return data; } private byte[] getLocaleSettings() { @@ -1418,11 +1476,22 @@ public class SettingsBackupAgent extends BackupAgentHelper { return localeList.toLanguageTags().getBytes(); } - private void restoreNewWifiConfigData(byte[] bytes) { + @VisibleForTesting + void restoreNewWifiConfigData(byte[] bytes) { if (DEBUG_BACKUP) { Log.v(TAG, "Applying restored wifi data"); } - mWifiManager.restoreBackupData(bytes); + if (areAgentMetricsEnabled) { + try { + mWifiManager.restoreBackupData(bytes); + mBackupRestoreEventLogger.logItemsRestored(KEY_WIFI_NEW_CONFIG, /* count= */ 1); + } catch (Exception e) { + mBackupRestoreEventLogger.logItemsRestoreFailed( + KEY_WIFI_NEW_CONFIG, /* count= */ 1, ERROR_FAILED_TO_RESTORE_WIFI_CONFIG); + } + } else { + mWifiManager.restoreBackupData(bytes); + } } private void restoreNetworkPolicies(byte[] data) { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index dedd7ebd1ef7..1c6d6816e9b4 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1715,6 +1715,9 @@ class SettingsProtoDumpUtil { Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, SecureSettingsProto.Accessibility.ENABLED_ACCESSIBILITY_SERVICES); dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE, + SecureSettingsProto.Accessibility.AUTOCLICK_CURSOR_AREA_SIZE); + dumpSetting(s, p, Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, SecureSettingsProto.Accessibility.AUTOCLICK_ENABLED); dumpSetting(s, p, @@ -2547,6 +2550,9 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.Secure.SCREENSAVER_DEFAULT_COMPONENT, SecureSettingsProto.Screensaver.DEFAULT_COMPONENT); + dumpSetting(s, p, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + SecureSettingsProto.Screensaver.ACTIVATE_ON_POSTURED); p.end(screensaverToken); final long searchToken = p.start(SecureSettingsProto.SEARCH); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index ed193515b382..cb656bdd5d54 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -6122,17 +6122,7 @@ public class SettingsProvider extends ContentProvider { } if (currentVersion == 220) { - final SettingsState globalSettings = getGlobalSettingsLocked(); - final Setting enableBackAnimation = - globalSettings.getSettingLocked(Global.ENABLE_BACK_ANIMATION); - if (enableBackAnimation.isNull()) { - final boolean defEnableBackAnimation = - getContext() - .getResources() - .getBoolean(R.bool.def_enable_back_animation); - initGlobalSettingsDefaultValLocked( - Settings.Global.ENABLE_BACK_ANIMATION, defEnableBackAnimation); - } + // Version 221: Removed currentVersion = 221; } diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index c88a7fd834d6..cbdb36fff98c 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -564,7 +564,6 @@ public class SettingsBackupTest { Settings.Global.WATCHDOG_TIMEOUT_MILLIS, Settings.Global.MANAGED_PROVISIONING_DEFER_PROVISIONING_TO_ROLE_HOLDER, Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, - Settings.Global.ENABLE_BACK_ANIMATION, // Temporary for T, dev option only Settings.Global.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME, // cache per hearing device Settings.Global.HEARING_DEVICE_LOCAL_NOTIFICATION, // cache per hearing device Settings.Global.Wearable.COMBINED_LOCATION_ENABLE, diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java index 18c43a704bcc..6e5b602c02c5 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java @@ -16,6 +16,9 @@ package com.android.providers.settings; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_WIFI_NEW_CONFIG; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_SOFTAP_CONFIG; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; @@ -26,8 +29,11 @@ import static org.junit.Assert.assertArrayEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; +import android.annotation.Nullable; import android.app.backup.BackupAnnotations.BackupDestination; import android.app.backup.BackupAnnotations.OperationType; import android.app.backup.BackupDataInput; @@ -42,6 +48,8 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; +import android.net.wifi.SoftApConfiguration; +import android.net.wifi.WifiManager; import android.os.Build; import android.os.Bundle; import android.os.UserHandle; @@ -64,6 +72,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -126,6 +135,7 @@ public class SettingsBackupAgentTest extends BaseSettingsProviderTest { @Mock private BackupDataInput mBackupDataInput; @Mock private BackupDataOutput mBackupDataOutput; + @Mock private static WifiManager mWifiManager; private TestFriendlySettingsBackupAgent mAgentUnderTest; private Context mContext; @@ -754,6 +764,148 @@ public class SettingsBackupAgentTest extends BaseSettingsProviderTest { assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest)); } + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void getSoftAPConfiguration_flagIsEnabled_numberOfSettingsInKeyAreRecorded() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP); + when(mWifiManager.retrieveSoftApBackupData()).thenReturn(null); + + mAgentUnderTest.getSoftAPConfiguration(); + + assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_SOFTAP_CONFIG), 1); + } + + @Test + @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void getSoftAPConfiguration_flagIsNotEnabled_numberOfSettingsInKeyAreNotRecorded() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP); + when(mWifiManager.retrieveSoftApBackupData()).thenReturn(null); + + mAgentUnderTest.getSoftAPConfiguration(); + + assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_SOFTAP_CONFIG), 0); + } + + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void + restoreSoftApConfiguration_flagIsEnabled_restoreIsSuccessful_successMetricsAreLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + SoftApConfiguration config = new SoftApConfiguration.Builder().setSsid("test").build(); + byte[] data = config.toString().getBytes(); + when(mWifiManager.restoreSoftApBackupData(any())).thenReturn(null); + + mAgentUnderTest.restoreSoftApConfiguration(data); + + DataTypeResult loggingResult = + getLoggingResultForDatatype(KEY_SOFTAP_CONFIG, mAgentUnderTest); + assertNotNull(loggingResult); + assertEquals(loggingResult.getSuccessCount(), 1); + } + + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void + restoreSoftApConfiguration_flagIsEnabled_restoreIsNotSuccessful_failureMetricsAreLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + SoftApConfiguration config = new SoftApConfiguration.Builder().setSsid("test").build(); + byte[] data = config.toString().getBytes(); + when(mWifiManager.restoreSoftApBackupData(any())).thenThrow(new RuntimeException()); + + mAgentUnderTest.restoreSoftApConfiguration(data); + + DataTypeResult loggingResult = + getLoggingResultForDatatype(KEY_SOFTAP_CONFIG, mAgentUnderTest); + assertNotNull(loggingResult); + assertEquals(loggingResult.getFailCount(), 1); + } + + @Test + @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void restoreSoftApConfiguration_flagIsNotEnabled_metricsAreNotLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + SoftApConfiguration config = new SoftApConfiguration.Builder().setSsid("test").build(); + byte[] data = config.toString().getBytes(); + when(mWifiManager.restoreSoftApBackupData(any())).thenReturn(null); + + mAgentUnderTest.restoreSoftApConfiguration(data); + + assertNull(getLoggingResultForDatatype(KEY_SOFTAP_CONFIG, mAgentUnderTest)); + } + + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void getNewWifiConfigData_flagIsEnabled_numberOfSettingsInKeyAreRecorded() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP); + when(mWifiManager.retrieveBackupData()).thenReturn(null); + + mAgentUnderTest.getNewWifiConfigData(); + + assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 1); + } + + @Test + @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void getNewWifiConfigData_flagIsNotEnabled_numberOfSettingsInKeyAreNotRecorded() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP); + when(mWifiManager.retrieveBackupData()).thenReturn(null); + + mAgentUnderTest.getNewWifiConfigData(); + + assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 0); + } + + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void + restoreNewWifiConfigData_flagIsEnabled_restoreIsSuccessful_successMetricsAreLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doNothing().when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + DataTypeResult loggingResult = + getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest); + assertNotNull(loggingResult); + assertEquals(loggingResult.getSuccessCount(), 1); + } + + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void + restoreNewWifiConfigData_flagIsEnabled_restoreIsNotSuccessful_failureMetricsAreLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doThrow(new RuntimeException()).when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + DataTypeResult loggingResult = + getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest); + assertNotNull(loggingResult); + assertEquals(loggingResult.getFailCount(), 1); + } + + @Test + @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void restoreNewWifiConfigData_flagIsNotEnabled_metricsAreNotLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doNothing().when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + assertNull(getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest)); + } + private byte[] generateBackupData(Map<String, String> keyValueData) { int totalBytes = 0; for (String key : keyValueData.keySet()) { @@ -890,6 +1042,13 @@ public class SettingsBackupAgentTest extends BaseSettingsProviderTest { this.numberOfSettingsPerKey.put(key, numberOfSettings); } } + + int getNumberOfSettingsPerKey(String key) { + if (numberOfSettingsPerKey == null || !numberOfSettingsPerKey.containsKey(key)) { + return 0; + } + return numberOfSettingsPerKey.get(key); + } } /** The TestSettingsHelper tracks which values have been backed up and/or restored. */ @@ -944,6 +1103,14 @@ public class SettingsBackupAgentTest extends BaseSettingsProviderTest { public ContentResolver getContentResolver() { return mContentResolver; } + + @Override + public Object getSystemService(String name) { + if (name.equals(Context.WIFI_SERVICE)) { + return mWifiManager; + } + return super.getSystemService(name); + } } /** ContentProvider which returns a set of known test values. */ diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 227fff59d327..6b2449fdaa49 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -92,7 +92,6 @@ filegroup { "tests/src/**/systemui/shade/NotificationShadeWindowViewControllerTest.kt", "tests/src/**/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt", "tests/src/**/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt", - "tests/src/**/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt", "tests/src/**/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt", "tests/src/**/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt", "tests/src/**/systemui/education/domain/ui/view/ContextualEduDialogTest.kt", @@ -288,6 +287,7 @@ filegroup { "tests/src/**/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java", "tests/src/**/systemui/shared/system/RemoteTransitionTest.java", "tests/src/**/systemui/qs/tiles/dialog/InternetDetailsContentControllerTest.java", + "tests/src/**/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt", "tests/src/**/systemui/qs/external/TileLifecycleManagerTest.java", "tests/src/**/systemui/ScreenDecorationsTest.java", "tests/src/**/systemui/statusbar/policy/BatteryControllerStartableTest.java", diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index 795b39576391..c6cc9a975191 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -119,4 +119,5 @@ xuqiu@google.com yeinj@google.com yuandizhou@google.com yurilin@google.com +yuzhechen@google.com zakcohen@google.com diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig index ad4a02764176..ee918c275b7b 100644 --- a/packages/SystemUI/aconfig/predictive_back.aconfig +++ b/packages/SystemUI/aconfig/predictive_back.aconfig @@ -7,17 +7,3 @@ flag { description: "Enable Shade Animations" bug: "327732946" } - -flag { - name: "predictive_back_animate_bouncer" - namespace: "systemui" - description: "Enable Predictive Back Animation in Bouncer" - bug: "327733487" -} - -flag { - name: "predictive_back_animate_dialogs" - namespace: "systemui" - description: "Enable Predictive Back Animation for SysUI dialogs" - bug: "327721544" -} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 715d22328f2b..70d4cc2e4e26 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -133,14 +133,6 @@ flag { } flag { - name: "notifications_footer_view_refactor" - namespace: "systemui" - description: "Enables the refactored version of the footer view in the notification shade " - "(containing the \"Clear all\" button). Should not bring any behavior changes" - bug: "293167744" -} - -flag { name: "notifications_icon_container_refactor" namespace: "systemui" description: "Enables the refactored version of the notification icon container in StatusBar, " @@ -432,24 +424,6 @@ flag { } flag { - name: "status_bar_use_repos_for_call_chip" - namespace: "systemui" - description: "Use repositories as the source of truth for call notifications shown as a chip in" - "the status bar" - bug: "328584859" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { - name: "status_bar_call_chip_notification_icon" - namespace: "systemui" - description: "Use the small icon set on the notification for the status bar call chip" - bug: "354930838" -} - -flag { name: "status_bar_signal_policy_refactor" namespace: "systemui" description: "Use a settings observer for airplane mode and make StatusBarSignalPolicy startable" @@ -1352,6 +1326,13 @@ flag { } flag { + name: "output_switcher_redesign" + namespace: "systemui" + description: "Enables visual update for Media Output Switcher" + bug: "388296370" +} + +flag { namespace: "systemui" name: "enable_view_capture_tracing" description: "Enables view capture tracing in System UI." diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt deleted file mode 100644 index 1c9dabbb0e07..000000000000 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.android.systemui.animation - -interface AnimationFeatureFlags { - val isPredictiveBackQsDialogAnim: Boolean - get() = false -} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt index 907c39d842ce..c88c4ebb1a8d 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt @@ -59,13 +59,8 @@ constructor( private val mainExecutor: Executor, private val callback: Callback, private val interactionJankMonitor: InteractionJankMonitor, - private val featureFlags: AnimationFeatureFlags, private val transitionAnimator: TransitionAnimator = - TransitionAnimator( - mainExecutor, - TIMINGS, - INTERPOLATORS, - ), + TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS), private val isForTesting: Boolean = false, ) { private companion object { @@ -219,7 +214,7 @@ constructor( dialog: Dialog, view: View, cuj: DialogCuj? = null, - animateBackgroundBoundsChange: Boolean = false + animateBackgroundBoundsChange: Boolean = false, ) { val controller = Controller.fromView(view, cuj) if (controller == null) { @@ -245,7 +240,7 @@ constructor( fun show( dialog: Dialog, controller: Controller, - animateBackgroundBoundsChange: Boolean = false + animateBackgroundBoundsChange: Boolean = false, ) { if (Looper.myLooper() != Looper.getMainLooper()) { throw IllegalStateException( @@ -263,15 +258,14 @@ constructor( val controller = animatedParent?.dialogContentWithBackground?.let { Controller.fromView(it, controller.cuj) - } - ?: controller + } ?: controller // Make sure we don't run the launch animation from the same source twice at the same time. if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) { Log.e( TAG, "Not running dialog launch animation from source as it is already expanded into a" + - " dialog" + " dialog", ) dialog.show() return @@ -288,7 +282,6 @@ constructor( animateBackgroundBoundsChange = animateBackgroundBoundsChange, parentAnimatedDialog = animatedParent, forceDisableSynchronization = isForTesting, - featureFlags = featureFlags, ) openedDialogs.add(animatedDialog) @@ -305,7 +298,7 @@ constructor( dialog: Dialog, animateFrom: Dialog, cuj: DialogCuj? = null, - animateBackgroundBoundsChange: Boolean = false + animateBackgroundBoundsChange: Boolean = false, ) { val view = openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground @@ -313,7 +306,7 @@ constructor( Log.w( TAG, "Showing dialog $dialog normally as the dialog it is shown from was not shown " + - "using DialogTransitionAnimator" + "using DialogTransitionAnimator", ) dialog.show() return @@ -323,7 +316,7 @@ constructor( dialog, view, animateBackgroundBoundsChange = animateBackgroundBoundsChange, - cuj = cuj + cuj = cuj, ) } @@ -346,8 +339,7 @@ constructor( val animatedDialog = openedDialogs.firstOrNull { it.dialog.window?.decorView?.viewRootImpl == view.viewRootImpl - } - ?: return null + } ?: return null return createActivityTransitionController(animatedDialog, cujType) } @@ -373,7 +365,7 @@ constructor( private fun createActivityTransitionController( animatedDialog: AnimatedDialog, - cujType: Int? = null + cujType: Int? = null, ): ActivityTransitionAnimator.Controller? { // At this point, we know that the intent of the caller is to dismiss the dialog to show // an app, so we disable the exit animation into the source because we will never want to @@ -440,7 +432,7 @@ constructor( } private fun disableDialogDismiss() { - dialog.setDismissOverride { /* Do nothing */} + dialog.setDismissOverride { /* Do nothing */ } } private fun enableDialogDismiss() { @@ -530,7 +522,6 @@ private class AnimatedDialog( * Whether synchronization should be disabled, which can be useful if we are running in a test. */ private val forceDisableSynchronization: Boolean, - private val featureFlags: AnimationFeatureFlags, ) { /** * The DecorView of this dialog window. @@ -643,8 +634,7 @@ private class AnimatedDialog( originalDialogBackgroundColor = GhostedViewTransitionAnimatorController.findGradientDrawable(background) ?.color - ?.defaultColor - ?: Color.BLACK + ?.defaultColor ?: Color.BLACK // Make the background view invisible until we start the animation. We use the transition // visibility like GhostView does so that we don't mess up with the accessibility tree (see @@ -700,7 +690,7 @@ private class AnimatedDialog( oldLeft: Int, oldTop: Int, oldRight: Int, - oldBottom: Int + oldBottom: Int, ) { dialogContentWithBackground.removeOnLayoutChangeListener(this) @@ -717,9 +707,7 @@ private class AnimatedDialog( // the dialog. dialog.setDismissOverride(this::onDialogDismissed) - if (featureFlags.isPredictiveBackQsDialogAnim) { - dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground) - } + dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground) // Show the dialog. dialog.show() @@ -815,7 +803,7 @@ private class AnimatedDialog( if (hasInstrumentedJank) { interactionJankMonitor.end(controller.cuj!!.cujType) } - } + }, ) } @@ -888,14 +876,14 @@ private class AnimatedDialog( onAnimationFinished(true /* instantDismiss */) onDialogDismissed(this@AnimatedDialog) } - } + }, ) } private fun startAnimation( isLaunching: Boolean, onLaunchAnimationStart: () -> Unit = {}, - onLaunchAnimationEnd: () -> Unit = {} + onLaunchAnimationEnd: () -> Unit = {}, ) { // Create 2 controllers to animate both the dialog and the source. val startController = @@ -969,7 +957,7 @@ private class AnimatedDialog( override fun onTransitionAnimationProgress( state: TransitionAnimator.State, progress: Float, - linearProgress: Float + linearProgress: Float, ) { startController.onTransitionAnimationProgress(state, progress, linearProgress) @@ -1026,7 +1014,7 @@ private class AnimatedDialog( oldLeft: Int, oldTop: Int, oldRight: Int, - oldBottom: Int + oldBottom: Int, ) { // Don't animate if bounds didn't actually change. if (left == oldLeft && top == oldTop && right == oldRight && bottom == oldBottom) { diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt index e02e8b483543..96401ce6e1c7 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt @@ -34,9 +34,11 @@ import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed @@ -52,7 +54,6 @@ import androidx.compose.ui.node.currentValueOf import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.Velocity -import androidx.compose.ui.util.fastSumBy import com.android.compose.modifiers.thenIf import kotlin.math.sign import kotlinx.coroutines.CompletableDeferred @@ -81,7 +82,13 @@ interface NestedDraggable { * in the direction given by [sign], with the given number of [pointersDown] when the touch slop * was detected. */ - fun onDragStarted(position: Offset, sign: Float, pointersDown: Int): Controller + fun onDragStarted( + position: Offset, + sign: Float, + pointersDown: Int, + // TODO(b/382665591): Make this non-nullable. + pointerType: PointerType?, + ): Controller /** * Whether this draggable should consume any scroll amount with the given [sign] coming from a @@ -140,21 +147,66 @@ private data class NestedDraggableElement( private val orientation: Orientation, private val overscrollEffect: OverscrollEffect?, private val enabled: Boolean, -) : ModifierNodeElement<NestedDraggableNode>() { - override fun create(): NestedDraggableNode { - return NestedDraggableNode(draggable, orientation, overscrollEffect, enabled) +) : ModifierNodeElement<NestedDraggableRootNode>() { + override fun create(): NestedDraggableRootNode { + return NestedDraggableRootNode(draggable, orientation, overscrollEffect, enabled) } - override fun update(node: NestedDraggableNode) { + override fun update(node: NestedDraggableRootNode) { node.update(draggable, orientation, overscrollEffect, enabled) } } +/** + * A root node on top of [NestedDraggableNode] so that no [PointerInputModifierNode] is installed + * when this draggable is disabled. + */ +private class NestedDraggableRootNode( + draggable: NestedDraggable, + orientation: Orientation, + overscrollEffect: OverscrollEffect?, + enabled: Boolean, +) : DelegatingNode() { + private var delegateNode = + if (enabled) create(draggable, orientation, overscrollEffect) else null + + fun update( + draggable: NestedDraggable, + orientation: Orientation, + overscrollEffect: OverscrollEffect?, + enabled: Boolean, + ) { + // Disabled. + if (!enabled) { + delegateNode?.let { undelegate(it) } + delegateNode = null + return + } + + // Disabled => Enabled. + val nullableDelegate = delegateNode + if (nullableDelegate == null) { + delegateNode = create(draggable, orientation, overscrollEffect) + return + } + + // Enabled => Enabled (update). + nullableDelegate.update(draggable, orientation, overscrollEffect) + } + + private fun create( + draggable: NestedDraggable, + orientation: Orientation, + overscrollEffect: OverscrollEffect?, + ): NestedDraggableNode { + return delegate(NestedDraggableNode(draggable, orientation, overscrollEffect)) + } +} + private class NestedDraggableNode( private var draggable: NestedDraggable, override var orientation: Orientation, private var overscrollEffect: OverscrollEffect?, - private var enabled: Boolean, ) : DelegatingNode(), PointerInputModifierNode, @@ -162,17 +214,11 @@ private class NestedDraggableNode( CompositionLocalConsumerModifierNode, OrientationAware { private val nestedScrollDispatcher = NestedScrollDispatcher() - private var trackDownPositionDelegate: SuspendingPointerInputModifierNode? = null - set(value) { - field?.let { undelegate(it) } - field = value?.also { delegate(it) } - } - - private var detectDragsDelegate: SuspendingPointerInputModifierNode? = null - set(value) { - field?.let { undelegate(it) } - field = value?.also { delegate(it) } - } + private val trackWheelScroll = + delegate(SuspendingPointerInputModifierNode { trackWheelScroll() }) + private val trackDownPositionDelegate = + delegate(SuspendingPointerInputModifierNode { trackDownPosition() }) + private val detectDragsDelegate = delegate(SuspendingPointerInputModifierNode { detectDrags() }) /** The controller created by the nested scroll logic (and *not* the drag logic). */ private var nestedScrollController: NestedScrollController? = null @@ -183,9 +229,10 @@ private class NestedDraggableNode( * This is use to track the started position of a drag started on a nested scrollable. */ private var lastFirstDown: Offset? = null + private var lastEventWasScrollWheel: Boolean = false - /** The number of pointers down. */ - private var pointersDownCount = 0 + /** The pointers currently down, in order of which they were done and mapping to their type. */ + private val pointersDown = linkedMapOf<PointerId, PointerType>() init { delegate(nestedScrollModifierNode(this, nestedScrollDispatcher)) @@ -200,23 +247,25 @@ private class NestedDraggableNode( draggable: NestedDraggable, orientation: Orientation, overscrollEffect: OverscrollEffect?, - enabled: Boolean, ) { + if ( + draggable == this.draggable && + orientation == this.orientation && + overscrollEffect == this.overscrollEffect + ) { + return + } + this.draggable = draggable this.orientation = orientation this.overscrollEffect = overscrollEffect - this.enabled = enabled - trackDownPositionDelegate?.resetPointerInputHandler() - detectDragsDelegate?.resetPointerInputHandler() + trackWheelScroll.resetPointerInputHandler() + trackDownPositionDelegate.resetPointerInputHandler() + detectDragsDelegate.resetPointerInputHandler() + nestedScrollController?.ensureOnDragStoppedIsCalled() nestedScrollController = null - - if (!enabled && trackDownPositionDelegate != null) { - check(detectDragsDelegate != null) - trackDownPositionDelegate = null - detectDragsDelegate = null - } } override fun onPointerEvent( @@ -224,21 +273,15 @@ private class NestedDraggableNode( pass: PointerEventPass, bounds: IntSize, ) { - if (!enabled) return - - if (trackDownPositionDelegate == null) { - check(detectDragsDelegate == null) - trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() } - detectDragsDelegate = SuspendingPointerInputModifierNode { detectDrags() } - } - - checkNotNull(trackDownPositionDelegate).onPointerEvent(pointerEvent, pass, bounds) - checkNotNull(detectDragsDelegate).onPointerEvent(pointerEvent, pass, bounds) + trackWheelScroll.onPointerEvent(pointerEvent, pass, bounds) + trackDownPositionDelegate.onPointerEvent(pointerEvent, pass, bounds) + detectDragsDelegate.onPointerEvent(pointerEvent, pass, bounds) } override fun onCancelPointerInput() { - trackDownPositionDelegate?.onCancelPointerInput() - detectDragsDelegate?.onCancelPointerInput() + trackWheelScroll.onCancelPointerInput() + trackDownPositionDelegate.onCancelPointerInput() + detectDragsDelegate.onCancelPointerInput() } /* @@ -256,7 +299,9 @@ private class NestedDraggableNode( check(down.position == lastFirstDown) { "Position from detectDrags() is not the same as position in trackDownPosition()" } - check(pointersDownCount == 1) { "pointersDownCount is equal to $pointersDownCount" } + check(pointersDown.size == 1 && pointersDown.keys.first() == down.id) { + "pointersDown should only contain $down but it contains $pointersDown" + } var overSlop = 0f val onTouchSlopReached = { change: PointerInputChange, over: Float -> @@ -295,8 +340,9 @@ private class NestedDraggableNode( } } - check(pointersDownCount > 0) { "pointersDownCount is equal to $pointersDownCount" } - val controller = draggable.onDragStarted(down.position, sign, pointersDownCount) + check(pointersDown.size > 0) { "pointersDown is empty" } + val controller = + draggable.onDragStarted(down.position, sign, pointersDown.size, drag.type) if (overSlop != 0f) { onDrag(controller, drag, overSlop, velocityTracker) } @@ -448,22 +494,33 @@ private class NestedDraggableNode( * =============================== */ + private suspend fun PointerInputScope.trackWheelScroll() { + awaitEachGesture { + val event = awaitPointerEvent(pass = PointerEventPass.Initial) + lastEventWasScrollWheel = event.type == PointerEventType.Scroll + } + } + private suspend fun PointerInputScope.trackDownPosition() { awaitEachGesture { - val down = awaitFirstDown(requireUnconsumed = false) - lastFirstDown = down.position - pointersDownCount = 1 + try { + val down = awaitFirstDown(requireUnconsumed = false) + lastFirstDown = down.position + pointersDown[down.id] = down.type - do { - pointersDownCount += - awaitPointerEvent().changes.fastSumBy { change -> + do { + awaitPointerEvent().changes.forEach { change -> when { - change.changedToDownIgnoreConsumed() -> 1 - change.changedToUpIgnoreConsumed() -> -1 - else -> 0 + change.changedToDownIgnoreConsumed() -> { + pointersDown[change.id] = change.type + } + change.changedToUpIgnoreConsumed() -> pointersDown.remove(change.id) } } - } while (pointersDownCount > 0) + } while (pointersDown.size > 0) + } finally { + pointersDown.clear() + } } } @@ -488,15 +545,21 @@ private class NestedDraggableNode( } val sign = offset.sign - if (nestedScrollController == null && draggable.shouldConsumeNestedScroll(sign)) { + if ( + nestedScrollController == null && + // TODO(b/388231324): Remove this. + !lastEventWasScrollWheel && + draggable.shouldConsumeNestedScroll(sign) + ) { val startedPosition = checkNotNull(lastFirstDown) { "lastFirstDown is not set" } - // TODO(b/382665591): Replace this by check(pointersDownCount > 0). - val pointersDown = pointersDownCount.coerceAtLeast(1) + // TODO(b/382665591): Ensure that there is at least one pointer down. + val pointersDownCount = pointersDown.size.coerceAtLeast(1) + val pointerType = pointersDown.entries.firstOrNull()?.value nestedScrollController = NestedScrollController( overscrollEffect, - draggable.onDragStarted(startedPosition, sign, pointersDown), + draggable.onDragStarted(startedPosition, sign, pointersDownCount, pointerType), ) } diff --git a/packages/SystemUI/compose/core/tests/AndroidManifest.xml b/packages/SystemUI/compose/core/tests/AndroidManifest.xml index 28f80d4af265..7c721b97ee47 100644 --- a/packages/SystemUI/compose/core/tests/AndroidManifest.xml +++ b/packages/SystemUI/compose/core/tests/AndroidManifest.xml @@ -15,6 +15,7 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" package="com.android.compose.core.tests" > <application> @@ -23,7 +24,8 @@ <activity android:name="androidx.activity.ComponentActivity" android:theme="@android:style/Theme.DeviceDefault.DayNight" - android:exported="true" /> + android:exported="true" + tools:replace="android:theme" /> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt index 9c49090916e3..5de0f1221f0f 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt @@ -18,25 +18,37 @@ package com.android.compose.gesture import androidx.compose.foundation.ScrollState import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.rememberScrollableState +import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.ScrollWheel +import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import androidx.compose.ui.test.swipeLeft @@ -653,6 +665,114 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw assertThat(flingIsDone).isTrue() } + @Test + fun pointerType() { + val draggable = TestDraggable() + val touchSlop = + rule.setContentWithTouchSlop { + Box(Modifier.fillMaxSize().nestedDraggable(draggable, orientation)) + } + + rule.onRoot().performTouchInput { + down(center) + moveBy(touchSlop.toOffset()) + } + + assertThat(draggable.onDragStartedPointerType).isEqualTo(PointerType.Touch) + } + + @Test + fun pointerType_mouse() { + val draggable = TestDraggable() + val touchSlop = + rule.setContentWithTouchSlop { + Box(Modifier.fillMaxSize().nestedDraggable(draggable, orientation)) + } + + rule.onRoot().performMouseInput { + moveTo(center) + press() + moveBy(touchSlop.toOffset()) + release() + } + + assertThat(draggable.onDragStartedPointerType).isEqualTo(PointerType.Mouse) + } + + @Test + @Ignore("b/388507816: re-enable this when the crash in HitPath is fixed") + fun pointersDown_clearedWhenDisabled() { + val draggable = TestDraggable() + var enabled by mutableStateOf(true) + rule.setContent { + Box(Modifier.fillMaxSize().nestedDraggable(draggable, orientation, enabled = enabled)) + } + + rule.onRoot().performTouchInput { down(center) } + + enabled = false + rule.waitForIdle() + + rule.onRoot().performTouchInput { up() } + + enabled = true + rule.waitForIdle() + + rule.onRoot().performTouchInput { down(center) } + } + + @Test + // TODO(b/388231324): Remove this. + fun nestedScrollWithMouseWheelIsIgnored() { + val draggable = TestDraggable() + val touchSlop = + rule.setContentWithTouchSlop { + Box( + Modifier.fillMaxSize() + .nestedDraggable(draggable, orientation) + .scrollable(rememberScrollableState { 0f }, orientation) + ) + } + + rule.onRoot().performMouseInput { + enter(center) + scroll( + touchSlop + 1f, + when (orientation) { + Orientation.Horizontal -> ScrollWheel.Horizontal + Orientation.Vertical -> ScrollWheel.Vertical + }, + ) + } + + assertThat(draggable.onDragStartedCalled).isFalse() + } + + @Test + fun doesNotConsumeGesturesWhenDisabled() { + val buttonTag = "button" + rule.setContent { + Box { + var count by remember { mutableStateOf(0) } + Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) { + Text("Count: $count") + } + + Box( + Modifier.fillMaxSize() + .nestedDraggable(remember { TestDraggable() }, orientation, enabled = false) + ) + } + } + + rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0") + + // Click on the root at its center, where the button is located. Clicks should go through + // the draggable and reach the button given that it is disabled. + repeat(3) { rule.onRoot().performClick() } + rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3") + } + private fun ComposeContentTestRule.setContentWithTouchSlop( content: @Composable () -> Unit ): Float { @@ -688,6 +808,7 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw var onDragStartedPosition = Offset.Zero var onDragStartedSign = 0f var onDragStartedPointersDown = 0 + var onDragStartedPointerType: PointerType? = null var onDragDelta = 0f override fun shouldStartDrag(change: PointerInputChange): Boolean = shouldStartDrag @@ -696,11 +817,13 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw position: Offset, sign: Float, pointersDown: Int, + pointerType: PointerType?, ): NestedDraggable.Controller { onDragStartedCalled = true onDragStartedPosition = position onDragStartedSign = sign onDragStartedPointersDown = pointersDown + onDragStartedPointerType = pointerType onDragDelta = 0f onDragStarted.invoke(position, sign) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt index 1a8c7f8ba24c..0054a4c899ec 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalFoundationApi::class) - package com.android.systemui.bouncer.ui.composable import android.app.AlertDialog @@ -26,7 +24,6 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -99,7 +96,6 @@ import com.android.compose.animation.scene.transitions import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel import com.android.systemui.bouncer.ui.BouncerDialogFactory -import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt index eb62d336b0df..328fec591b41 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt @@ -16,13 +16,11 @@ package com.android.systemui.bouncer.ui.composable +import androidx.annotation.VisibleForTesting import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import com.android.compose.windowsizeclass.LocalWindowSizeClass -import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout -import com.android.systemui.bouncer.ui.helper.SizeClass -import com.android.systemui.bouncer.ui.helper.calculateLayoutInternal /** * Returns the [BouncerSceneLayout] that should be used by the bouncer scene. If @@ -57,3 +55,50 @@ private fun WindowHeightSizeClass.toEnum(): SizeClass { else -> error("Unsupported WindowHeightSizeClass \"$this\"") } } + +/** Enumerates all known adaptive layout configurations. */ +enum class BouncerSceneLayout { + /** The default UI with the bouncer laid out normally. */ + STANDARD_BOUNCER, + /** The bouncer is displayed vertically stacked with the user switcher. */ + BELOW_USER_SWITCHER, + /** The bouncer is displayed side-by-side with the user switcher or an empty space. */ + BESIDE_USER_SWITCHER, + /** The bouncer is split in two with both sides shown side-by-side. */ + SPLIT_BOUNCER, +} + +/** Enumerates the supported window size classes. */ +enum class SizeClass { + COMPACT, + MEDIUM, + EXPANDED, +} + +/** + * Internal version of `calculateLayout` in the System UI Compose library, extracted here to allow + * for testing that's not dependent on Compose. + */ +@VisibleForTesting +fun calculateLayoutInternal( + width: SizeClass, + height: SizeClass, + isOneHandedModeSupported: Boolean, +): BouncerSceneLayout { + return when (height) { + SizeClass.COMPACT -> BouncerSceneLayout.SPLIT_BOUNCER + SizeClass.MEDIUM -> + when (width) { + SizeClass.COMPACT -> BouncerSceneLayout.STANDARD_BOUNCER + SizeClass.MEDIUM -> BouncerSceneLayout.STANDARD_BOUNCER + SizeClass.EXPANDED -> BouncerSceneLayout.BESIDE_USER_SWITCHER + } + SizeClass.EXPANDED -> + when (width) { + SizeClass.COMPACT -> BouncerSceneLayout.STANDARD_BOUNCER + SizeClass.MEDIUM -> BouncerSceneLayout.BELOW_USER_SWITCHER + SizeClass.EXPANDED -> BouncerSceneLayout.BESIDE_USER_SWITCHER + } + }.takeIf { it != BouncerSceneLayout.BESIDE_USER_SWITCHER || isOneHandedModeSupported } + ?: BouncerSceneLayout.STANDARD_BOUNCER +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 9c53afecad11..a2a91fcd5d52 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -78,6 +78,18 @@ object TransitionDuration { const val EDIT_MODE_TO_HUB_GRID_END_MS = EDIT_MODE_TO_HUB_GRID_DELAY_MS + EDIT_MODE_TO_HUB_CONTENT_MS const val HUB_TO_EDIT_MODE_CONTENT_MS = 250 + const val TO_GLANCEABLE_HUB_DURATION_MS = 1000 +} + +val sceneTransitionsV2 = transitions { + to(CommunalScenes.Communal) { + spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS) + fade(AllElements) + } + to(CommunalScenes.Blank) { + spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS)) + fade(AllElements) + } } val sceneTransitions = transitions { @@ -157,7 +169,7 @@ fun CommunalContainer( MutableSceneTransitionLayoutState( initialScene = currentSceneKey, canChangeScene = { _ -> viewModel.canChangeScene() }, - transitions = sceneTransitions, + transitions = if (viewModel.v2FlagEnabled()) sceneTransitionsV2 else sceneTransitions, ) } val isUiBlurred by viewModel.isUiBlurred.collectAsStateWithLifecycle() 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 c3dc84d0a12c..a6a63624cf7c 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 @@ -33,6 +33,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.MutableSceneTransitionLayoutState @@ -43,11 +44,13 @@ import com.android.compose.animation.scene.SceneTransitions import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.observableTransitionState +import com.android.systemui.lifecycle.rememberActivated import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.qs.ui.composable.QuickSettingsTheme import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.ui.view.SceneJankMonitor import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import javax.inject.Provider @@ -82,16 +85,38 @@ fun SceneContainer( sceneTransitions: SceneTransitions, dataSourceDelegator: SceneDataSourceDelegator, qsSceneAdapter: Provider<QSSceneAdapter>, + sceneJankMonitorFactory: SceneJankMonitor.Factory, modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() - val state: MutableSceneTransitionLayoutState = remember { - MutableSceneTransitionLayoutState( - initialScene = initialSceneKey, - canChangeScene = { toScene -> viewModel.canChangeScene(toScene) }, - transitions = sceneTransitions, - ) - } + + val view = LocalView.current + val sceneJankMonitor = + rememberActivated(traceName = "sceneJankMonitor") { sceneJankMonitorFactory.create() } + + val state: MutableSceneTransitionLayoutState = + remember(view, sceneJankMonitor) { + MutableSceneTransitionLayoutState( + initialScene = initialSceneKey, + canChangeScene = { toScene -> viewModel.canChangeScene(toScene) }, + transitions = sceneTransitions, + onTransitionStart = { transition -> + sceneJankMonitor.onTransitionStart( + view = view, + from = transition.fromContent, + to = transition.toContent, + cuj = transition.cuj, + ) + }, + onTransitionEnd = { transition -> + sceneJankMonitor.onTransitionEnd( + from = transition.fromContent, + to = transition.toContent, + cuj = transition.cuj, + ) + }, + ) + } DisposableEffect(state) { val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index ee8535eff3ae..6d24fc16df23 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -3,6 +3,7 @@ package com.android.systemui.scene.ui.composable import androidx.compose.animation.core.spring import com.android.compose.animation.scene.TransitionKey import com.android.compose.animation.scene.transitions +import com.android.internal.jank.Cuj import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes @@ -56,14 +57,41 @@ val SceneContainerTransitions = transitions { from(Scenes.Dream, to = Scenes.Bouncer) { dreamToBouncerTransition() } from(Scenes.Dream, to = Scenes.Communal) { dreamToCommunalTransition() } from(Scenes.Dream, to = Scenes.Gone) { dreamToGoneTransition() } - from(Scenes.Dream, to = Scenes.Shade) { dreamToShadeTransition() } - from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() } - from(Scenes.Gone, to = Scenes.Shade, key = ToSplitShade) { goneToSplitShadeTransition() } - from(Scenes.Gone, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) { + from(Scenes.Dream, to = Scenes.Shade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) { + dreamToShadeTransition() + } + from(Scenes.Gone, to = Scenes.Shade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) { + goneToShadeTransition() + } + from( + Scenes.Gone, + to = Scenes.Shade, + key = ToSplitShade, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, + ) { + goneToSplitShadeTransition() + } + from( + Scenes.Gone, + to = Scenes.Shade, + key = SlightlyFasterShadeCollapse, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, + ) { goneToShadeTransition(durationScale = 0.9) } - from(Scenes.Gone, to = Scenes.QuickSettings) { goneToQuickSettingsTransition() } - from(Scenes.Gone, to = Scenes.QuickSettings, key = SlightlyFasterShadeCollapse) { + from( + Scenes.Gone, + to = Scenes.QuickSettings, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, + ) { + goneToQuickSettingsTransition() + } + from( + Scenes.Gone, + to = Scenes.QuickSettings, + key = SlightlyFasterShadeCollapse, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, + ) { goneToQuickSettingsTransition(durationScale = 0.9) } @@ -78,49 +106,112 @@ val SceneContainerTransitions = transitions { } from(Scenes.Lockscreen, to = Scenes.Communal) { lockscreenToCommunalTransition() } from(Scenes.Lockscreen, to = Scenes.Dream) { lockscreenToDreamTransition() } - from(Scenes.Lockscreen, to = Scenes.Shade) { lockscreenToShadeTransition() } - from(Scenes.Lockscreen, to = Scenes.Shade, key = ToSplitShade) { + from(Scenes.Lockscreen, to = Scenes.Shade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) { + lockscreenToShadeTransition() + } + from( + Scenes.Lockscreen, + to = Scenes.Shade, + key = ToSplitShade, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, + ) { lockscreenToSplitShadeTransition() sharedElement(Shade.Elements.BackgroundScrim, enabled = false) } - from(Scenes.Lockscreen, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) { + from( + Scenes.Lockscreen, + to = Scenes.Shade, + key = SlightlyFasterShadeCollapse, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, + ) { lockscreenToShadeTransition(durationScale = 0.9) } - from(Scenes.Lockscreen, to = Scenes.QuickSettings) { lockscreenToQuickSettingsTransition() } + from( + Scenes.Lockscreen, + to = Scenes.QuickSettings, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, + ) { + lockscreenToQuickSettingsTransition() + } from(Scenes.Lockscreen, to = Scenes.Gone) { lockscreenToGoneTransition() } - from(Scenes.QuickSettings, to = Scenes.Shade) { + from( + Scenes.QuickSettings, + to = Scenes.Shade, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, + ) { reversed { shadeToQuickSettingsTransition() } sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false) } - from(Scenes.Shade, to = Scenes.QuickSettings) { shadeToQuickSettingsTransition() } - from(Scenes.Shade, to = Scenes.Lockscreen) { + from( + Scenes.Shade, + to = Scenes.QuickSettings, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, + ) { + shadeToQuickSettingsTransition() + } + from(Scenes.Shade, to = Scenes.Lockscreen, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) { reversed { lockscreenToShadeTransition() } sharedElement(Notifications.Elements.NotificationStackPlaceholder, enabled = false) sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false) } - from(Scenes.Shade, to = Scenes.Lockscreen, key = ToSplitShade) { + from( + Scenes.Shade, + to = Scenes.Lockscreen, + key = ToSplitShade, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, + ) { reversed { lockscreenToSplitShadeTransition() } } - from(Scenes.Communal, to = Scenes.Shade) { communalToShadeTransition() } + from(Scenes.Communal, to = Scenes.Shade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) { + communalToShadeTransition() + } from(Scenes.Communal, to = Scenes.Bouncer) { communalToBouncerTransition() } // Overlay transitions - to(Overlays.NotificationsShade) { toNotificationsShadeTransition() } - to(Overlays.QuickSettingsShade) { toQuickSettingsShadeTransition() } - from(Overlays.NotificationsShade, to = Overlays.QuickSettingsShade) { + to(Overlays.NotificationsShade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) { + toNotificationsShadeTransition() + } + to(Overlays.QuickSettingsShade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE) { + toQuickSettingsShadeTransition() + } + from( + Overlays.NotificationsShade, + to = Overlays.QuickSettingsShade, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, + ) { notificationsShadeToQuickSettingsShadeTransition() } - from(Scenes.Gone, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) { + from( + Scenes.Gone, + to = Overlays.NotificationsShade, + key = SlightlyFasterShadeCollapse, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, + ) { toNotificationsShadeTransition(durationScale = 0.9) } - from(Scenes.Gone, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) { + from( + Scenes.Gone, + to = Overlays.QuickSettingsShade, + key = SlightlyFasterShadeCollapse, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, + ) { toQuickSettingsShadeTransition(durationScale = 0.9) } - from(Scenes.Lockscreen, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) { + from( + Scenes.Lockscreen, + to = Overlays.NotificationsShade, + key = SlightlyFasterShadeCollapse, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, + ) { toNotificationsShadeTransition(durationScale = 0.9) } - from(Scenes.Lockscreen, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) { + from( + Scenes.Lockscreen, + to = Overlays.QuickSettingsShade, + key = SlightlyFasterShadeCollapse, + cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, + ) { toQuickSettingsShadeTransition(durationScale = 0.9) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt index 93c10b6224ab..6ca9c7934979 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt @@ -17,17 +17,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.communal.ui.compose.AllElements -import com.android.systemui.communal.ui.compose.Communal fun TransitionBuilder.dreamToCommunalTransition() { spec = tween(durationMillis = 1000) - // Translate communal hub grid from the end direction. - translate(Communal.Elements.Grid, Edge.End) - - // Fade all communal hub elements. - timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) } + // Fade in all communal hub elements. + fade(AllElements) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt index 826a2550cca3..de9a78c497f8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt @@ -17,21 +17,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.communal.ui.compose.AllElements -import com.android.systemui.communal.ui.compose.Communal -import com.android.systemui.scene.shared.model.Scenes fun TransitionBuilder.lockscreenToCommunalTransition() { spec = tween(durationMillis = 1000) - // Translate lockscreen to the start direction. - translate(Scenes.Lockscreen.rootElementKey, Edge.Start) - - // Translate communal hub grid from the end direction. - translate(Communal.Elements.Grid, Edge.End) - - // Fade all communal hub elements. - timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) } + // Fade all communal hub elements in. + fade(AllElements) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 6bb579d18bf9..2ca846424d93 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -60,17 +60,12 @@ internal interface DragController { * Stop the current drag with the given [velocity]. * * @param velocity The velocity of the drag when it stopped. - * @param canChangeContent Whether the content can be changed as a result of this drag. * @return the consumed [velocity] when the animation complete */ - suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float + suspend fun onStop(velocity: Float): Float - /** - * Cancels the current drag. - * - * @param canChangeContent Whether the content can be changed as a result of this drag. - */ - fun onCancel(canChangeContent: Boolean) + /** Cancels the current drag. */ + fun onCancel() } internal class DraggableHandlerImpl( @@ -295,17 +290,16 @@ private class DragControllerImpl( return newOffset - previousOffset } - override suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float { + override suspend fun onStop(velocity: Float): Float { // To ensure that any ongoing animation completes gracefully and avoids an undefined state, // we execute the actual `onStop` logic in a non-cancellable context. This prevents the // coroutine from being cancelled prematurely, which could interrupt the animation. // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags. - return withContext(NonCancellable) { onStop(velocity, canChangeContent, swipeAnimation) } + return withContext(NonCancellable) { onStop(velocity, swipeAnimation) } } private suspend fun <T : ContentKey> onStop( velocity: Float, - canChangeContent: Boolean, // Important: Make sure that this has the same name as [this.swipeAnimation] so that all the // code here references the current animation when [onDragStopped] is called, otherwise the @@ -319,35 +313,27 @@ private class DragControllerImpl( } val fromContent = swipeAnimation.fromContent + // If we are halfway between two contents, we check what the target will be based on + // the velocity and offset of the transition, then we launch the animation. + + val toContent = swipeAnimation.toContent + + // Compute the destination content (and therefore offset) to settle in. + val offset = swipeAnimation.dragOffset + val distance = swipeAnimation.distance() val targetContent = - if (canChangeContent) { - // If we are halfway between two contents, we check what the target will be based on - // the velocity and offset of the transition, then we launch the animation. - - val toContent = swipeAnimation.toContent - - // Compute the destination content (and therefore offset) to settle in. - val offset = swipeAnimation.dragOffset - val distance = swipeAnimation.distance() - if ( - distance != DistanceUnspecified && - shouldCommitSwipe( - offset = offset, - distance = distance, - velocity = velocity, - wasCommitted = swipeAnimation.currentContent == toContent, - requiresFullDistanceSwipe = swipeAnimation.requiresFullDistanceSwipe, - ) - ) { - toContent - } else { - fromContent - } + if ( + distance != DistanceUnspecified && + shouldCommitSwipe( + offset = offset, + distance = distance, + velocity = velocity, + wasCommitted = swipeAnimation.currentContent == toContent, + requiresFullDistanceSwipe = swipeAnimation.requiresFullDistanceSwipe, + ) + ) { + toContent } else { - // We are doing an overscroll preview animation between scenes. - check(fromContent == swipeAnimation.currentContent) { - "canChangeContent is false but currentContent != fromContent" - } fromContent } @@ -423,10 +409,8 @@ private class DragControllerImpl( } } - override fun onCancel(canChangeContent: Boolean) { - swipeAnimation.contentTransition.coroutineScope.launch { - onStop(velocity = 0f, canChangeContent = canChangeContent) - } + override fun onCancel() { + swipeAnimation.contentTransition.coroutineScope.launch { onStop(velocity = 0f) } } } @@ -445,6 +429,58 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol } /** + * Finds the best matching [UserActionResult] for the given [swipe] within this [Content]. + * Prioritizes actions with matching [Swipe.Resolved.fromSource]. + * + * @param swipe The swipe to match against. + * @return The best matching [UserActionResult], or `null` if no match is found. + */ + private fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { + if (!areSwipesAllowed()) { + return null + } + + var bestPoints = Int.MIN_VALUE + var bestMatch: UserActionResult? = null + userActions.forEach { (actionSwipe, actionResult) -> + if ( + actionSwipe !is Swipe.Resolved || + // The direction must match. + actionSwipe.direction != swipe.direction || + // The number of pointers down must match. + actionSwipe.pointerCount != swipe.pointerCount || + // The action requires a specific fromSource. + (actionSwipe.fromSource != null && + actionSwipe.fromSource != swipe.fromSource) || + // The action requires a specific pointerType. + (actionSwipe.pointersType != null && + actionSwipe.pointersType != swipe.pointersType) + ) { + // This action is not eligible. + return@forEach + } + + val sameFromSource = actionSwipe.fromSource == swipe.fromSource + val samePointerType = actionSwipe.pointersType == swipe.pointersType + // Prioritize actions with a perfect match. + if (sameFromSource && samePointerType) { + return actionResult + } + + var points = 0 + if (sameFromSource) points++ + if (samePointerType) points++ + + // Otherwise, keep track of the best eligible action. + if (points > bestPoints) { + bestPoints = points + bestMatch = actionResult + } + } + return bestMatch + } + + /** * Update the swipes results. * * Usually we don't want to update them while doing a drag, because this could change the target @@ -519,11 +555,11 @@ private fun scrollController( } override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { - return dragController.onStop(velocity = initialVelocity, canChangeContent = true) + return dragController.onStop(velocity = initialVelocity) } override fun onCancel() { - dragController.onCancel(canChangeContent = true) + dragController.onCancel() } /** @@ -547,9 +583,9 @@ internal const val OffsetVisibilityThreshold = 0.5f private object NoOpDragController : DragController { override fun onDrag(delta: Float) = 0f - override suspend fun onStop(velocity: Float, canChangeContent: Boolean) = 0f + override suspend fun onStop(velocity: Float) = 0f - override fun onCancel(canChangeContent: Boolean) { + override fun onCancel() { /* do nothing */ } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index f5f01d4d1a35..89320f1303e5 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -307,13 +307,13 @@ internal class MultiPointerDraggableNode( velocityTracker.calculateVelocity(maxVelocity) } .toFloat(), - onFling = { controller.onStop(it, canChangeContent = true) }, + onFling = { controller.onStop(it) }, ) }, onDragCancel = { controller -> startFlingGesture( initialVelocity = 0f, - onFling = { controller.onStop(it, canChangeContent = true) }, + onFling = { controller.onStop(it) }, ) }, swipeDetector = swipeDetector, 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 c704a3e96467..de428a7d3548 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 @@ -35,6 +35,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection +import com.android.compose.gesture.NestedScrollableBound import com.android.compose.gesture.effect.ContentOverscrollEffect /** @@ -238,6 +239,18 @@ interface BaseContentScope : ElementStateScope { fun Modifier.noResizeDuringTransitions(): Modifier /** + * Temporarily disable this content swipe actions when any scrollable below this modifier has + * consumed any amount of scroll delta, until the scroll gesture is finished. + * + * This can for instance be used to ensure that a scrollable list is overscrolled once it + * reached its bounds instead of directly starting a scene transition from the same scroll + * gesture. + */ + fun Modifier.disableSwipesWhenScrolling( + bounds: NestedScrollableBound = NestedScrollableBound.Any + ): Modifier + + /** * A [NestedSceneTransitionLayout] will share its elements with its ancestor STLs therefore * enabling sharedElement transitions between them. */ diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt index 607e4fadc256..ba92f9bea07d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt @@ -315,16 +315,10 @@ internal class SwipeAnimation<T : ContentKey>( val skipAnimation = hasReachedTargetContent && !contentTransition.isWithinProgressRange(initialProgress) - val targetOffset = - if (targetContent == fromContent) { - 0f - } else { - val distance = distance() - check(distance != DistanceUnspecified) { - "distance is equal to $DistanceUnspecified" - } - distance - } + val distance = distance() + check(distance != DistanceUnspecified) { "distance is equal to $DistanceUnspecified" } + + val targetOffset = if (targetContent == fromContent) 0f else distance // If the effective current content changed, it should be reflected right now in the // current state, even before the settle animation is ongoing. That way all the @@ -343,7 +337,16 @@ internal class SwipeAnimation<T : ContentKey>( } val animatable = - Animatable(initialOffset, OffsetVisibilityThreshold).also { offsetAnimation = it } + Animatable(initialOffset, OffsetVisibilityThreshold).also { + offsetAnimation = it + + // We should animate when the progress value is between [0, 1]. + if (distance > 0) { + it.updateBounds(0f, distance) + } else { + it.updateBounds(distance, 0f) + } + } check(isAnimatingOffset()) @@ -370,42 +373,26 @@ internal class SwipeAnimation<T : ContentKey>( val velocityConsumed = CompletableDeferred<Float>() offsetAnimationRunnable.complete { - try { + val result = animatable.animateTo( targetValue = targetOffset, animationSpec = swipeSpec, initialVelocity = initialVelocity, - ) { - // Immediately stop this transition if we are bouncing on a content that - // does not bounce. - if (!contentTransition.isWithinProgressRange(progress)) { - // We are no longer able to consume the velocity, the rest can be - // consumed by another component in the hierarchy. - velocityConsumed.complete(initialVelocity - velocity) - throw SnapException() - } - } - } catch (_: SnapException) { - /* Ignore. */ - } finally { - if (!velocityConsumed.isCompleted) { - // The animation consumed the whole available velocity - velocityConsumed.complete(initialVelocity) - } + ) - // Wait for overscroll to finish so that the transition is removed from the STLState - // only after the overscroll is done, to avoid dropping frame right when the user - // lifts their finger and overscroll is animated to 0. - overscrollCompletable?.await() - } + // We are no longer able to consume the velocity, the rest can be consumed by another + // component in the hierarchy. + velocityConsumed.complete(initialVelocity - result.endState.velocity) + + // Wait for overscroll to finish so that the transition is removed from the STLState + // only after the overscroll is done, to avoid dropping frame right when the user + // lifts their finger and overscroll is animated to 0. + overscrollCompletable?.await() } return velocityConsumed.await() } - /** An exception thrown during the animation to stop it immediately. */ - private class SnapException : Exception() - private fun canChangeContent(targetContent: ContentKey): Boolean { return when (val transition = contentTransition) { is TransitionState.Transition.ChangeScene -> diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index c5b3df222855..e2212113404d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -37,11 +37,7 @@ internal fun Modifier.swipeToScene( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, ): Modifier { - return if (draggableHandler.enabled()) { - this.then(SwipeToSceneElement(draggableHandler, swipeDetector)) - } else { - this - } + return then(SwipeToSceneElement(draggableHandler, swipeDetector, draggableHandler.enabled())) } private fun DraggableHandlerImpl.enabled(): Boolean { @@ -54,87 +50,69 @@ private fun DraggableHandlerImpl.contentForSwipes(): Content { /** Whether swipe should be enabled in the given [orientation]. */ internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean { - if (userActions.isEmpty()) { + if (userActions.isEmpty() || !areSwipesAllowed()) { return false } return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation } } -/** - * Finds the best matching [UserActionResult] for the given [swipe] within this [Content]. - * Prioritizes actions with matching [Swipe.Resolved.fromSource]. - * - * @param swipe The swipe to match against. - * @return The best matching [UserActionResult], or `null` if no match is found. - */ -internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { - var bestPoints = Int.MIN_VALUE - var bestMatch: UserActionResult? = null - userActions.forEach { (actionSwipe, actionResult) -> - if ( - actionSwipe !is Swipe.Resolved || - // The direction must match. - actionSwipe.direction != swipe.direction || - // The number of pointers down must match. - actionSwipe.pointerCount != swipe.pointerCount || - // The action requires a specific fromSource. - (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) || - // The action requires a specific pointerType. - (actionSwipe.pointersType != null && actionSwipe.pointersType != swipe.pointersType) - ) { - // This action is not eligible. - return@forEach - } - - val sameFromSource = actionSwipe.fromSource == swipe.fromSource - val samePointerType = actionSwipe.pointersType == swipe.pointersType - // Prioritize actions with a perfect match. - if (sameFromSource && samePointerType) { - return actionResult - } - - var points = 0 - if (sameFromSource) points++ - if (samePointerType) points++ - - // Otherwise, keep track of the best eligible action. - if (points > bestPoints) { - bestPoints = points - bestMatch = actionResult - } - } - return bestMatch -} - private data class SwipeToSceneElement( val draggableHandler: DraggableHandlerImpl, val swipeDetector: SwipeDetector, + val enabled: Boolean, ) : ModifierNodeElement<SwipeToSceneRootNode>() { override fun create(): SwipeToSceneRootNode = - SwipeToSceneRootNode(draggableHandler, swipeDetector) + SwipeToSceneRootNode(draggableHandler, swipeDetector, enabled) override fun update(node: SwipeToSceneRootNode) { - node.update(draggableHandler, swipeDetector) + node.update(draggableHandler, swipeDetector, enabled) } } private class SwipeToSceneRootNode( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, + enabled: Boolean, ) : DelegatingNode() { - private var delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) + private var delegateNode = if (enabled) create(draggableHandler, swipeDetector) else null + + fun update( + draggableHandler: DraggableHandlerImpl, + swipeDetector: SwipeDetector, + enabled: Boolean, + ) { + // Disabled. + if (!enabled) { + delegateNode?.let { undelegate(it) } + delegateNode = null + return + } - fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) { - if (draggableHandler == delegateNode.draggableHandler) { + // Disabled => Enabled. + val nullableDelegate = delegateNode + if (nullableDelegate == null) { + delegateNode = create(draggableHandler, swipeDetector) + return + } + + // Enabled => Enabled (update). + if (draggableHandler == nullableDelegate.draggableHandler) { // Simple update, just update the swipe detector directly and keep the node. - delegateNode.swipeDetector = swipeDetector + nullableDelegate.swipeDetector = swipeDetector } else { // The draggableHandler changed, force recreate the underlying SwipeToSceneNode. - undelegate(delegateNode) - delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) + undelegate(nullableDelegate) + delegateNode = create(draggableHandler, swipeDetector) } } + + private fun create( + draggableHandler: DraggableHandlerImpl, + swipeDetector: SwipeDetector, + ): SwipeToSceneNode { + return delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) + } } private class SwipeToSceneNode( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt index 4c15f7a4534f..59b4a09385f5 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt @@ -56,7 +56,10 @@ import com.android.compose.animation.scene.effect.GestureEffect import com.android.compose.animation.scene.effect.VisualEffect import com.android.compose.animation.scene.element import com.android.compose.animation.scene.modifiers.noResizeDuringTransitions +import com.android.compose.gesture.NestedScrollControlState +import com.android.compose.gesture.NestedScrollableBound import com.android.compose.gesture.effect.OffsetOverscrollEffect +import com.android.compose.gesture.nestedScrollController import com.android.compose.modifiers.thenIf import com.android.compose.ui.graphics.ContainerState import com.android.compose.ui.graphics.container @@ -70,7 +73,8 @@ internal sealed class Content( actions: Map<UserAction.Resolved, UserActionResult>, zIndex: Float, ) { - internal val scope = ContentScopeImpl(layoutImpl, content = this) + private val nestedScrollControlState = NestedScrollControlState() + internal val scope = ContentScopeImpl(layoutImpl, content = this, nestedScrollControlState) val containerState = ContainerState() var content by mutableStateOf(content) @@ -101,11 +105,14 @@ internal sealed class Content( scope.content() } } + + fun areSwipesAllowed(): Boolean = nestedScrollControlState.isOuterScrollAllowed } internal class ContentScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val content: Content, + private val nestedScrollControlState: NestedScrollControlState, ) : ContentScope, ElementStateScope by layoutImpl.elementStateScope { override val contentKey: ContentKey get() = content.key @@ -176,6 +183,10 @@ internal class ContentScopeImpl( return noResizeDuringTransitions(layoutState = layoutImpl.state) } + override fun Modifier.disableSwipesWhenScrolling(bounds: NestedScrollableBound): Modifier { + return nestedScrollController(nestedScrollControlState, bounds) + } + @Composable override fun NestedSceneTransitionLayout( state: SceneTransitionLayoutState, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ContentTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ContentTest.kt new file mode 100644 index 000000000000..06a9735d97e2 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ContentTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2025 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.compose.animation.scene + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.rememberScrollableState +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performTouchInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestScenes.SceneA +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ContentTest { + @get:Rule val rule = createComposeRule() + + @Test + fun disableSwipesWhenScrolling() { + lateinit var layoutImpl: SceneTransitionLayoutImpl + rule.setContent { + SceneTransitionLayoutForTesting( + remember { MutableSceneTransitionLayoutState(SceneA) }, + onLayoutImpl = { layoutImpl = it }, + ) { + scene(SceneA) { + Box( + Modifier.fillMaxSize() + .disableSwipesWhenScrolling() + .scrollable(rememberScrollableState { it }, Orientation.Vertical) + ) + } + } + } + + val content = layoutImpl.content(SceneA) + assertThat(content.areSwipesAllowed()).isTrue() + rule.onRoot().performTouchInput { + down(topLeft) + moveBy(bottomLeft) + } + + assertThat(content.areSwipesAllowed()).isFalse() + rule.onRoot().performTouchInput { up() } + assertThat(content.areSwipesAllowed()).isTrue() + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 5a35d11c0b29..dbac62ffb713 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -247,32 +247,26 @@ class DraggableHandlerTest { suspend fun DragController.onDragStoppedAnimateNow( velocity: Float, - canChangeScene: Boolean = true, onAnimationStart: () -> Unit, onAnimationEnd: (Float) -> Unit, ) { - val velocityConsumed = onDragStoppedAnimateLater(velocity, canChangeScene) + val velocityConsumed = onDragStoppedAnimateLater(velocity) onAnimationStart() onAnimationEnd(velocityConsumed.await()) } suspend fun DragController.onDragStoppedAnimateNow( velocity: Float, - canChangeScene: Boolean = true, onAnimationStart: () -> Unit, ) = onDragStoppedAnimateNow( velocity = velocity, - canChangeScene = canChangeScene, onAnimationStart = onAnimationStart, onAnimationEnd = {}, ) - fun DragController.onDragStoppedAnimateLater( - velocity: Float, - canChangeScene: Boolean = true, - ): Deferred<Float> { - val velocityConsumed = testScope.async { onStop(velocity, canChangeScene) } + fun DragController.onDragStoppedAnimateLater(velocity: Float): Deferred<Float> { + val velocityConsumed = testScope.async { onStop(velocity) } testScope.testScheduler.runCurrent() return velocityConsumed } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt index 4153350fce60..5c6f91bb0e90 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt @@ -72,12 +72,12 @@ class MultiPointerDraggableTest { return delta } - override suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float { + override suspend fun onStop(velocity: Float): Float { onStop.invoke(velocity) return velocity } - override fun onCancel(canChangeContent: Boolean) { + override fun onCancel() { error("MultiPointerDraggable never calls onCancel()") } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index 7c8c6e5f6c12..e580e3c40690 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -21,6 +21,7 @@ import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size @@ -33,6 +34,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assertHeightIsEqualTo @@ -43,6 +45,9 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onChild import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset @@ -469,4 +474,41 @@ class SceneTransitionLayoutTest { assertThat(layoutImpl.overlaysOrNullForTest()).isNull() } + + @Test + fun transitionProgressBoundedBetween0And1() { + val layoutWidth = 200.dp + val layoutHeight = 400.dp + + // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is + // detected as a drag event. + var touchSlop = 0f + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(initialScene = SceneA) } + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) { + scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { + Spacer(Modifier.fillMaxSize()) + } + scene(SceneB) { Spacer(Modifier.fillMaxSize()) } + } + } + assertThat(state.transitionState).isIdle() + + rule.mainClock.autoAdvance = false + + // Swipe the verticalSwipeDistance. + rule.onRoot().performTouchInput { + swipeDown(endY = bottom + touchSlop, durationMillis = 50) + } + + rule.mainClock.advanceTimeBy(16) + val transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).isNotNull() + assertThat(transition).hasProgress(1f, tolerance = 0.01f) + + rule.mainClock.advanceTimeBy(16) + // Fling animation, we are overscrolling now. Progress should always be between [0, 1]. + assertThat(transition).hasProgress(1f) + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index 9135fdd15b3a..e80805a4e374 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -936,4 +936,45 @@ class SwipeToSceneTest { assertThat(state.transitionState).isIdle() assertThat(state.transitionState).hasCurrentScene(SceneC) } + + @Test + fun swipeToSceneNodeIsKeptWhenDisabled() { + var hasHorizontalActions by mutableStateOf(false) + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene( + SceneA, + userActions = + buildList { + add(Swipe.Down to SceneB) + + if (hasHorizontalActions) { + add(Swipe.Left to SceneC) + } + } + .toMap(), + ) { + Box(Modifier.fillMaxSize()) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + } + } + + // Swipe down to start a transition to B. + rule.onRoot().performTouchInput { + down(middle) + moveBy(Offset(0f, touchSlop)) + } + + assertThat(state.transitionState).isSceneTransition() + + // Add new horizontal user actions. This should not stop the current transition, even if a + // new horizontal Modifier.swipeToScene() handler is introduced where the vertical one was. + hasHorizontalActions = true + rule.waitForIdle() + assertThat(state.transitionState).isSceneTransition() + } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index aed3a2df0436..e69fa994931d 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -50,7 +50,6 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController DEFAULT_CLOCK_ID, clockCtx.resources.getString(R.string.clock_default_name), clockCtx.resources.getString(R.string.clock_default_description), - isReactiveToTone = true, ) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index 0f8ca947479b..2b0825f39243 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -30,7 +30,6 @@ import android.util.AttributeSet import android.util.Log import android.util.MathUtils import android.util.TypedValue -import android.view.View.MeasureSpec.AT_MOST import android.view.View.MeasureSpec.EXACTLY import android.view.animation.Interpolator import android.widget.TextView @@ -77,7 +76,6 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe var maxSingleDigitWidth = -1 var digitTranslateAnimator: DigitTranslateAnimator? = null var aodFontSizePx: Float = -1F - var isVertical: Boolean = false // Store the font size when there's no height constraint as a reference when adjusting font size private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE @@ -148,16 +146,7 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { logger.d("onMeasure()") - if (isVertical) { - // use at_most to avoid apply measuredWidth from last measuring to measuredHeight - // cause we use max to setMeasuredDimension - super.onMeasure( - MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), AT_MOST), - MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), AT_MOST), - ) - } else { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - } + super.onMeasure(widthMeasureSpec, heightMeasureSpec) val layout = this.layout if (layout != null) { @@ -213,18 +202,10 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe ) } - if (isVertical) { - expectedWidth = expectedHeight.also { expectedHeight = expectedWidth } - } setMeasuredDimension(expectedWidth, expectedHeight) } override fun onDraw(canvas: Canvas) { - if (isVertical) { - canvas.save() - canvas.translate(0F, measuredHeight.toFloat()) - canvas.rotate(-90F) - } logger.d({ "onDraw(); ls: $str1" }) { str1 = textAnimator.textInterpolator.shapedText } val translation = getLocalTranslation() canvas.translate(translation.x.toFloat(), translation.y.toFloat()) @@ -238,9 +219,6 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe canvas.translate(-it.updatedTranslate.x.toFloat(), -it.updatedTranslate.y.toFloat()) } canvas.translate(-translation.x.toFloat(), -translation.y.toFloat()) - if (isVertical) { - canvas.restore() - } } override fun invalidate() { @@ -353,18 +331,20 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe } private fun updateXtranslation(inPoint: Point, interpolatedTextBounds: Rect): Point { - val viewWidth = if (isVertical) measuredHeight else measuredWidth when (horizontalAlignment) { HorizontalAlignment.LEFT -> { inPoint.x = lockScreenPaint.strokeWidth.toInt() - interpolatedTextBounds.left } HorizontalAlignment.RIGHT -> { inPoint.x = - viewWidth - interpolatedTextBounds.right - lockScreenPaint.strokeWidth.toInt() + measuredWidth - + interpolatedTextBounds.right - + lockScreenPaint.strokeWidth.toInt() } HorizontalAlignment.CENTER -> { inPoint.x = - (viewWidth - interpolatedTextBounds.width()) / 2 - interpolatedTextBounds.left + (measuredWidth - interpolatedTextBounds.width()) / 2 - + interpolatedTextBounds.left } } return inPoint @@ -373,7 +353,6 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe // translation of reference point of text // used for translation when calling textInterpolator private fun getLocalTranslation(): Point { - val viewHeight = if (isVertical) measuredWidth else measuredHeight val interpolatedTextBounds = updateInterpolatedTextBounds() val localTranslation = Point(0, 0) val correctedBaseline = if (baseline != -1) baseline else baselineFromMeasure @@ -381,7 +360,7 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe when (verticalAlignment) { VerticalAlignment.CENTER -> { localTranslation.y = - ((viewHeight - interpolatedTextBounds.height()) / 2 - + ((measuredHeight - interpolatedTextBounds.height()) / 2 - interpolatedTextBounds.top - correctedBaseline) } @@ -392,7 +371,7 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe } VerticalAlignment.BOTTOM -> { localTranslation.y = - viewHeight - + measuredHeight - interpolatedTextBounds.bottom - lockScreenPaint.strokeWidth.toInt() - correctedBaseline diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 2c1dacdfae73..4d2a6d9bd57a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -232,7 +232,6 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Test fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() { val pinViewController = constructPinViewController(mockKeyguardPinView) - `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true) `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(3) @@ -249,7 +248,6 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Test fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() { val pinViewController = constructPinViewController(mockKeyguardPinView) - `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true) `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(6) @@ -275,7 +273,6 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Test fun onUserInput_autoConfirmation_attemptsUnlock() { val pinViewController = constructPinViewController(mockKeyguardPinView) - whenever(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) whenever(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) whenever(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true) whenever(passwordTextView.text).thenReturn("000000") diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt index a11dace0505c..4c329dcf2f2b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt @@ -18,12 +18,20 @@ package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothLeBroadcast import android.bluetooth.BluetoothLeBroadcastMetadata +import android.content.ContentResolver +import android.content.applicationContext import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.BluetoothEventManager import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager +import com.android.settingslib.bluetooth.VolumeControlProfile +import com.android.settingslib.volume.shared.AudioSharingLogger import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -38,10 +46,16 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.times +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -50,8 +64,11 @@ import org.mockito.kotlin.any class AudioSharingInteractorTest : SysuiTestCase() { @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() private val kosmos = testKosmos() + @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast + @Mock private lateinit var bluetoothLeBroadcastMetadata: BluetoothLeBroadcastMetadata + @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback> private lateinit var underTest: AudioSharingInteractor @@ -157,13 +174,15 @@ class AudioSharingInteractorTest : SysuiTestCase() { fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) + runCurrent() assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() job.cancel() @@ -174,15 +193,14 @@ class AudioSharingInteractorTest : SysuiTestCase() { fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() - verify(localBluetoothLeBroadcast) - .registerServiceCallBack(any(), callbackCaptor.capture()) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) runCurrent() assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() @@ -194,13 +212,15 @@ class AudioSharingInteractorTest : SysuiTestCase() { fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + runCurrent() verify(localBluetoothLeBroadcast) .registerServiceCallBack(any(), callbackCaptor.capture()) runCurrent() @@ -211,4 +231,100 @@ class AudioSharingInteractorTest : SysuiTestCase() { job.cancel() } } + + @Test + fun testHandleAudioSourceWhenReady_skipInitialValue_noAudioSharing_sourceNotAdded() = + with(kosmos) { + testScope.runTest { + val (broadcast, repository) = setupRepositoryImpl() + val interactor = + object : + AudioSharingInteractorImpl( + applicationContext, + localBluetoothManager, + repository, + testDispatcher, + ) { + override suspend fun audioSharingAvailable() = true + } + val job = launch { interactor.handleAudioSourceWhenReady() } + runCurrent() + // Verify callback registered for onBroadcastStartedOrStopped + verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture()) + runCurrent() + // Verify source is not added + verify(repository, never()).addSource() + job.cancel() + } + } + + @Test + fun testHandleAudioSourceWhenReady_skipInitialValue_newAudioSharing_sourceAdded() = + with(kosmos) { + testScope.runTest { + val (broadcast, repository) = setupRepositoryImpl() + val interactor = + object : + AudioSharingInteractorImpl( + applicationContext, + localBluetoothManager, + repository, + testDispatcher, + ) { + override suspend fun audioSharingAvailable() = true + } + val job = launch { interactor.handleAudioSourceWhenReady() } + runCurrent() + // Verify callback registered for onBroadcastStartedOrStopped + verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture()) + // Audio sharing started, trigger onBroadcastStarted + whenever(broadcast.isEnabled(null)).thenReturn(true) + callbackCaptor.value.onBroadcastStarted(0, 0) + runCurrent() + // Verify callback registered for onBroadcastMetadataChanged + verify(broadcast, times(2)).registerServiceCallBack(any(), callbackCaptor.capture()) + runCurrent() + // Trigger onBroadcastMetadataChanged (ready to add source) + callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata) + runCurrent() + // Verify source added + verify(repository).addSource() + job.cancel() + } + } + + private fun setupRepositoryImpl(): Pair<LocalBluetoothLeBroadcast, AudioSharingRepositoryImpl> { + with(kosmos) { + val broadcast = + mock<LocalBluetoothLeBroadcast> { + on { isProfileReady } doReturn true + on { isEnabled(null) } doReturn false + } + val assistant = + mock<LocalBluetoothLeBroadcastAssistant> { on { isProfileReady } doReturn true } + val volumeControl = mock<VolumeControlProfile> { on { isProfileReady } doReturn true } + val profileManager = + mock<LocalBluetoothProfileManager> { + on { leAudioBroadcastProfile } doReturn broadcast + on { leAudioBroadcastAssistantProfile } doReturn assistant + on { volumeControlProfile } doReturn volumeControl + } + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(localBluetoothManager.eventManager).thenReturn(mock<BluetoothEventManager> {}) + + val repository = + AudioSharingRepositoryImpl( + localBluetoothManager, + com.android.settingslib.volume.data.repository.AudioSharingRepositoryImpl( + mock<ContentResolver> {}, + localBluetoothManager, + testScope.backgroundScope, + testScope.testScheduler, + mock<AudioSharingLogger> {}, + ), + testDispatcher, + ) + return Pair(broadcast, spy(repository)) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt index acfe9dd45f75..f0746064f67f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt @@ -111,6 +111,28 @@ class AudioSharingRepositoryTest : SysuiTestCase() { } @Test + fun testStopAudioSharing() = + with(kosmos) { + testScope.runTest { + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile) + audioSharingRepository.setAudioSharingAvailable(true) + underTest.stopAudioSharing() + verify(leAudioBroadcastProfile).stopLatestBroadcast() + } + } + + @Test + fun testStopAudioSharing_flagOff_doNothing() = + with(kosmos) { + testScope.runTest { + audioSharingRepository.setAudioSharingAvailable(false) + underTest.stopAudioSharing() + verify(leAudioBroadcastProfile, never()).stopLatestBroadcast() + } + } + + @Test fun testAddSource_flagOff_doesNothing() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt index 44f9720cb9e4..ad0337e5ce86 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt @@ -15,14 +15,15 @@ */ package com.android.systemui.bluetooth.qsdialog -import androidx.test.ext.junit.runners.AndroidJUnit4 import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -48,6 +49,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { private lateinit var notConnectedDeviceItem: DeviceItem private lateinit var connectedMediaDeviceItem: DeviceItem private lateinit var connectedOtherDeviceItem: DeviceItem + private lateinit var audioSharingDeviceItem: DeviceItem @Mock private lateinit var dialog: SystemUIDialog @Before @@ -59,7 +61,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, ) notConnectedDeviceItem = DeviceItem( @@ -68,7 +70,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, ) connectedMediaDeviceItem = DeviceItem( @@ -77,7 +79,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, ) connectedOtherDeviceItem = DeviceItem( @@ -86,7 +88,16 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, + ) + audioSharingDeviceItem = + DeviceItem( + type = DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null, ) actionInteractorImpl = kosmos.deviceItemActionInteractorImpl } @@ -135,6 +146,29 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { } } + @Test + fun onActionIconClick_onIntent() { + with(kosmos) { + testScope.runTest { + var onIntentCalledOnAddress = "" + whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) + actionInteractorImpl.onActionIconClick(connectedMediaDeviceItem) { + onIntentCalledOnAddress = connectedMediaDeviceItem.cachedBluetoothDevice.address + } + assertThat(onIntentCalledOnAddress).isEqualTo(DEVICE_ADDRESS) + } + } + } + + @Test(expected = IllegalArgumentException::class) + fun onActionIconClick_audioSharingDeviceType_throwException() { + with(kosmos) { + testScope.runTest { + actionInteractorImpl.onActionIconClick(audioSharingDeviceItem) {} + } + } + } + private companion object { const val DEVICE_NAME = "device" const val DEVICE_CONNECTION_SUMMARY = "active" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt index b33a83cf202a..a65415509d38 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt @@ -69,6 +69,7 @@ import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.scene.ui.composable.SceneContainer import com.android.systemui.scene.ui.composable.SceneContainerTransitions +import com.android.systemui.scene.ui.view.sceneJankMonitorFactory import com.android.systemui.testKosmos import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.awaitCancellation @@ -193,6 +194,7 @@ class BouncerPredictiveBackTest : SysuiTestCase() { overlayByKey = emptyMap(), dataSourceDelegator = kosmos.sceneDataSourceDelegator, qsSceneAdapter = { kosmos.fakeQsSceneAdapter }, + sceneJankMonitorFactory = kosmos.sceneJankMonitorFactory, ) } }, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayoutTest.kt index 3ede841bb25b..b4b41787d833 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayoutTest.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.android.systemui.bouncer.ui.helper +package com.android.systemui.bouncer.ui.composable import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.BELOW_USER_SWITCHER -import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.BESIDE_USER_SWITCHER -import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.SPLIT_BOUNCER -import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.STANDARD_BOUNCER +import com.android.systemui.bouncer.ui.composable.BouncerSceneLayout.BELOW_USER_SWITCHER +import com.android.systemui.bouncer.ui.composable.BouncerSceneLayout.BESIDE_USER_SWITCHER +import com.android.systemui.bouncer.ui.composable.BouncerSceneLayout.SPLIT_BOUNCER +import com.android.systemui.bouncer.ui.composable.BouncerSceneLayout.STANDARD_BOUNCER import com.google.common.truth.Truth.assertThat import java.util.Locale import org.junit.Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index 20d66155e5ca..6c955bf1818d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -256,6 +256,22 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @EnableSceneContainer @Test + fun playSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() = + testScope.runTest { + underTest = kosmos.deviceEntryHapticsInteractor + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + + enrollFace() + kosmos.configureKeyguardBypass(isBypassAvailable = false) + runCurrent() + configureDeviceEntryFromBiometricSource(isFaceUnlock = true, bypassEnabled = false) + kosmos.fakeDeviceEntryFaceAuthRepository.isAuthenticated.value = true + + assertThat(playSuccessHaptic).isNotNull() + } + + @EnableSceneContainer + @Test fun skipSuccessHaptic_onDeviceEntryFromSfps_whenPowerDown_sceneContainer() = testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) @@ -299,6 +315,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { private fun configureDeviceEntryFromBiometricSource( isFpUnlock: Boolean = false, isFaceUnlock: Boolean = false, + bypassEnabled: Boolean = true, ) { // Mock DeviceEntrySourceInteractor#deviceEntryBiometricAuthSuccessState if (isFpUnlock) { @@ -314,11 +331,14 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { ) // Mock DeviceEntrySourceInteractor#faceWakeAndUnlockMode = MODE_UNLOCK_COLLAPSING - kosmos.sceneInteractor.setTransitionState( - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(Scenes.Lockscreen) + // if the successful face authentication will bypass keyguard + if (bypassEnabled) { + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(Scenes.Lockscreen) + ) ) - ) + } } underTest = kosmos.deviceEntryHapticsInteractor } diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt index 74e8257f4f08..5e023a203267 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt @@ -292,8 +292,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue + 1) } @@ -306,8 +305,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } @@ -321,8 +319,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue + 1) } @@ -335,8 +332,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } @@ -347,8 +343,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) assertThat(model?.lastShortcutTriggeredTime).isNull() - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType) assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant()) } @@ -358,15 +353,14 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge testScope.runTest { setUpForDeviceConnection() tutorialSchedulerRepository.setScheduledTutorialLaunchTime( - DeviceType.TOUCHPAD, + getTargetDevice(gestureType), eduClock.instant(), ) val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount eduClock.offset(initialDelayElapsedDuration) - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue + 1) } @@ -376,33 +370,92 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge testScope.runTest { setUpForDeviceConnection() tutorialSchedulerRepository.setScheduledTutorialLaunchTime( - DeviceType.TOUCHPAD, + getTargetDevice(gestureType), eduClock.instant(), ) val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount // No offset to the clock to simulate update before initial delay - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } @Test - fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() = + fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchOrNotifyTime() = testScope.runTest { - // No update to OOBE launch time to simulate no OOBE is launched yet + // No update to OOBE launch/notify time to simulate no OOBE is launched yet setUpForDeviceConnection() val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } + @Test + fun dataUpdatedOnIncrementSignalCountAfterNotifyTimeDelayWithoutLaunchTime() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.setNotifiedTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + eduClock.offset(initialDelayElapsedDuration) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountBeforeLaunchTimeDelayWithNotifyTime() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.setNotifiedTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + eduClock.offset(initialDelayElapsedDuration) + + tutorialSchedulerRepository.setScheduledTutorialLaunchTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + // No offset to the clock to simulate update before initial delay of launch time + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUpdatedOnIncrementSignalCountAfterLaunchTimeDelayWithNotifyTime() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.setNotifiedTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + eduClock.offset(initialDelayElapsedDuration) + + tutorialSchedulerRepository.setScheduledTutorialLaunchTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + eduClock.offset(initialDelayElapsedDuration) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + private suspend fun setUpForInitialDelayElapse() { tutorialSchedulerRepository.setScheduledTutorialLaunchTime( DeviceType.TOUCHPAD, @@ -465,12 +518,18 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge keyboardRepository.setIsAnyKeyboardConnected(true) } - private fun getOverviewProxyListener(): OverviewProxyListener { + private fun updateContextualEduStats(isTrackpadGesture: Boolean, gestureType: GestureType) { val listenerCaptor = argumentCaptor<OverviewProxyListener>() verify(overviewProxyService).addCallback(listenerCaptor.capture()) - return listenerCaptor.firstValue + listenerCaptor.firstValue.updateContextualEduStats(isTrackpadGesture, gestureType) } + private fun getTargetDevice(gestureType: GestureType) = + when (gestureType) { + ALL_APPS -> DeviceType.KEYBOARD + else -> DeviceType.TOUCHPAD + } + companion object { private val USER_INFOS = listOf(UserInfo(101, "Second User", 0)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt index 692b9c67f322..692b9c67f322 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt index e659ef274980..698fac107a1d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt @@ -18,7 +18,9 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context import android.content.Context.INPUT_SERVICE +import android.content.Intent import android.hardware.input.InputGestureData +import android.hardware.input.InputManager import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS import android.hardware.input.fakeInputManager import android.platform.test.annotations.EnableFlags @@ -27,9 +29,12 @@ import androidx.test.filters.SmallTest import com.android.hardware.input.Flags.FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyboard.shortcut.customInputGesturesRepository import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsInputGestureData +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData +import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper import com.android.systemui.kosmos.testScope import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.userTracker @@ -48,18 +53,41 @@ import org.mockito.kotlin.whenever @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER) class CustomInputGesturesRepositoryTest : SysuiTestCase() { - private val mockUserContext: Context = mock() + private val primaryUserContext: Context = mock() + private val secondaryUserContext: Context = mock() + private var activeUserContext: Context = primaryUserContext + private val kosmos = testKosmos().also { - it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext }) + it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { activeUserContext }) } private val inputManager = kosmos.fakeInputManager.inputManager + private val broadcastDispatcher = kosmos.broadcastDispatcher + private val inputManagerForSecondaryUser: InputManager = mock() private val testScope = kosmos.testScope + private val testHelper = kosmos.shortcutHelperTestHelper private val customInputGesturesRepository = kosmos.customInputGesturesRepository @Before - fun setup(){ - whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager) + fun setup() { + activeUserContext = primaryUserContext + whenever(primaryUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager) + whenever(secondaryUserContext.getSystemService(INPUT_SERVICE)) + .thenReturn(inputManagerForSecondaryUser) + } + + @Test + fun customInputGestures_emitsNewUsersInputGesturesWhenUserIsSwitch() { + testScope.runTest { + setCustomInputGesturesForPrimaryUser(allAppsInputGestureData) + setCustomInputGesturesForSecondaryUser(goHomeInputGestureData) + + val inputGestures by collectLastValue(customInputGesturesRepository.customInputGestures) + assertThat(inputGestures).containsExactly(allAppsInputGestureData) + + switchToSecondaryUser() + assertThat(inputGestures).containsExactly(goHomeInputGestureData) + } } @Test @@ -115,4 +143,24 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { } } + private fun setCustomInputGesturesForPrimaryUser(vararg inputGesture: InputGestureData) { + whenever( + inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) + ).thenReturn(inputGesture.toList()) + } + + private fun setCustomInputGesturesForSecondaryUser(vararg inputGesture: InputGestureData) { + whenever( + inputManagerForSecondaryUser.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) + ).thenReturn(inputGesture.toList()) + } + + private fun switchToSecondaryUser() { + activeUserContext = secondaryUserContext + broadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(Intent.ACTION_USER_SWITCHED) + ) + } + }
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index 7c88d76f28bd..183e4d6f624b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -24,6 +24,7 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME import android.os.SystemClock import android.view.KeyEvent import android.view.KeyEvent.ACTION_DOWN +import android.view.KeyEvent.ACTION_UP import android.view.KeyEvent.KEYCODE_A import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CTRL_ON @@ -540,11 +541,7 @@ object TestShortcuts { simpleShortcutCategory(System, "System apps", "Take a note"), simpleShortcutCategory(System, "System controls", "Take screenshot"), simpleShortcutCategory(System, "System controls", "Go back"), - simpleShortcutCategory( - MultiTasking, - "Split screen", - "Switch to full screen", - ), + simpleShortcutCategory(MultiTasking, "Split screen", "Switch to full screen"), simpleShortcutCategory( MultiTasking, "Split screen", @@ -704,7 +701,7 @@ object TestShortcuts { android.view.KeyEvent( /* downTime = */ SystemClock.uptimeMillis(), /* eventTime = */ SystemClock.uptimeMillis(), - /* action = */ ACTION_DOWN, + /* action = */ ACTION_UP, /* code = */ KEYCODE_A, /* repeat = */ 0, /* metaState = */ 0, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt index 755c218f6789..d9d34f5ace7b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt @@ -92,13 +92,14 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - assertThat(uiState).isEqualTo( - AddShortcutDialog( - shortcutLabel = "Standard shortcut", - defaultCustomShortcutModifierKey = - ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta), + assertThat(uiState) + .isEqualTo( + AddShortcutDialog( + shortcutLabel = "Standard shortcut", + defaultCustomShortcutModifierKey = + ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta), + ) ) - ) } } @@ -137,8 +138,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - assertThat((uiState as AddShortcutDialog).pressedKeys) - .isEmpty() + assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty() } } @@ -161,8 +161,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) - assertThat((uiState as AddShortcutDialog).errorMessage) - .isEmpty() + assertThat((uiState as AddShortcutDialog).errorMessage).isEmpty() } } @@ -244,32 +243,34 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } @Test - fun onKeyPressed_handlesKeyEvents_whereActionKeyIsAlsoPressed() { + fun onShortcutKeyCombinationSelected_handlesKeyEvents_whereActionKeyIsAlsoPressed() { testScope.runTest { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - val isHandled = viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) + val isHandled = + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) assertThat(isHandled).isTrue() } } @Test - fun onKeyPressed_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() { + fun onShortcutKeyCombinationSelected_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() { testScope.runTest { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - val isHandled = viewModel.onKeyPressed(keyDownEventWithoutActionKeyPressed) + val isHandled = + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithoutActionKeyPressed) assertThat(isHandled).isFalse() } } @Test - fun onKeyPressed_convertsKeyEventsAndUpdatesUiStatesPressedKey() { + fun onShortcutKeyCombinationSelected_convertsKeyEventsAndUpdatesUiStatesPressedKey() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) // Note that Action Key is excluded as it's already displayed on the UI assertThat((uiState as AddShortcutDialog).pressedKeys) @@ -282,8 +283,8 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) // Note that Action Key is excluded as it's already displayed on the UI assertThat((uiState as AddShortcutDialog).pressedKeys) @@ -292,16 +293,15 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { // Close the dialog and show it again viewModel.onDialogDismissed() viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - assertThat((uiState as AddShortcutDialog).pressedKeys) - .isEmpty() + assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty() } } private suspend fun openAddShortcutDialogAndSetShortcut() { viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) viewModel.onSetShortcut() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt index fde9b8ce6a50..bf49186a7f01 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt @@ -28,7 +28,7 @@ import android.provider.Settings.Secure.ZEN_DURATION_PROMPT import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.EnableZenModeDialog -import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription @@ -187,7 +187,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE) + zenModeRepository.activateMode(MANUAL_DND) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -2) collectLastValue(underTest.lockScreenState) runCurrent() @@ -233,7 +233,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER) collectLastValue(underTest.lockScreenState) runCurrent() @@ -278,7 +277,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { fun onTriggered_dndModeIsOff_settingNotFOREVERorPROMPT_dndWithDuration() = testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -900) runCurrent() @@ -323,7 +321,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { fun onTriggered_dndModeIsOff_settingIsPROMPT_showDialog() = testScope.runTest { val expandable: Expandable = mock() - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT) whenever(enableZenModeDialog.createDialog()).thenReturn(mock()) collectLastValue(underTest.lockScreenState) @@ -405,10 +402,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val lockScreenState by collectLastValue(underTest.lockScreenState) - val manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE - zenModeRepository.addMode(manualDnd) - runCurrent() - assertThat(lockScreenState) .isEqualTo( KeyguardQuickAffordanceConfig.LockScreenState.Visible( @@ -420,7 +413,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { ) ) - zenModeRepository.activateMode(manualDnd) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() assertThat(lockScreenState) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt index e60d971c7289..282bebcd629a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt @@ -25,13 +25,14 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.keyguardClockRepository import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.shared.model.ClockSize +import com.android.systemui.keyguard.shared.model.DozeStateModel +import com.android.systemui.keyguard.shared.model.DozeTransitionModel 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.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.data.repository.mediaFilterRepository @@ -75,25 +76,16 @@ class KeyguardClockInteractorTest : SysuiTestCase() { } @Test - @DisableSceneContainer - fun clockShouldBeCentered_sceneContainerFlagOff_basedOnRepository() = - testScope.runTest { - val value by collectLastValue(underTest.clockShouldBeCentered) - kosmos.keyguardInteractor.setClockShouldBeCentered(true) - assertThat(value).isTrue() - - kosmos.keyguardInteractor.setClockShouldBeCentered(false) - assertThat(value).isFalse() - } - - @Test @EnableSceneContainer fun clockSize_forceSmallClock_SMALL() = testScope.runTest { val value by collectLastValue(underTest.clockSize) kosmos.fakeKeyguardClockRepository.setShouldForceSmallClock(true) kosmos.fakeFeatureFlagsClassic.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, true) - transitionTo(KeyguardState.AOD, KeyguardState.LOCKSCREEN) + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + ) assertThat(value).isEqualTo(ClockSize.SMALL) } @@ -190,7 +182,10 @@ class KeyguardClockInteractorTest : SysuiTestCase() { val value by collectLastValue(underTest.clockShouldBeCentered) kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.activeNotificationListRepository.setActiveNotifs(1) - transitionTo(KeyguardState.LOCKSCREEN, KeyguardState.AOD) + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + ) assertThat(value).isTrue() } @@ -201,15 +196,187 @@ class KeyguardClockInteractorTest : SysuiTestCase() { val value by collectLastValue(underTest.clockShouldBeCentered) kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.activeNotificationListRepository.setActiveNotifs(1) - transitionTo(KeyguardState.AOD, KeyguardState.LOCKSCREEN) + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + ) assertThat(value).isFalse() } - private suspend fun transitionTo(from: KeyguardState, to: KeyguardState) { - with(kosmos.fakeKeyguardTransitionRepository) { - sendTransitionStep(TransitionStep(from, to, 0f, TransitionState.STARTED)) - sendTransitionStep(TransitionStep(from, to, 0.5f, TransitionState.RUNNING)) - sendTransitionStep(TransitionStep(from, to, 1f, TransitionState.FINISHED)) + @Test + @DisableSceneContainer + fun clockShouldBeCentered_sceneContainerFlagOff_notSplitMode_true() = + testScope.runTest { + val value by collectLastValue(underTest.clockShouldBeCentered) + kosmos.shadeRepository.setShadeLayoutWide(false) + assertThat(value).isTrue() + } + + @Test + @DisableSceneContainer + fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_lockscreen_withNotifs_false() = + testScope.runTest { + val value by collectLastValue(underTest.clockShouldBeCentered) + kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.activeNotificationListRepository.setActiveNotifs(1) + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + ) + assertThat(value).isFalse() + } + + @Test + @DisableSceneContainer + fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_lockscreen_withoutNotifs_true() = + testScope.runTest { + val value by collectLastValue(underTest.clockShouldBeCentered) + kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.activeNotificationListRepository.setActiveNotifs(0) + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + ) + assertThat(value).isTrue() + } + + @Test + @DisableSceneContainer + fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_LsToAod_withNotifs_true() = + testScope.runTest { + val value by collectLastValue(underTest.clockShouldBeCentered) + kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.activeNotificationListRepository.setActiveNotifs(1) + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.OFF, + KeyguardState.LOCKSCREEN, + ) + assertThat(value).isFalse() + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + ) + assertThat(value).isTrue() + } + + @Test + @DisableSceneContainer + fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_AodToLs_withNotifs_false() = + testScope.runTest { + val value by collectLastValue(underTest.clockShouldBeCentered) + kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.activeNotificationListRepository.setActiveNotifs(1) + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + ) + assertThat(value).isTrue() + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + ) + assertThat(value).isFalse() + } + + @Test + @DisableSceneContainer + fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_Aod_withPulsingNotifs_false() = + testScope.runTest { + val value by collectLastValue(underTest.clockShouldBeCentered) + kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + ) + assertThat(value).isTrue() + kosmos.fakeKeyguardRepository.setDozeTransitionModel( + DozeTransitionModel( + from = DozeStateModel.DOZE_AOD, + to = DozeStateModel.DOZE_PULSING, + ) + ) + assertThat(value).isFalse() + } + + @Test + @DisableSceneContainer + fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_LStoGone_withoutNotifs_true() = + testScope.runTest { + val value by collectLastValue(underTest.clockShouldBeCentered) + kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.activeNotificationListRepository.setActiveNotifs(0) + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.OFF, + KeyguardState.LOCKSCREEN, + ) + assertThat(value).isTrue() + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.GONE, + ) + kosmos.activeNotificationListRepository.setActiveNotifs(1) + assertThat(value).isTrue() + } + + @Test + @DisableSceneContainer + fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_AodOn_GoneToAOD() = + testScope.runTest { + val value by collectLastValue(underTest.clockShouldBeCentered) + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + ) + kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.activeNotificationListRepository.setActiveNotifs(0) + assertThat(value).isTrue() + + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.GONE, + ) + kosmos.activeNotificationListRepository.setActiveNotifs(1) + assertThat(value).isTrue() + + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.GONE, + KeyguardState.AOD, + ) + assertThat(value).isTrue() + } + + @Test + @DisableSceneContainer + fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_AodOff_GoneToDoze() = + testScope.runTest { + val value by collectLastValue(underTest.clockShouldBeCentered) + kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.DOZING, + KeyguardState.LOCKSCREEN, + ) + kosmos.activeNotificationListRepository.setActiveNotifs(0) + assertThat(value).isTrue() + + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.GONE, + ) + kosmos.activeNotificationListRepository.setActiveNotifs(1) + assertThat(value).isTrue() + + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.GONE, + KeyguardState.DOZING, + ) + kosmos.activeNotificationListRepository.setActiveNotifs(1) + assertThat(value).isTrue() + + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.DOZING, + KeyguardState.LOCKSCREEN, + ) + kosmos.activeNotificationListRepository.setActiveNotifs(0) + assertThat(value).isTrue() } - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt index b3417b9de36d..c44f27ef348b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -46,8 +46,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope -import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest -import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes @@ -76,7 +74,6 @@ class KeyguardInteractorTest : SysuiTestCase() { private val configRepository by lazy { kosmos.fakeConfigurationRepository } private val bouncerRepository by lazy { kosmos.keyguardBouncerRepository } private val shadeRepository by lazy { kosmos.shadeRepository } - private val powerInteractor by lazy { kosmos.powerInteractor } private val keyguardRepository by lazy { kosmos.keyguardRepository } private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } @@ -444,7 +441,6 @@ class KeyguardInteractorTest : SysuiTestCase() { repository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - powerInteractor.setAwakeForTest() advanceTimeBy(1000L) assertThat(isAbleToDream).isEqualTo(false) @@ -460,9 +456,6 @@ class KeyguardInteractorTest : SysuiTestCase() { repository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - powerInteractor.setAwakeForTest() - runCurrent() - // After some delay, still false advanceTimeBy(300L) assertThat(isAbleToDream).isEqualTo(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt index b0698555941c..98e3c68e6e33 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest @@ -406,4 +407,48 @@ class KeyguardWakeDirectlyToGoneInteractorTest : SysuiTestCase() { // It should not have any effect. assertEquals(listOf(false, true, false, true), canWake) } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testCanWakeDirectlyToGone_falseAsSoonAsTransitionsAwayFromGone() = + testScope.runTest { + val canWake by collectValues(underTest.canWakeDirectlyToGone) + + assertEquals( + listOf( + false // Defaults to false. + ), + canWake, + ) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope, + ) + + assertEquals( + listOf( + false, + true, // Because we're GONE. + ), + canWake, + ) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope = testScope, + throughTransitionState = TransitionState.RUNNING, + ) + + assertEquals( + listOf( + false, + true, + false, // False as soon as we start a transition away from GONE. + ), + canWake, + ) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt index 87ab3c89a671..1cf45f8f8b8e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt @@ -27,7 +27,6 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.customization.R as customR -import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.keyguardSmartspaceInteractor @@ -156,7 +155,6 @@ class ClockSectionTest : SysuiTestCase() { shadeRepository.setShadeLayoutWide(false) keyguardClockInteractor.setClockSize(ClockSize.LARGE) - fakeKeyguardRepository.setClockShouldBeCentered(true) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) keyguardSmartspaceInteractor.setBcSmartspaceVisibility(VISIBLE) fakeConfigurationController.notifyConfigurationChanged() @@ -181,7 +179,6 @@ class ClockSectionTest : SysuiTestCase() { shadeRepository.setShadeLayoutWide(true) keyguardClockInteractor.setClockSize(ClockSize.LARGE) - fakeKeyguardRepository.setClockShouldBeCentered(true) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) keyguardSmartspaceInteractor.setBcSmartspaceVisibility(VISIBLE) fakeConfigurationController.notifyConfigurationChanged() @@ -206,7 +203,6 @@ class ClockSectionTest : SysuiTestCase() { shadeRepository.setShadeLayoutWide(false) keyguardClockInteractor.setClockSize(ClockSize.LARGE) - fakeKeyguardRepository.setClockShouldBeCentered(true) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) keyguardSmartspaceInteractor.setBcSmartspaceVisibility(VISIBLE) fakeConfigurationController.notifyConfigurationChanged() @@ -230,7 +226,6 @@ class ClockSectionTest : SysuiTestCase() { shadeRepository.setShadeLayoutWide(true) keyguardClockInteractor.setClockSize(ClockSize.SMALL) - fakeKeyguardRepository.setClockShouldBeCentered(true) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) keyguardSmartspaceInteractor.setBcSmartspaceVisibility(VISIBLE) fakeConfigurationController.notifyConfigurationChanged() @@ -254,7 +249,6 @@ class ClockSectionTest : SysuiTestCase() { shadeRepository.setShadeLayoutWide(false) keyguardClockInteractor.setClockSize(ClockSize.SMALL) - fakeKeyguardRepository.setClockShouldBeCentered(true) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) keyguardSmartspaceInteractor.setBcSmartspaceVisibility(VISIBLE) fakeConfigurationController.notifyConfigurationChanged() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt index feaf06aca29a..ade7614ae853 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt @@ -16,10 +16,13 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues +import com.android.systemui.flags.BrokenWithSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -72,6 +75,28 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() } @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notifications_areFullyVisible_whenShadeIsOpen() = + testScope.runTest { + val values by collectValues(underTest.notificationAlpha) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.1f), + step(0.2f), + step(0.3f), + step(1f), + ), + testScope, + ) + + values.forEach { assertThat(it).isEqualTo(1f) } + } + + @Test fun blurRadiusGoesToMaximumWhenShadeIsExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -88,6 +113,25 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() } @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notificationBlur_isNonZero_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationBlurRadius) + + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + + @Test fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt index 05a6b8785daf..8a599a1bd948 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt @@ -20,15 +20,15 @@ import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.BrokenWithSceneContainer import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.keyguardClockRepository -import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.keyguard.shared.model.ClockSizeSetting +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel.ClockLayout import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.clocks.ClockConfig @@ -37,6 +37,8 @@ import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever @@ -87,7 +89,11 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() with(kosmos) { shadeRepository.setShadeLayoutWide(true) - keyguardRepository.setClockShouldBeCentered(true) + kosmos.activeNotificationListRepository.setActiveNotifs(0) + fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + ) keyguardClockRepository.setClockSize(ClockSize.LARGE) } @@ -95,14 +101,18 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test - @BrokenWithSceneContainer(339465026) + @EnableSceneContainer fun currentClockLayout_splitShadeOn_clockNotCentered_largeClock_splitShadeLargeClock() = testScope.runTest { val currentClockLayout by collectLastValue(underTest.currentClockLayout) with(kosmos) { shadeRepository.setShadeLayoutWide(true) - keyguardRepository.setClockShouldBeCentered(false) + activeNotificationListRepository.setActiveNotifs(1) + fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + ) keyguardClockRepository.setClockSize(ClockSize.LARGE) } @@ -110,42 +120,46 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test - @BrokenWithSceneContainer(339465026) - fun currentClockLayout_splitShadeOn_clockNotCentered_smallClock_splitShadeSmallClock() = + @EnableSceneContainer + fun currentClockLayout_splitShadeOn_clockNotCentered_forceSmallClock_splitShadeSmallClock() = testScope.runTest { val currentClockLayout by collectLastValue(underTest.currentClockLayout) with(kosmos) { shadeRepository.setShadeLayoutWide(true) - keyguardRepository.setClockShouldBeCentered(false) - keyguardClockRepository.setClockSize(ClockSize.SMALL) + activeNotificationListRepository.setActiveNotifs(1) + fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + ) + fakeKeyguardClockRepository.setShouldForceSmallClock(true) } assertThat(currentClockLayout).isEqualTo(ClockLayout.SPLIT_SHADE_SMALL_CLOCK) } @Test - @BrokenWithSceneContainer(339465026) - fun currentClockLayout_singleShade_smallClock_smallClock() = + @EnableSceneContainer + fun currentClockLayout_singleShade_withNotifs_smallClock() = testScope.runTest { val currentClockLayout by collectLastValue(underTest.currentClockLayout) with(kosmos) { shadeRepository.setShadeLayoutWide(false) - keyguardClockRepository.setClockSize(ClockSize.SMALL) + activeNotificationListRepository.setActiveNotifs(1) } assertThat(currentClockLayout).isEqualTo(ClockLayout.SMALL_CLOCK) } @Test - fun currentClockLayout_singleShade_largeClock_largeClock() = + fun currentClockLayout_singleShade_withoutNotifs_largeClock() = testScope.runTest { val currentClockLayout by collectLastValue(underTest.currentClockLayout) with(kosmos) { shadeRepository.setShadeLayoutWide(false) - keyguardClockRepository.setClockSize(ClockSize.LARGE) + activeNotificationListRepository.setActiveNotifs(0) } assertThat(currentClockLayout).isEqualTo(ClockLayout.LARGE_CLOCK) @@ -195,7 +209,7 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test - @BrokenWithSceneContainer(339465026) + @DisableSceneContainer fun testClockSize_dynamicClockSize() = testScope.runTest { with(kosmos) { @@ -219,7 +233,7 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test - @BrokenWithSceneContainer(339465026) + @DisableSceneContainer fun isLargeClockVisible_whenSmallClockSize_isFalse() = testScope.runTest { val value by collectLastValue(underTest.isLargeClockVisible) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt index d909c5ab5f1b..914094fa39df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt @@ -16,9 +16,11 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -153,7 +155,7 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza } @Test - @BrokenWithSceneContainer(330311871) + @BrokenWithSceneContainer(388068805) fun blurRadiusIsMaxWhenShadeIsExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -170,7 +172,7 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza } @Test - @BrokenWithSceneContainer(330311871) + @BrokenWithSceneContainer(388068805) fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -185,6 +187,44 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza ) } + @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notificationBlur_isNonZero_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationBlurRadius) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + runCurrent() + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + + @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notifications_areFullyVisible_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationAlpha) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + runCurrent() + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = 1.0f, + endValue = 1.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + private fun step( value: Float, state: TransitionState = TransitionState.RUNNING, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt index 5798e0776c4f..338b06824e85 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt @@ -24,8 +24,9 @@ import com.android.systemui.keyguard.shared.model.TransitionInfo import java.util.function.Consumer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @@ -36,12 +37,10 @@ import org.junit.Assert.fail * Gives direct control over ValueAnimator, in order to make transition tests deterministic. See * [AnimationHandler]. Animators are required to be run on the main thread, so dispatch accordingly. */ -class KeyguardTransitionRunner(val repository: KeyguardTransitionRepository) : - AnimationFrameCallbackProvider { - - private var frameCount = 1L - private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null)) - private var job: Job? = null +class KeyguardTransitionRunner( + val frames: Flow<Long>, + val repository: KeyguardTransitionRepository, +) { @Volatile private var isTerminated = false /** @@ -54,21 +53,12 @@ class KeyguardTransitionRunner(val repository: KeyguardTransitionRepository) : maxFrames: Int = 100, frameCallback: Consumer<Long>? = null, ) { - // AnimationHandler uses ThreadLocal storage, and ValueAnimators MUST start from main - // thread - withContext(Dispatchers.Main) { - info.animator!!.getAnimationHandler().setProvider(this@KeyguardTransitionRunner) - } - - job = + val job = scope.launch { - frames.collect { - val (frameNumber, callback) = it - + frames.collect { frameNumber -> isTerminated = frameNumber >= maxFrames if (!isTerminated) { try { - withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) } frameCallback?.accept(frameNumber) } catch (e: IllegalStateException) { e.printStackTrace() @@ -78,27 +68,46 @@ class KeyguardTransitionRunner(val repository: KeyguardTransitionRepository) : } withContext(Dispatchers.Main) { repository.startTransition(info) } - waitUntilComplete(info.animator!!) + waitUntilComplete(info, info.animator!!) + job.cancel() } - private suspend fun waitUntilComplete(animator: ValueAnimator) { + private suspend fun waitUntilComplete(info: TransitionInfo, animator: ValueAnimator) { withContext(Dispatchers.Main) { val startTime = System.currentTimeMillis() while (!isTerminated && animator.isRunning()) { delay(1) if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) { - fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION") + fail("Failed due to excessive runtime of: $MAX_TEST_DURATION, info: $info") } } - - animator.getAnimationHandler().setProvider(null) } + } - job?.cancel() + companion object { + private const val MAX_TEST_DURATION = 300L + } +} + +class FrameCallbackProvider(val scope: CoroutineScope) : AnimationFrameCallbackProvider { + private val callback = MutableSharedFlow<FrameCallback?>(replay = 2) + private var frameCount = 0L + val frames = MutableStateFlow(frameCount) + + init { + scope.launch { + callback.collect { + withContext(Dispatchers.Main) { + delay(1) + it?.doFrame(frameCount) + } + } + } } override fun postFrameCallback(cb: FrameCallback) { - frames.value = Pair(frameCount++, cb) + frames.value = ++frameCount + callback.tryEmit(cb) } override fun postCommitCallback(runnable: Runnable) {} @@ -108,8 +117,4 @@ class KeyguardTransitionRunner(val repository: KeyguardTransitionRepository) : override fun getFrameDelay() = 1L override fun setFrameDelay(delay: Long) {} - - companion object { - private const val MAX_TEST_DURATION = 200L - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt index eeccbdf20540..79556baed067 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.qs.tiles import android.os.Handler +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf import android.service.quicksettings.Tile @@ -24,18 +26,26 @@ import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger +import com.android.systemui.Flags +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake +import com.android.systemui.flags.setFlagValue +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.flags.QSComposeFragment +import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tiles.dialog.InternetDialogManager import com.android.systemui.qs.tiles.dialog.WifiStateWorker import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.connectivity.AccessPointController +import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor @@ -256,6 +266,41 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() { verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true) } + @Test + @DisableFlags(QsDetailedView.FLAG_NAME) + fun click_withQsDetailedViewDisabled() { + underTest.click(null) + looper.processAllMessages() + + verify(dialogManager, times(1)).create( + aboveStatusBar = true, + accessPointController.canConfigMobileData(), + accessPointController.canConfigWifi(), + null, + ) + } + + @Test + @EnableFlags( + value = [ + QsDetailedView.FLAG_NAME, + FLAG_SCENE_CONTAINER, + KeyguardWmStateRefactor.FLAG_NAME, + NotificationThrottleHun.FLAG_NAME, + DualShade.FLAG_NAME] + ) + fun click_withQsDetailedViewEnabled() { + underTest.click(null) + looper.processAllMessages() + + verify(dialogManager, times(0)).create( + aboveStatusBar = true, + accessPointController.canConfigMobileData(), + accessPointController.canConfigWifi(), + null, + ) + } + companion object { const val WIFI_SSID = "test ssid" val ACTIVE_WIFI = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java index fc1d73b62abd..3a3f5371d195 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.when; import android.app.Dialog; import android.media.projection.StopReason; import android.os.Handler; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.FlagsParameterization; import android.service.quicksettings.Tile; import android.testing.TestableLooper; @@ -52,6 +53,7 @@ import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.flags.QsDetailedView; import com.android.systemui.qs.flags.QsInCompose; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; @@ -63,6 +65,7 @@ import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -70,11 +73,11 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.List; - import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; +import java.util.List; + @RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest @@ -82,7 +85,8 @@ public class ScreenRecordTileTest extends SysuiTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX); + return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, + QsDetailedView.FLAG_NAME); } @Mock @@ -336,6 +340,30 @@ public class ScreenRecordTileTest extends SysuiTestCase { .notifyPermissionRequestDisplayed(mContext.getUserId()); } + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testNotStartingAndRecording_returnDetailsViewModel() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(false); + mTile.getDetailsViewModel(Assert::assertNotNull); + } + + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testStarting_notReturnDetailsViewModel() { + when(mController.isStarting()).thenReturn(true); + when(mController.isRecording()).thenReturn(false); + mTile.getDetailsViewModel(Assert::assertNull); + } + + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testRecording_notReturnDetailsViewModel() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(true); + mTile.getDetailsViewModel(Assert::assertNull); + } + private QSTile.Icon createExpectedIcon(int resId) { if (QsInCompose.isEnabled()) { return new QSTileImpl.DrawableIconWithRes(mContext.getDrawable(resId), resId); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt index 04e094f25f5d..c8b3aba9b846 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt @@ -24,6 +24,7 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable @@ -87,9 +88,9 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val activeModes by collectLastValue(zenModeInteractor.activeModes) + zenModeRepository.activateMode(MANUAL_DND) zenModeRepository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder().setName("Mode 1").setActive(true).build(), TestModeBuilder().setName("Mode 2").setActive(true).build(), ) @@ -111,7 +112,7 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(zenModeInteractor.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE) + zenModeRepository.activateMode(MANUAL_DND) assertThat(dndMode?.isActive).isTrue() underTest.handleInput( @@ -127,7 +128,6 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(zenModeInteractor.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) assertThat(dndMode?.isActive).isFalse() underTest.handleInput( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 48edded5df18..de54e75281aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -575,4 +575,50 @@ class SceneInteractorTest : SysuiTestCase() { assertThat(currentScene).isNotEqualTo(disabledScene) } + + @Test + fun transitionAnimations() = + kosmos.runTest { + val isVisible by collectLastValue(underTest.isVisible) + assertThat(isVisible).isTrue() + + underTest.setVisible(false, "test") + assertThat(isVisible).isFalse() + + underTest.onTransitionAnimationStart() + // One animation is active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + + underTest.onTransitionAnimationStart() + // One animation is active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationCancelled() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + + underTest.setVisible(true, "test") + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationStart() + underTest.onTransitionAnimationStart() + // Two animations are active, forced visible. + assertThat(isVisible).isTrue() + + underTest.setVisible(false, "test") + // Two animations are active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // One animation is still active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 06dd046564df..51f056aa18da 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -36,6 +36,8 @@ import com.android.keyguard.AuthInteractionProperties import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.domain.interactor.authenticationInteractor @@ -141,6 +143,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever @SmallTest @@ -169,6 +172,7 @@ class SceneContainerStartableTest : SysuiTestCase() { private val uiEventLoggerFake = kosmos.uiEventLoggerFake private val msdlPlayer = kosmos.fakeMSDLPlayer private val authInteractionProperties = AuthInteractionProperties() + private val mockActivityTransitionAnimator = mock<ActivityTransitionAnimator>() private lateinit var underTest: SceneContainerStartable @@ -177,6 +181,8 @@ class SceneContainerStartableTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) .thenReturn(true) + kosmos.activityTransitionAnimator = mockActivityTransitionAnimator + underTest = kosmos.sceneContainerStartable } @@ -2716,6 +2722,27 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentOverlays).isEmpty() } + @Test + fun hydrateActivityTransitionAnimationState() = + kosmos.runTest { + underTest.start() + + val isVisible by collectLastValue(sceneInteractor.isVisible) + assertThat(isVisible).isTrue() + + sceneInteractor.setVisible(false, "reason") + assertThat(isVisible).isFalse() + + val argumentCaptor = argumentCaptor<ActivityTransitionAnimator.Listener>() + verify(mockActivityTransitionAnimator).addListener(argumentCaptor.capture()) + + val listeners = argumentCaptor.allValues + listeners.forEach { it.onTransitionAnimationStart() } + assertThat(isVisible).isTrue() + listeners.forEach { it.onTransitionAnimationEnd() } + assertThat(isVisible).isFalse() + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/view/SceneJankMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/view/SceneJankMonitorTest.kt new file mode 100644 index 000000000000..984f8fd13cde --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/view/SceneJankMonitorTest.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2025 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.scene.ui.view + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.jank.Cuj +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SceneJankMonitorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val underTest: SceneJankMonitor = kosmos.sceneJankMonitorFactory.create() + + @Before + fun setUp() { + underTest.activateIn(kosmos.testScope) + } + + @Test + fun onTransitionStart_withProvidedCuj_beginsThatCuj() = + kosmos.runTest { + val cuj = 1337 + underTest.onTransitionStart( + view = mock(), + from = Scenes.Communal, + to = Scenes.Dream, + cuj = cuj, + ) + verify(interactionJankMonitor).begin(any(), eq(cuj)) + verify(interactionJankMonitor, never()).end(anyInt()) + } + + @Test + fun onTransitionEnd_withProvidedCuj_endsThatCuj() = + kosmos.runTest { + val cuj = 1337 + underTest.onTransitionEnd(from = Scenes.Communal, to = Scenes.Dream, cuj = cuj) + verify(interactionJankMonitor, never()).begin(any(), anyInt()) + verify(interactionJankMonitor).end(cuj) + } + + @Test + fun bouncer_authMethodPin() = + kosmos.runTest { + bouncer( + authenticationMethod = AuthenticationMethodModel.Pin, + appearCuj = Cuj.CUJ_LOCKSCREEN_PIN_APPEAR, + disappearCuj = Cuj.CUJ_LOCKSCREEN_PIN_DISAPPEAR, + ) + } + + @Test + fun bouncer_authMethodSim() = + kosmos.runTest { + bouncer( + authenticationMethod = AuthenticationMethodModel.Sim, + appearCuj = Cuj.CUJ_LOCKSCREEN_PIN_APPEAR, + disappearCuj = Cuj.CUJ_LOCKSCREEN_PIN_DISAPPEAR, + // When the auth method is SIM, unlocking doesn't work like normal. Instead of + // leaving the bouncer, the bouncer is switched over to the real authentication + // method when the SIM is unlocked. + // + // Therefore, there's no point in testing this code path and it will, in fact, fail + // to unlock. + testUnlockedDisappearance = false, + ) + } + + @Test + fun bouncer_authMethodPattern() = + kosmos.runTest { + bouncer( + authenticationMethod = AuthenticationMethodModel.Pattern, + appearCuj = Cuj.CUJ_LOCKSCREEN_PATTERN_APPEAR, + disappearCuj = Cuj.CUJ_LOCKSCREEN_PATTERN_DISAPPEAR, + ) + } + + @Test + fun bouncer_authMethodPassword() = + kosmos.runTest { + bouncer( + authenticationMethod = AuthenticationMethodModel.Password, + appearCuj = Cuj.CUJ_LOCKSCREEN_PASSWORD_APPEAR, + disappearCuj = Cuj.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR, + ) + } + + private fun Kosmos.bouncer( + authenticationMethod: AuthenticationMethodModel, + appearCuj: Int, + disappearCuj: Int, + testUnlockedDisappearance: Boolean = true, + ) { + // Set up state: + fakeAuthenticationRepository.setAuthenticationMethod(authenticationMethod) + runCurrent() + + fun verifyCujCounts( + beginAppearCount: Int = 0, + beginDisappearCount: Int = 0, + endAppearCount: Int = 0, + endDisappearCount: Int = 0, + ) { + verify(interactionJankMonitor, times(beginAppearCount)).begin(any(), eq(appearCuj)) + verify(interactionJankMonitor, times(beginDisappearCount)) + .begin(any(), eq(disappearCuj)) + verify(interactionJankMonitor, times(endAppearCount)).end(appearCuj) + verify(interactionJankMonitor, times(endDisappearCount)).end(disappearCuj) + } + + // Precondition checks: + assertThat(deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked).isFalse() + verifyCujCounts() + + // Bouncer appears CUJ: + underTest.onTransitionStart( + view = mock(), + from = Scenes.Lockscreen, + to = Scenes.Bouncer, + cuj = null, + ) + verifyCujCounts(beginAppearCount = 1) + underTest.onTransitionEnd(from = Scenes.Lockscreen, to = Scenes.Bouncer, cuj = null) + verifyCujCounts(beginAppearCount = 1, endAppearCount = 1) + + // Bouncer disappear CUJ but it doesn't log because the device isn't unlocked. + underTest.onTransitionStart( + view = mock(), + from = Scenes.Bouncer, + to = Scenes.Lockscreen, + cuj = null, + ) + verifyCujCounts(beginAppearCount = 1, endAppearCount = 1) + underTest.onTransitionEnd(from = Scenes.Bouncer, to = Scenes.Lockscreen, cuj = null) + verifyCujCounts(beginAppearCount = 1, endAppearCount = 1) + + if (!testUnlockedDisappearance) { + return + } + + // Unlock the device and transition away from the bouncer. + fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + assertThat(deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked).isTrue() + + // Bouncer disappear CUJ and it doeslog because the device is unlocked. + underTest.onTransitionStart( + view = mock(), + from = Scenes.Bouncer, + to = Scenes.Gone, + cuj = null, + ) + verifyCujCounts(beginAppearCount = 1, endAppearCount = 1, beginDisappearCount = 1) + underTest.onTransitionEnd(from = Scenes.Bouncer, to = Scenes.Gone, cuj = null) + verifyCujCounts( + beginAppearCount = 1, + endAppearCount = 1, + beginDisappearCount = 1, + endDisappearCount = 1, + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 0e931679ec74..62c360400582 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -85,7 +85,6 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; @@ -335,16 +334,14 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false); mMainDispatcher = getMainDispatcher(); - KeyguardInteractorFactory.WithDependencies keyguardInteractorDeps = - KeyguardInteractorFactory.create(); - mFakeKeyguardRepository = keyguardInteractorDeps.getRepository(); + mFakeKeyguardRepository = mKosmos.getKeyguardRepository(); mFakeKeyguardClockRepository = new FakeKeyguardClockRepository(); mKeyguardClockInteractor = mKosmos.getKeyguardClockInteractor(); - mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor(); + mKeyguardInteractor = mKosmos.getKeyguardInteractor(); mShadeRepository = new FakeShadeRepository(); mShadeAnimationInteractor = new ShadeAnimationInteractorLegacyImpl( new ShadeAnimationRepository(), mShadeRepository); - mPowerInteractor = keyguardInteractorDeps.getPowerInteractor(); + mPowerInteractor = mKosmos.getPowerInteractor(); when(mKeyguardTransitionInteractor.isInTransitionWhere(any(), any())).thenReturn( MutableStateFlow(false)); when(mKeyguardTransitionInteractor.isInTransition(any(), any())) @@ -531,9 +528,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mNotificationPanelViewController = new NotificationPanelViewController( mView, - mMainHandler, - mLayoutInflater, - mFeatureFlags, coordinator, expansionHandler, mDynamicPrivacyController, mKeyguardBypassController, mFalsingManager, new FalsingCollectorFake(), mKeyguardStateController, @@ -553,7 +547,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mKeyguardStatusBarViewComponentFactory, mLockscreenShadeTransitionController, mScrimController, - mUserManager, mMediaDataManager, mNotificationShadeDepthController, mAmbientState, @@ -564,7 +557,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mQsController, mFragmentService, mStatusBarService, - mContentResolver, mShadeHeaderController, mScreenOffAnimationController, mLockscreenGestureLogger, @@ -575,7 +567,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mKeyguardUnlockAnimationController, mKeyguardIndicationController, mNotificationListContainer, - mNotificationStackSizeCalculator, mUnlockedScreenOffAnimationController, systemClock, mKeyguardClockInteractor, @@ -594,7 +585,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { new ResourcesSplitShadeStateController(), mPowerInteractor, mKeyguardClockPositionAlgorithm, - mNaturalScrollingSettingObserver, mMSDLPlayer, mBrightnessMirrorShowingInteractor); mNotificationPanelViewController.initDependencies( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java index 2e9d6e85d0aa..49cbb5a924f1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java @@ -53,7 +53,6 @@ import androidx.test.filters.SmallTest; import com.android.systemui.plugins.qs.QS; import com.android.systemui.qs.flags.QSComposeFragment; import com.android.systemui.res.R; -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; import org.junit.Test; import org.junit.runner.RunWith; @@ -365,7 +364,6 @@ public class QuickSettingsControllerImplTest extends QuickSettingsControllerImpl } @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) public void updateExpansion_partiallyExpanded_fullscreenFalse() { // WHEN QS are only partially expanded mQsController.setExpanded(true); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt index a98d1a2ea4a5..d3ba3dceb4cf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt @@ -22,10 +22,13 @@ import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.scene.ui.view.mockShadeRootView import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository import com.android.systemui.testKosmos +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -41,6 +44,7 @@ import org.mockito.kotlin.whenever class ShadeDisplaysInteractorTest : SysuiTestCase() { val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val testScope = kosmos.testScope private val shadeRootview = kosmos.mockShadeRootView private val positionRepository = kosmos.fakeShadeDisplaysRepository private val shadeContext = kosmos.mockedWindowContext @@ -49,7 +53,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { private val configuration = mock<Configuration>() private val display = mock<Display>() - private val underTest = kosmos.shadeDisplaysInteractor + private val underTest by lazy { kosmos.shadeDisplaysInteractor } @Before fun setup() { @@ -84,12 +88,14 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { } @Test - fun start_shadeInWrongPosition_logsStartToLatencyTracker() { - whenever(display.displayId).thenReturn(0) - positionRepository.setDisplayId(1) + fun start_shadeInWrongPosition_logsStartToLatencyTracker() = + testScope.runTest { + whenever(display.displayId).thenReturn(0) + positionRepository.setDisplayId(1) - underTest.start() + underTest.start() + advanceUntilIdle() - verify(latencyTracker).onShadeDisplayChanging(eq(1)) - } + verify(latencyTracker).onShadeDisplayChanging(eq(1)) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt new file mode 100644 index 000000000000..58396e7cef82 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 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.shade.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl.NotificationElement +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl.QSElement +import com.android.systemui.shade.shadeTestUtil +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +@EnableSceneContainer +class ShadeExpandedStateInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val testScope = kosmos.testScope + private val shadeTestUtil by lazy { kosmos.shadeTestUtil } + + private val underTest: ShadeExpandedStateInteractor by lazy { + kosmos.shadeExpandedStateInteractor + } + + @Test + fun expandedElement_qsExpanded_returnsQSElement() = + testScope.runTest { + shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 0f, qsExpansion = 1f) + val currentlyExpandedElement = underTest.currentlyExpandedElement + + val element = currentlyExpandedElement.value + + assertThat(element).isInstanceOf(QSElement::class.java) + } + + @Test + fun expandedElement_shadeExpanded_returnsShade() = + testScope.runTest { + shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 1f, qsExpansion = 0f) + + val element = underTest.currentlyExpandedElement.value + + assertThat(element).isInstanceOf(NotificationElement::class.java) + } + + @Test + fun expandedElement_noneExpanded_returnsNull() = + testScope.runTest { + shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 0f, qsExpansion = 0f) + + val element = underTest.currentlyExpandedElement.value + + assertThat(element).isNull() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt index 83fb14aaf792..6b2c4b260806 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt @@ -9,9 +9,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -25,7 +24,7 @@ class ConditionExtensionsTest : SysuiTestCase() { @Before fun setUp() { - testScope = TestScope(StandardTestDispatcher()) + testScope = TestScope(UnconfinedTestDispatcher()) } @Test @@ -34,11 +33,9 @@ class ConditionExtensionsTest : SysuiTestCase() { val flow = flowOf(true) val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) - runCurrent() assertThat(condition.isConditionSet).isFalse() condition.start() - runCurrent() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isTrue() } @@ -49,11 +46,9 @@ class ConditionExtensionsTest : SysuiTestCase() { val flow = flowOf(false) val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) - runCurrent() assertThat(condition.isConditionSet).isFalse() condition.start() - runCurrent() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() } @@ -65,7 +60,6 @@ class ConditionExtensionsTest : SysuiTestCase() { val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) condition.start() - runCurrent() assertThat(condition.isConditionSet).isFalse() assertThat(condition.isConditionMet).isFalse() } @@ -78,11 +72,10 @@ class ConditionExtensionsTest : SysuiTestCase() { flow.toCondition( scope = this, strategy = Condition.START_EAGERLY, - initialValue = true + initialValue = true, ) condition.start() - runCurrent() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isTrue() } @@ -95,11 +88,10 @@ class ConditionExtensionsTest : SysuiTestCase() { flow.toCondition( scope = this, strategy = Condition.START_EAGERLY, - initialValue = false + initialValue = false, ) condition.start() - runCurrent() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() } @@ -111,16 +103,13 @@ class ConditionExtensionsTest : SysuiTestCase() { val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) condition.start() - runCurrent() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() flow.value = true - runCurrent() assertThat(condition.isConditionMet).isTrue() flow.value = false - runCurrent() assertThat(condition.isConditionMet).isFalse() condition.stop() @@ -131,15 +120,12 @@ class ConditionExtensionsTest : SysuiTestCase() { testScope.runTest { val flow = MutableSharedFlow<Boolean>() val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) - runCurrent() assertThat(flow.subscriptionCount.value).isEqualTo(0) condition.start() - runCurrent() assertThat(flow.subscriptionCount.value).isEqualTo(1) condition.stop() - runCurrent() assertThat(flow.subscriptionCount.value).isEqualTo(0) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 20474c842b51..deaf57999b21 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -526,7 +526,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1)); // Device is not currently locked - when(mKeyguardManager.isDeviceLocked()).thenReturn(false); + mLockscreenUserManager.mLocked.set(false); // Sensitive Content notifications are always redacted assertEquals(REDACTION_TYPE_NONE, @@ -540,7 +540,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1, mCurrentUser.id); changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); - when(mKeyguardManager.isDeviceLocked()).thenReturn(true); + mLockscreenUserManager.mLocked.set(true); // Device was locked after this notification arrived mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime + TimeUnit.DAYS.toMillis(1)); @@ -560,7 +560,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { // Device has been locked for 1 second before the notification came in, which is too short mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime - TimeUnit.SECONDS.toMillis(1)); - when(mKeyguardManager.isDeviceLocked()).thenReturn(true); + mLockscreenUserManager.mLocked.set(true); // Sensitive Content notifications are always redacted assertEquals(REDACTION_TYPE_NONE, @@ -577,7 +577,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { // Claim the device was last locked 1 day ago mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1)); - when(mKeyguardManager.isDeviceLocked()).thenReturn(true); + mLockscreenUserManager.mLocked.set(true); // Sensitive Content notifications are always redacted assertEquals(REDACTION_TYPE_NONE, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index 2d7dc2e63650..0a0564994e69 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -43,6 +43,7 @@ import com.android.systemui.testKosmos import com.android.systemui.util.WallpaperController import com.android.systemui.util.mockito.eq import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor +import com.android.wm.shell.appzoomout.AppZoomOut import com.google.common.truth.Truth.assertThat import java.util.function.Consumer import org.junit.Before @@ -65,6 +66,7 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit +import java.util.Optional @RunWith(AndroidJUnit4::class) @RunWithLooper @@ -82,6 +84,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Mock private lateinit var wallpaperController: WallpaperController @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController @Mock private lateinit var dumpManager: DumpManager + @Mock private lateinit var appZoomOutOptional: Optional<AppZoomOut> @Mock private lateinit var root: View @Mock private lateinit var viewRootImpl: ViewRootImpl @Mock private lateinit var windowToken: IBinder @@ -128,6 +131,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { ResourcesSplitShadeStateController(), windowRootViewBlurInteractor, applicationScope, + appZoomOutOptional, dumpManager, configurationController, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt index bb9141afe404..5f73ac45d12f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt @@ -45,8 +45,11 @@ import kotlin.test.Test import org.junit.Before import org.junit.runner.RunWith import org.mockito.Mockito.verify +import org.mockito.kotlin.any import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verifyNoMoreInteractions @SmallTest @@ -106,10 +109,10 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { // Make sure the legacy code path does not change airplane mode when the refactor // flag is enabled. underTest.setIsAirplaneMode(IconState(true, TelephonyIcons.FLIGHT_MODE_ICON, "")) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) underTest.setIsAirplaneMode(IconState(false, TelephonyIcons.FLIGHT_MODE_ICON, "")) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) } @Test @@ -144,10 +147,10 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { // Make sure changing airplane mode from airplaneModeRepository does nothing // if the StatusBarSignalPolicyRefactor is not enabled. airplaneModeInteractor.setIsAirplaneMode(true) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) airplaneModeInteractor.setIsAirplaneMode(false) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) } @Test @@ -196,7 +199,7 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { underTest.setEthernetIndicators( IconState(/* visible= */ true, /* icon= */ 1, /* contentDescription= */ "Ethernet") ) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) underTest.setEthernetIndicators( IconState( @@ -205,7 +208,7 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { /* contentDescription= */ "No ethernet", ) ) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) } @Test @@ -217,13 +220,13 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { clearInvocations(statusBarIconController) connectivityRepository.fake.setEthernetConnected(default = true, validated = true) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) connectivityRepository.fake.setEthernetConnected(default = false, validated = false) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) connectivityRepository.fake.setEthernetConnected(default = true, validated = false) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index a62d9d5ce62f..75d000b63d62 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -22,7 +22,6 @@ import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue @@ -126,33 +125,35 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_positiveStartTime_notifIconFlagOff_iconIsPhone() = + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_positiveStartTime_connectedDisplaysFlagOn_iconIsNotifIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) + val notifKey = "testNotifKey" repo.setOngoingCallState( - inCallModel(startTimeMs = 1000, notificationIcon = mock<StatusBarIconView>()) + inCallModel(startTimeMs = 1000, notificationIcon = null, notificationKey = notifKey) ) assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) - val icon = + .isInstanceOf( + OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon::class.java + ) + val actualNotifKey = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.SingleColorIcon) - .impl as Icon.Resource - assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) - assertThat(icon.contentDescription).isNotNull() + as OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon) + .notificationKey + assertThat(actualNotifKey).isEqualTo(notifKey) } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_positiveStartTime_notifIconFlagOn_iconIsNotifIcon() = + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_zeroStartTime_cdFlagOff_iconIsNotifIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) - val notifIcon = mock<StatusBarIconView>() - repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = notifIcon)) + val notifIcon = createStatusBarIconViewOrNull() + repo.setOngoingCallState(inCallModel(startTimeMs = 0, notificationIcon = notifIcon)) assertThat((latest as OngoingActivityChipModel.Shown).icon) .isInstanceOf(OngoingActivityChipModel.ChipIcon.StatusBarView::class.java) @@ -164,36 +165,30 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun chip_positiveStartTime_notifIconAndConnectedDisplaysFlagOn_iconIsNotifIcon() = + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_zeroStartTime_cdFlagOn_iconIsNotifKeyIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) - val notifKey = "testNotifKey" repo.setOngoingCallState( - inCallModel(startTimeMs = 1000, notificationIcon = null, notificationKey = notifKey) + inCallModel( + startTimeMs = 0, + notificationIcon = createStatusBarIconViewOrNull(), + notificationKey = "notifKey", + ) ) assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf( - OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon::class.java - ) - val actualNotifKey = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon) - .notificationKey - assertThat(actualNotifKey).isEqualTo(notifKey) + .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey")) } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_zeroStartTime_notifIconFlagOff_iconIsPhone() = + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOff_iconIsPhone() = testScope.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState( - inCallModel(startTimeMs = 0, notificationIcon = mock<StatusBarIconView>()) - ) + repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = null)) assertThat((latest as OngoingActivityChipModel.Shown).icon) .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) @@ -206,39 +201,21 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_zeroStartTime_notifIconFlagOn_iconIsNotifIcon() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - val notifIcon = mock<StatusBarIconView>() - repo.setOngoingCallState(inCallModel(startTimeMs = 0, notificationIcon = notifIcon)) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.StatusBarView::class.java) - val actualIcon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.StatusBarView) - .impl - assertThat(actualIcon).isEqualTo(notifIcon) - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_notifIconFlagOn_butNullNotifIcon_iconIsPhone() = + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_notifIconFlagOn_butNullNotifIcon_iconNotifKey() = testScope.runTest { val latest by collectLastValue(underTest.chip) - repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = null)) + repo.setOngoingCallState( + inCallModel( + startTimeMs = 1000, + notificationIcon = null, + notificationKey = "notifKey", + ) + ) assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) - val icon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.SingleColorIcon) - .impl as Icon.Resource - assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) - assertThat(icon.contentDescription).isNotNull() + .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey")) } @Test @@ -330,4 +307,13 @@ class CallChipViewModelTest : SysuiTestCase() { verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null) } + + companion object { + fun createStatusBarIconViewOrNull(): StatusBarIconView? = + if (StatusBarConnectedDisplays.isEnabled) { + null + } else { + mock<StatusBarIconView>() + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt index 0d033a4098ec..fe15eac46e2d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.notification.domain.interactor +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -148,7 +149,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { } @Test - fun notificationChip_missingStatusBarIconChipView_inConstructor_emitsNull() = + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun notificationChip_missingStatusBarIconChipView_cdFlagDisabled_inConstructor_emitsNull() = kosmos.runTest { val underTest = factory.create( @@ -167,6 +169,25 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun notificationChip_missingStatusBarIconChipView_cdFlagEnabled_inConstructor_emitsNotNull() = + kosmos.runTest { + val underTest = + factory.create( + activeNotificationModel( + key = "notif1", + statusBarChipIcon = null, + promotedContent = PROMOTED_CONTENT, + ), + 32L, + ) + + val latest by collectLastValue(underTest.notificationChip) + + assertThat(latest).isNotNull() + } + + @Test + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun notificationChip_cdEnabled_missingStatusBarIconChipView_inConstructor_emitsNotNull() = kosmos.runTest { val underTest = @@ -186,7 +207,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { } @Test - fun notificationChip_missingStatusBarIconChipView_inSet_emitsNull() = + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun notificationChip_cdFlagDisabled_missingStatusBarIconChipView_inSet_emitsNull() = kosmos.runTest { val startingNotif = activeNotificationModel( @@ -211,6 +233,31 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun notificationChip_cdFlagEnabled_missingStatusBarIconChipView_inSet_emitsNotNull() = + kosmos.runTest { + val startingNotif = + activeNotificationModel( + key = "notif1", + statusBarChipIcon = mock(), + promotedContent = PROMOTED_CONTENT, + ) + val underTest = factory.create(startingNotif, 123L) + val latest by collectLastValue(underTest.notificationChip) + assertThat(latest).isNotNull() + + underTest.setNotification( + activeNotificationModel( + key = "notif1", + statusBarChipIcon = null, + promotedContent = PROMOTED_CONTENT, + ) + ) + + assertThat(latest).isNotNull() + } + + @Test + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun notificationChip_missingStatusBarIconChipView_inSet_cdEnabled_emitsNotNull() = kosmos.runTest { val startingNotif = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt index f703d785ceac..ee4a52d35d68 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -83,7 +84,8 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun notificationChips_notifMissingStatusBarChipIconView_empty() = + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun notificationChips_notifMissingStatusBarChipIconView_cdFlagOff_empty() = kosmos.runTest { val latest by collectLastValue(underTest.notificationChips) @@ -101,6 +103,25 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME, StatusBarConnectedDisplays.FLAG_NAME) + fun notificationChips_notifMissingStatusBarChipIconView_cdFlagOn_notEmpty() = + kosmos.runTest { + val latest by collectLastValue(underTest.notificationChips) + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = null, + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + ) + ) + ) + + assertThat(latest).isNotEmpty() + } + + @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun notificationChips_onePromotedNotif_statusBarIconViewMatches() = kosmos.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index 17076b4d7505..902db5e10589 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -23,7 +23,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.kosmos.collectLastValue @@ -31,6 +30,7 @@ import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.ColorsModel @@ -48,7 +48,6 @@ import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -84,8 +83,8 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_notifMissingStatusBarChipIconView_empty() = + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY, StatusBarConnectedDisplays.FLAG_NAME) + fun chips_notifMissingStatusBarChipIconView_cdFlagDisabled_empty() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -104,11 +103,31 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chips_notifMissingStatusBarChipIconView_cdFlagEnabled_notEmpty() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = null, + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + ) + ) + ) + + assertThat(latest).isNotEmpty() + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_onePromotedNotif_statusBarIconViewMatches() = kosmos.runTest { val latest by collectLastValue(underTest.chips) - val icon = mock<StatusBarIconView>() + val icon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( @@ -121,8 +140,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { assertThat(latest).hasSize(1) val chip = latest!![0] - assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown::class.java) - assertThat(chip.icon).isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(icon)) + assertIsNotifChip(chip, icon, "notif") } @Test @@ -168,7 +186,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) @@ -187,8 +205,8 @@ class NotifChipsViewModelTest : SysuiTestCase() { kosmos.runTest { val latest by collectLastValue(underTest.chips) - val firstIcon = mock<StatusBarIconView>() - val secondIcon = mock<StatusBarIconView>() + val firstIcon = createStatusBarIconViewOrNull() + val secondIcon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( @@ -203,15 +221,15 @@ class NotifChipsViewModelTest : SysuiTestCase() { ), activeNotificationModel( key = "notif3", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = null, ), ) ) assertThat(latest).hasSize(2) - assertIsNotifChip(latest!![0], firstIcon) - assertIsNotifChip(latest!![1], secondIcon) + assertIsNotifChip(latest!![0], firstIcon, "notif1") + assertIsNotifChip(latest!![1], secondIcon, "notif2") } @Test @@ -269,7 +287,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) @@ -293,7 +311,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) @@ -323,7 +341,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) @@ -353,7 +371,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) @@ -382,7 +400,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) @@ -411,7 +429,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) @@ -439,7 +457,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) @@ -467,7 +485,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) @@ -483,7 +501,99 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_hasHeadsUpByUser_onlyShowsIcon() = + fun chips_hasHeadsUpBySystem_showsTime() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 6543L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + // WHEN there's a HUN pinned by the system + kosmos.headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "notif", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), + ) + ) + + // THEN the chip keeps showing time + // (In real life the chip won't show at all, but that's handled in a different part of + // the system. What we know here is that the chip shouldn't shrink to icon only.) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_hasHeadsUpByUser_forOtherNotif_showsTime() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 6543L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + val otherPromotedContentBuilder = + PromotedNotificationContentModel.Builder("other notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 654321L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + val icon = createStatusBarIconViewOrNull() + val otherIcon = createStatusBarIconViewOrNull() + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = icon, + promotedContent = promotedContentBuilder.build(), + ), + activeNotificationModel( + key = "other notif", + statusBarChipIcon = otherIcon, + promotedContent = otherPromotedContentBuilder.build(), + ), + ) + ) + + // WHEN there's a HUN pinned for the "other notif" chip + kosmos.headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "other notif", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + // THEN the "notif" chip keeps showing time + val chip = latest!![0] + assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java) + assertIsNotifChip(chip, icon, "notif") + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_hasHeadsUpByUser_forThisNotif_onlyShowsIcon() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -505,7 +615,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { ) ) - // WHEN there's a HUN pinned by a user + // WHEN this notification is pinned by the user kosmos.headsUpNotificationRepository.setNotifications( UnconfinedFakeHeadsUpRowRepository( key = "notif", @@ -513,6 +623,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { ) ) + // THEN the chip shrinks to icon only assertThat(latest!![0]) .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java) } @@ -531,7 +642,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "clickTest", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = PromotedNotificationContentModel.Builder("clickTest").build(), ) @@ -552,9 +663,21 @@ class NotifChipsViewModelTest : SysuiTestCase() { } companion object { - fun assertIsNotifChip(latest: OngoingActivityChipModel?, expectedIcon: StatusBarIconView) { - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(expectedIcon)) + fun assertIsNotifChip( + latest: OngoingActivityChipModel?, + expectedIcon: StatusBarIconView?, + notificationKey: String, + ) { + val shown = latest as OngoingActivityChipModel.Shown + if (StatusBarConnectedDisplays.isEnabled) { + assertThat(shown.icon) + .isEqualTo( + OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(notificationKey) + ) + } else { + assertThat(latest.icon) + .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(expectedIcon!!)) + } } fun assertIsNotifKey(latest: OngoingActivityChipModel?, expectedKey: String) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index 4fb42e94adb2..42358cce59a2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository @@ -169,29 +170,35 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { @Test fun primaryChip_screenRecordAndShareToAppAndCastToOtherHideAndCallShown_callShown() = testScope.runTest { + val notificationKey = "call" screenRecordState.value = ScreenRecordModel.DoingNothing // MediaProjection covers both share-to-app and cast-to-other-device mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = notificationKey) + ) val latest by collectLastValue(underTest.primaryChip) - assertIsCallChip(latest) + assertIsCallChip(latest, notificationKey) } @Test fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() = testScope.runTest { // Start with just the lowest priority chip shown - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + val callNotificationKey = "call" + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) + ) // And everything else hidden mediaProjectionState.value = MediaProjectionState.NotProjecting screenRecordState.value = ScreenRecordModel.DoingNothing val latest by collectLastValue(underTest.primaryChip) - assertIsCallChip(latest) + assertIsCallChip(latest, callNotificationKey) // WHEN the higher priority media projection chip is added mediaProjectionState.value = @@ -218,7 +225,10 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + val callNotificationKey = "call" + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) + ) val latest by collectLastValue(underTest.primaryChip) @@ -235,7 +245,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.NotProjecting // THEN the lower priority call is used - assertIsCallChip(latest) + assertIsCallChip(latest, callNotificationKey) } /** Regression test for b/347726238. */ @@ -364,13 +374,27 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all) } - fun assertIsCallChip(latest: OngoingActivityChipModel?) { - assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + fun assertIsCallChip(latest: OngoingActivityChipModel?, notificationKey: String) { + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java) + if (StatusBarConnectedDisplays.isEnabled) { + assertNotificationIcon(latest, notificationKey) + return + } val icon = (((latest as OngoingActivityChipModel.Shown).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) } + + private fun assertNotificationIcon( + latest: OngoingActivityChipModel?, + notificationKey: String, + ) { + val shown = latest as OngoingActivityChipModel.Shown + val notificationIcon = + shown.icon as OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon + assertThat(notificationIcon.notificationKey).isEqualTo(notificationKey) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 0050ebee64d6..0f42f29e76ee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -34,7 +34,7 @@ import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager import com.android.systemui.res.R import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.screenRecordRepository -import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor @@ -186,13 +186,16 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @Test fun chips_screenRecordShowAndCallShow_primaryIsScreenRecordSecondaryIsCall() = testScope.runTest { + val callNotificationKey = "call" screenRecordState.value = ScreenRecordModel.Recording - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) + ) val latest by collectLastValue(underTest.chips) assertIsScreenRecordChip(latest!!.primary) - assertIsCallChip(latest!!.secondary) + assertIsCallChip(latest!!.secondary, callNotificationKey) } @Test @@ -240,15 +243,18 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @Test fun chips_shareToAppShowAndCallShow_primaryIsShareToAppSecondaryIsCall() = testScope.runTest { + val callNotificationKey = "call" screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) + ) val latest by collectLastValue(underTest.chips) assertIsShareToAppChip(latest!!.primary) - assertIsCallChip(latest!!.secondary) + assertIsCallChip(latest!!.secondary, callNotificationKey) } @Test @@ -258,25 +264,31 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // MediaProjection covers both share-to-app and cast-to-other-device mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + val callNotificationKey = "call" + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) + ) val latest by collectLastValue(underTest.primaryChip) - assertIsCallChip(latest) + assertIsCallChip(latest, callNotificationKey) } @Test fun chips_onlyCallShown_primaryIsCallSecondaryIsHidden() = testScope.runTest { + val callNotificationKey = "call" screenRecordState.value = ScreenRecordModel.DoingNothing // MediaProjection covers both share-to-app and cast-to-other-device mediaProjectionState.value = MediaProjectionState.NotProjecting - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) + ) val latest by collectLastValue(underTest.chips) - assertIsCallChip(latest!!.primary) + assertIsCallChip(latest!!.primary, callNotificationKey) assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) } @@ -285,7 +297,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.chips) - val icon = mock<StatusBarIconView>() + val icon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( @@ -296,7 +308,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) ) - assertIsNotifChip(latest!!.primary, icon) + assertIsNotifChip(latest!!.primary, icon, "notif") assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) } @@ -305,8 +317,8 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.chips) - val firstIcon = mock<StatusBarIconView>() - val secondIcon = mock<StatusBarIconView>() + val firstIcon = createStatusBarIconViewOrNull() + val secondIcon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( @@ -324,8 +336,8 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) ) - assertIsNotifChip(latest!!.primary, firstIcon) - assertIsNotifChip(latest!!.secondary, secondIcon) + assertIsNotifChip(latest!!.primary, firstIcon, "firstNotif") + assertIsNotifChip(latest!!.secondary, secondIcon, "secondNotif") } @Test @@ -333,9 +345,9 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.chips) - val firstIcon = mock<StatusBarIconView>() - val secondIcon = mock<StatusBarIconView>() - val thirdIcon = mock<StatusBarIconView>() + val firstIcon = createStatusBarIconViewOrNull() + val secondIcon = createStatusBarIconViewOrNull() + val thirdIcon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( @@ -359,8 +371,8 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) ) - assertIsNotifChip(latest!!.primary, firstIcon) - assertIsNotifChip(latest!!.secondary, secondIcon) + assertIsNotifChip(latest!!.primary, firstIcon, "firstNotif") + assertIsNotifChip(latest!!.secondary, secondIcon, "secondNotif") } @Test @@ -368,8 +380,12 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.chips) - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) - val firstIcon = mock<StatusBarIconView>() + val callNotificationKey = "call" + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) + ) + + val firstIcon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( @@ -380,43 +396,47 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ), activeNotificationModel( key = "secondNotif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = PromotedNotificationContentModel.Builder("secondNotif").build(), ), ) ) - assertIsCallChip(latest!!.primary) - assertIsNotifChip(latest!!.secondary, firstIcon) + assertIsCallChip(latest!!.primary, callNotificationKey) + assertIsNotifChip(latest!!.secondary, firstIcon, "firstNotif") } @Test fun chips_screenRecordAndCallAndPromotedNotifs_notifsNotShown() = testScope.runTest { + val callNotificationKey = "call" val latest by collectLastValue(underTest.chips) - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) + ) screenRecordState.value = ScreenRecordModel.Recording setNotifs( listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = PromotedNotificationContentModel.Builder("notif").build(), ) ) ) assertIsScreenRecordChip(latest!!.primary) - assertIsCallChip(latest!!.secondary) + assertIsCallChip(latest!!.secondary, callNotificationKey) } @Test fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() = testScope.runTest { + val callNotificationKey = "call" // Start with just the lowest priority chip shown - val notifIcon = mock<StatusBarIconView>() + val notifIcon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( @@ -433,13 +453,15 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.primaryChip) - assertIsNotifChip(latest, notifIcon) + assertIsNotifChip(latest, notifIcon, "notif") // WHEN the higher priority call chip is added - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) + ) // THEN the higher priority call chip is used - assertIsCallChip(latest) + assertIsCallChip(latest, callNotificationKey) // WHEN the higher priority media projection chip is added mediaProjectionState.value = @@ -462,12 +484,15 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @Test fun primaryChip_highestPriorityChipRemoved_showsNextPriorityChip() = testScope.runTest { + val callNotificationKey = "call" // WHEN all chips are active screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) - val notifIcon = mock<StatusBarIconView>() + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) + ) + val notifIcon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( @@ -493,20 +518,21 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.NotProjecting // THEN the lower priority call is used - assertIsCallChip(latest) + assertIsCallChip(latest, callNotificationKey) // WHEN the higher priority call is removed callRepo.setOngoingCallState(OngoingCallModel.NoCall) // THEN the lower priority notif is used - assertIsNotifChip(latest, notifIcon) + assertIsNotifChip(latest, notifIcon, "notif") } @Test fun chips_movesChipsAroundAccordingToPriority() = testScope.runTest { + val callNotificationKey = "call" // Start with just the lowest priority chip shown - val notifIcon = mock<StatusBarIconView>() + val notifIcon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( @@ -523,16 +549,18 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chips) - assertIsNotifChip(latest!!.primary, notifIcon) + assertIsNotifChip(latest!!.primary, notifIcon, "notif") assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) // WHEN the higher priority call chip is added - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + callRepo.setOngoingCallState( + inCallModel(startTimeMs = 34, notificationKey = callNotificationKey) + ) // THEN the higher priority call chip is used as primary and notif is demoted to // secondary - assertIsCallChip(latest!!.primary) - assertIsNotifChip(latest!!.secondary, notifIcon) + assertIsCallChip(latest!!.primary, callNotificationKey) + assertIsNotifChip(latest!!.secondary, notifIcon, "notif") // WHEN the higher priority media projection chip is added mediaProjectionState.value = @@ -545,7 +573,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // THEN the higher priority media projection chip is used as primary and call is demoted // to secondary (and notif is dropped altogether) assertIsShareToAppChip(latest!!.primary) - assertIsCallChip(latest!!.secondary) + assertIsCallChip(latest!!.secondary, callNotificationKey) // WHEN the higher priority screen record chip is added screenRecordState.value = ScreenRecordModel.Recording @@ -559,13 +587,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // THEN media projection and notif remain assertIsShareToAppChip(latest!!.primary) - assertIsNotifChip(latest!!.secondary, notifIcon) + assertIsNotifChip(latest!!.secondary, notifIcon, "notif") // WHEN media projection is dropped mediaProjectionState.value = MediaProjectionState.NotProjecting // THEN notif is promoted to primary - assertIsNotifChip(latest!!.primary, notifIcon) + assertIsNotifChip(latest!!.primary, notifIcon, "notif") assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt index dd81b75e180e..1a5f57dd43f8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.featurepods.media.domain.interactor +import android.graphics.drawable.Drawable import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -23,12 +24,15 @@ import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.shared.model.MediaAction +import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) @@ -102,4 +106,70 @@ class MediaControlChipInteractorTest : SysuiTestCase() { assertThat(model?.songName).isEqualTo(newSongName) } + + @Test + fun mediaControlModel_playPauseActionChanges_emitsUpdatedModel() = + kosmos.runTest { + val model by collectLastValue(underTest.mediaControlModel) + + val mockDrawable = mock<Drawable>() + + val initialAction = + MediaAction( + icon = mockDrawable, + action = {}, + contentDescription = "Initial Action", + background = mockDrawable, + ) + val mediaButton = MediaButton(playOrPause = initialAction) + val userMedia = MediaData(active = true, semanticActions = mediaButton) + val instanceId = userMedia.instanceId + mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + assertThat(model).isNotNull() + assertThat(model?.playOrPause).isEqualTo(initialAction) + + val newAction = + MediaAction( + icon = mockDrawable, + action = {}, + contentDescription = "New Action", + background = mockDrawable, + ) + val updatedMediaButton = MediaButton(playOrPause = newAction) + val updatedUserMedia = userMedia.copy(semanticActions = updatedMediaButton) + mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) + + assertThat(model?.playOrPause).isEqualTo(newAction) + } + + @Test + fun mediaControlModel_playPauseActionRemoved_playPauseNull() = + kosmos.runTest { + val model by collectLastValue(underTest.mediaControlModel) + + val mockDrawable = mock<Drawable>() + + val initialAction = + MediaAction( + icon = mockDrawable, + action = {}, + contentDescription = "Initial Action", + background = mockDrawable, + ) + val mediaButton = MediaButton(playOrPause = initialAction) + val userMedia = MediaData(active = true, semanticActions = mediaButton) + val instanceId = userMedia.instanceId + mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + assertThat(model).isNotNull() + assertThat(model?.playOrPause).isEqualTo(initialAction) + + val updatedUserMedia = userMedia.copy(semanticActions = MediaButton()) + mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) + + assertThat(model?.playOrPause).isNull() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt index c9c961791e89..49b95d92129c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt @@ -45,7 +45,7 @@ import com.google.common.truth.Truth.assertWithMessage import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt new file mode 100644 index 000000000000..72001758d01f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import android.content.res.Configuration +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectValues +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider +import com.android.systemui.statusbar.policy.configurationController +import com.android.systemui.statusbar.policy.fake +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) +class StatusBarContentInsetsViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val configuration = Configuration() + + private val Kosmos.underTest by Kosmos.Fixture { statusBarContentInsetsViewModel } + + @Test + fun contentArea_onMaxBoundsChanged_emitsNewValue() = + kosmos.runTest { + statusBarContentInsetsProvider.start() + + val values by collectValues(underTest.contentArea) + + // WHEN the content area changes + configurationController.fake.notifyLayoutDirectionChanged(isRtl = true) + configurationController.fake.notifyDensityOrFontScaleChanged() + + // THEN the flow emits the new bounds + assertThat(values[0]).isNotEqualTo(values[1]) + } + + @Test + fun contentArea_onDensityOrFontScaleChanged_emitsLastBounds() = + kosmos.runTest { + configuration.densityDpi = 12 + statusBarContentInsetsProvider.start() + + val values by collectValues(underTest.contentArea) + + // WHEN a change happens but it doesn't affect content area + configuration.densityDpi = 20 + configurationController.onConfigurationChanged(configuration) + configurationController.fake.notifyDensityOrFontScaleChanged() + + // THEN it still has the last bounds + assertThat(values).hasSize(1) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index a3ffd91b1728..609885d0214b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -458,7 +458,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_hasNotifEntry_shownAsHUN() = + fun onPromotedNotificationChipTapped_hasNotifEntry_shownAsHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) @@ -473,7 +473,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_noNotifEntry_noHUN() = + fun onPromotedNotificationChipTapped_noNotifEntry_noHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(null) @@ -488,7 +488,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_shownAsHUNEvenIfEntryShouldNot() = + fun onPromotedNotificationChipTapped_shownAsHUNEvenIfEntryShouldNot() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) @@ -511,7 +511,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_atSameTimeAsOnAdded_promotedShownAsHUN() = + fun onPromotedNotificationChipTapped_atSameTimeAsOnAdded_promotedShownAsHUN() = testScope.runTest { // First, the promoted notification appears as not heads up val promotedEntry = NotificationEntryBuilder().setPkg("promotedPackage").build() @@ -548,6 +548,33 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun onPromotedNotificationChipTapped_chipTappedTwice_hunHiddenOnSecondTap() = + testScope.runTest { + whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) + + // WHEN chip tapped first + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) + executor.advanceClockToLast() + executor.runAllReady() + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + // THEN HUN is shown + finishBind(entry) + verify(headsUpManager).showNotification(entry, isPinnedByUser = true) + addHUN(entry) + + // WHEN chip is tapped again + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) + executor.advanceClockToLast() + executor.runAllReady() + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + // THEN HUN is hidden + verify(headsUpManager).removeNotification(eq(entry.key), eq(false), any()) + } + + @Test fun testTransferIsolatedChildAlert_withGroupAlertSummary() { setShouldHeadsUp(groupSummary) whenever(notifPipeline.allNotifs).thenReturn(listOf(groupSummary, groupSibling1)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt index a70d24efada7..e6fbc725af04 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt @@ -28,11 +28,11 @@ import com.android.systemui.statusbar.notification.collection.ShadeListBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderEntryListener import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderGroupListener import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener +import com.android.systemui.util.mockito.withArgCaptor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -59,10 +59,9 @@ class RenderStageManagerTest : SysuiTestCase() { fun setUp() { renderStageManager = RenderStageManager() renderStageManager.attach(shadeListBuilder) - - val captor = argumentCaptor<ShadeListBuilder.OnRenderListListener>() - verify(shadeListBuilder).setOnRenderListListener(captor.capture()) - onRenderListListener = captor.lastValue + onRenderListListener = withArgCaptor { + verify(shadeListBuilder).setOnRenderListListener(capture()) + } } private fun setUpRenderer() { @@ -102,7 +101,6 @@ class RenderStageManagerTest : SysuiTestCase() { // VERIFY that the renderer is not queried for group or row controllers inOrder(spyViewRenderer).apply { verify(spyViewRenderer, times(1)).onRenderList(any()) - verify(spyViewRenderer, times(1)).getStackController() verify(spyViewRenderer, never()).getGroupController(any()) verify(spyViewRenderer, never()).getRowController(any()) verify(spyViewRenderer, times(1)).onDispatchComplete() @@ -122,7 +120,6 @@ class RenderStageManagerTest : SysuiTestCase() { // VERIFY that the renderer is queried once per group/entry inOrder(spyViewRenderer).apply { verify(spyViewRenderer, times(1)).onRenderList(any()) - verify(spyViewRenderer, times(1)).getStackController() verify(spyViewRenderer, times(2)).getGroupController(any()) verify(spyViewRenderer, times(8)).getRowController(any()) verify(spyViewRenderer, times(1)).onDispatchComplete() @@ -145,7 +142,6 @@ class RenderStageManagerTest : SysuiTestCase() { // VERIFY that the renderer is queried once per group/entry inOrder(spyViewRenderer).apply { verify(spyViewRenderer, times(1)).onRenderList(any()) - verify(spyViewRenderer, times(1)).getStackController() verify(spyViewRenderer, times(2)).getGroupController(any()) verify(spyViewRenderer, times(8)).getRowController(any()) verify(spyViewRenderer, times(1)).onDispatchComplete() @@ -163,7 +159,7 @@ class RenderStageManagerTest : SysuiTestCase() { onRenderListListener.onRenderList(listWith2Groups8Entries()) // VERIFY that the listeners are invoked once per group and once per entry - verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any()) + verify(onAfterRenderListListener, times(1)).onAfterRenderList(any()) verify(onAfterRenderGroupListener, times(2)).onAfterRenderGroup(any(), any()) verify(onAfterRenderEntryListener, times(8)).onAfterRenderEntry(any(), any()) verifyNoMoreInteractions( @@ -183,7 +179,7 @@ class RenderStageManagerTest : SysuiTestCase() { onRenderListListener.onRenderList(listOf()) // VERIFY that the stack listener is invoked once but other listeners are not - verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any()) + verify(onAfterRenderListListener, times(1)).onAfterRenderList(any()) verify(onAfterRenderGroupListener, never()).onAfterRenderGroup(any(), any()) verify(onAfterRenderEntryListener, never()).onAfterRenderEntry(any(), any()) verifyNoMoreInteractions( @@ -204,8 +200,6 @@ class RenderStageManagerTest : SysuiTestCase() { private class FakeNotifViewRenderer : NotifViewRenderer { override fun onRenderList(notifList: List<ListEntry>) {} - override fun getStackController(): NotifStackController = mock() - override fun getGroupController(group: GroupEntry): NotifGroupController = mock() override fun getRowController(entry: NotificationEntry): NotifRowController = mock() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt index 54ce88b40c11..83c61507a506 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt @@ -26,7 +26,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -275,7 +275,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -293,7 +292,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -311,7 +309,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 0, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -329,7 +326,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -347,7 +343,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = true, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = true, @@ -365,7 +360,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -383,7 +377,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = true, @@ -401,7 +394,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt index 22ef408e266c..fae7d515d305 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor @@ -412,46 +413,53 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { @Test fun statusBarHeadsUpState_pinnedBySystem() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedBySystem) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedBySystem) + assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedBySystem)) + assertThat(status).isEqualTo(PinnedStatus.PinnedBySystem) } @Test fun statusBarHeadsUpState_pinnedByUser() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedByUser) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedByUser) + assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedByUser)) + assertThat(status).isEqualTo(PinnedStatus.PinnedByUser) } @Test fun statusBarHeadsUpState_withoutPinnedNotifications_notPinned() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", PinnedStatus.NotPinned) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.NotPinned) + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status).isEqualTo(PinnedStatus.NotPinned) } @Test fun statusBarHeadsUpState_whenShadeExpanded_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -463,13 +471,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // should emit `false`. kosmos.fakeShadeRepository.setLegacyShadeExpansion(1.0f) - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_notificationsAreHidden_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -477,13 +487,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND the notifications are hidden keyguardViewStateRepository.areNotificationsFullyHidden.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_onLockScreen_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -494,13 +506,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { testSetup = true, ) - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_onByPassLockScreen_true() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -513,13 +527,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND bypass is enabled faceAuthRepository.isBypassEnabled.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isTrue() + assertThat(state).isInstanceOf(TopPinnedState.Pinned::class.java) + assertThat(status!!.isPinned).isTrue() } @Test fun statusBarHeadsUpState_onByPassLockScreen_withoutNotifications_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN no pinned rows // AND the lock screen is shown @@ -530,7 +546,8 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND bypass is enabled faceAuthRepository.isBypassEnabled.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt index 34f46088ad79..3d5d1eddf581 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt @@ -33,7 +33,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.policy.data.repository.zenModeRepository import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -48,7 +47,6 @@ import platform.test.runner.parameterized.Parameters @OptIn(ExperimentalCoroutinesApi::class) @RunWith(ParameterizedAndroidJunit4::class) @SmallTest -@EnableFlags(FooterViewRefactor.FLAG_NAME) class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java index 615f4b01df9b..daa1db2d49fa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.notification.footer.ui.view; -import static com.android.systemui.log.LogAssertKt.assertLogsWtf; - import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertFalse; @@ -34,7 +32,6 @@ import static org.mockito.Mockito.verify; import android.content.Context; import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.FlagsParameterization; import android.view.LayoutInflater; import android.view.View; @@ -44,7 +41,6 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.res.R; -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter; import org.junit.Before; @@ -62,8 +58,7 @@ public class FooterViewTest extends SysuiTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getFlags() { - return FlagsParameterization.progressionOf(FooterViewRefactor.FLAG_NAME, - NotifRedesignFooter.FLAG_NAME); + return FlagsParameterization.allCombinationsOf(NotifRedesignFooter.FLAG_NAME); } public FooterViewTest(FlagsParameterization flags) { @@ -106,24 +101,6 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void setHistoryShown() { - mView.showHistory(true); - assertTrue(mView.isHistoryShown()); - assertTrue(((TextView) mView.findViewById(R.id.manage_text)) - .getText().toString().contains("History")); - } - - @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void setHistoryNotShown() { - mView.showHistory(false); - assertFalse(mView.isHistoryShown()); - assertTrue(((TextView) mView.findViewById(R.id.manage_text)) - .getText().toString().contains("Manage")); - } - - @Test public void testPerformVisibilityAnimation() { mView.setVisible(false /* visible */, false /* animate */); assertFalse(mView.isVisible()); @@ -140,7 +117,6 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) @DisableFlags(NotifRedesignFooter.FLAG_NAME) public void testSetManageOrHistoryButtonText_resourceOnlyFetchedOnce() { int resId = R.string.manage_notifications_history_text; @@ -160,16 +136,6 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void testSetManageOrHistoryButtonText_expectsFlagEnabled() { - clearInvocations(mSpyContext); - int resId = R.string.manage_notifications_history_text; - assertLogsWtf(() -> mView.setManageOrHistoryButtonText(resId)); - verify(mSpyContext, never()).getString(anyInt()); - } - - @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) @DisableFlags(NotifRedesignFooter.FLAG_NAME) public void testSetManageOrHistoryButtonDescription_resourceOnlyFetchedOnce() { int resId = R.string.manage_notifications_history_text; @@ -189,16 +155,6 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void testSetManageOrHistoryButtonDescription_expectsFlagEnabled() { - clearInvocations(mSpyContext); - int resId = R.string.accessibility_clear_all; - assertLogsWtf(() -> mView.setManageOrHistoryButtonDescription(resId)); - verify(mSpyContext, never()).getString(anyInt()); - } - - @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) public void testSetClearAllButtonText_resourceOnlyFetchedOnce() { int resId = R.string.clear_all_notifications_text; mView.setClearAllButtonText(resId); @@ -217,16 +173,6 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void testSetClearAllButtonText_expectsFlagEnabled() { - clearInvocations(mSpyContext); - int resId = R.string.clear_all_notifications_text; - assertLogsWtf(() -> mView.setClearAllButtonText(resId)); - verify(mSpyContext, never()).getString(anyInt()); - } - - @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) public void testSetClearAllButtonDescription_resourceOnlyFetchedOnce() { int resId = R.string.accessibility_clear_all; mView.setClearAllButtonDescription(resId); @@ -245,16 +191,6 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void testSetClearAllButtonDescription_expectsFlagEnabled() { - clearInvocations(mSpyContext); - int resId = R.string.accessibility_clear_all; - assertLogsWtf(() -> mView.setClearAllButtonDescription(resId)); - verify(mSpyContext, never()).getString(anyInt()); - } - - @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) public void testSetMessageString_resourceOnlyFetchedOnce() { int resId = R.string.unlock_to_see_notif_text; mView.setMessageString(resId); @@ -273,16 +209,6 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void testSetMessageString_expectsFlagEnabled() { - clearInvocations(mSpyContext); - int resId = R.string.unlock_to_see_notif_text; - assertLogsWtf(() -> mView.setMessageString(resId)); - verify(mSpyContext, never()).getString(anyInt()); - } - - @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) public void testSetMessageIcon_resourceOnlyFetchedOnce() { int resId = R.drawable.ic_friction_lock_closed; mView.setMessageIcon(resId); @@ -298,15 +224,6 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void testSetMessageIcon_expectsFlagEnabled() { - clearInvocations(mSpyContext); - int resId = R.drawable.ic_friction_lock_closed; - assertLogsWtf(() -> mView.setMessageIcon(resId)); - verify(mSpyContext, never()).getDrawable(anyInt()); - } - - @Test public void testSetFooterLabelVisible() { mView.setFooterLabelVisible(true); assertThat(mView.findViewById(R.id.unlock_prompt_footer).getVisibility()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt index 1adfc2b72214..b3a60b052d08 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt @@ -37,10 +37,9 @@ import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.res.R import com.android.systemui.shade.shadeTestUtil import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter import com.android.systemui.testKosmos import com.android.systemui.util.ui.isAnimating @@ -57,7 +56,6 @@ import platform.test.runner.parameterized.Parameters @RunWith(ParameterizedAndroidJunit4::class) @SmallTest -@EnableFlags(FooterViewRefactor.FLAG_NAME) class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos().apply { @@ -117,7 +115,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -135,7 +132,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -153,7 +149,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -185,7 +180,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { // AND there are clearable notifications activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -219,7 +213,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { // AND there are clearable notifications activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index 72a91bc12f8d..14bbd38ece2c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -279,7 +279,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { notification); RemoteViews headerRemoteViews; if (lowPriority) { - headerRemoteViews = builder.makeLowPriorityContentView(true, false); + headerRemoteViews = builder.makeLowPriorityContentView(true); } else { headerRemoteViews = builder.makeNotificationGroupHeader(); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index c6cffa9da13b..20cd6c7517e2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -25,14 +25,10 @@ import static com.android.systemui.statusbar.notification.stack.NotificationStac import static kotlinx.coroutines.flow.FlowKt.emptyFlow; -import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -45,7 +41,6 @@ import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper; import android.view.MotionEvent; -import android.view.View; import android.view.ViewTreeObserver; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -57,15 +52,12 @@ import com.android.internal.logging.nano.MetricsProto; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.ExpandHelper; import com.android.systemui.SysuiTestCase; -import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.DisableSceneContainer; import com.android.systemui.flags.EnableSceneContainer; import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; -import com.android.systemui.keyguard.shared.model.KeyguardState; -import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.media.controls.ui.controller.KeyguardMediaController; import com.android.systemui.plugins.ActivityStarter; @@ -78,23 +70,18 @@ import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener; -import com.android.systemui.statusbar.NotificationRemoteInputManager; -import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.DynamicPrivacyController; -import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.collection.NotifCollection; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; -import com.android.systemui.statusbar.notification.collection.render.NotifStats; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; -import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; -import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor; -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; +import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper; import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -106,11 +93,8 @@ import com.android.systemui.statusbar.notification.stack.ui.viewbinder.Notificat import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.DeviceProvisionedController; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; -import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor; @@ -145,16 +129,13 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private Provider<IStatusBarService> mStatusBarService; @Mock private NotificationRoundnessManager mNotificationRoundnessManager; @Mock private TunerService mTunerService; - @Mock private DeviceProvisionedController mDeviceProvisionedController; @Mock private DynamicPrivacyController mDynamicPrivacyController; @Mock private ConfigurationController mConfigurationController; @Mock private NotificationStackScrollLayout mNotificationStackScrollLayout; - @Mock private ZenModeController mZenModeController; @Mock private KeyguardMediaController mKeyguardMediaController; @Mock private SysuiStatusBarStateController mSysuiStatusBarStateController; @Mock private KeyguardBypassController mKeyguardBypassController; @Mock private PowerInteractor mPowerInteractor; - @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor; @Mock private WallpaperInteractor mWallpaperInteractor; @Mock private NotificationLockscreenUserManager mNotificationLockscreenUserManager; @Mock private MetricsLogger mMetricsLogger; @@ -164,12 +145,10 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { private NotificationSwipeHelper.Builder mNotificationSwipeHelperBuilder; @Mock private NotificationSwipeHelper mNotificationSwipeHelper; @Mock private GroupExpansionManager mGroupExpansionManager; - @Mock private SectionHeaderController mSilentHeaderController; @Mock private NotifPipeline mNotifPipeline; @Mock private NotifCollection mNotifCollection; @Mock private UiEventLogger mUiEventLogger; @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController; - @Mock private NotificationRemoteInputManager mRemoteInputManager; @Mock private VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; @Mock private ShadeController mShadeController; @Mock private Provider<WindowRootView> mWindowRootView; @@ -193,9 +172,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor; - private final SeenNotificationsInteractor mSeenNotificationsInteractor = - mKosmos.getSeenNotificationsInteractor(); - private NotificationStackScrollLayoutController mController; private NotificationTestHelper mNotificationTestHelper; @@ -279,114 +255,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void testUpdateEmptyShadeView_notificationsVisible_zenHiding() { - when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(true); - initController(/* viewIsAttached= */ true); - - setupShowEmptyShadeViewState(true); - reset(mNotificationStackScrollLayout); - mController.updateShowEmptyShadeView(); - verify(mNotificationStackScrollLayout).updateEmptyShadeView( - /* visible= */ true, - /* notifVisibleInShade= */ true); - - setupShowEmptyShadeViewState(false); - reset(mNotificationStackScrollLayout); - mController.updateShowEmptyShadeView(); - verify(mNotificationStackScrollLayout).updateEmptyShadeView( - /* visible= */ false, - /* notifVisibleInShade= */ true); - } - - @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void testUpdateEmptyShadeView_notificationsHidden_zenNotHiding() { - when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false); - initController(/* viewIsAttached= */ true); - - setupShowEmptyShadeViewState(true); - reset(mNotificationStackScrollLayout); - mController.updateShowEmptyShadeView(); - verify(mNotificationStackScrollLayout).updateEmptyShadeView( - /* visible= */ true, - /* notifVisibleInShade= */ false); - - setupShowEmptyShadeViewState(false); - reset(mNotificationStackScrollLayout); - mController.updateShowEmptyShadeView(); - verify(mNotificationStackScrollLayout).updateEmptyShadeView( - /* visible= */ false, - /* notifVisibleInShade= */ false); - } - - @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void testUpdateEmptyShadeView_splitShadeMode_alwaysShowEmptyView() { - when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false); - initController(/* viewIsAttached= */ true); - - verify(mSysuiStatusBarStateController).addCallback( - mStateListenerArgumentCaptor.capture(), anyInt()); - StatusBarStateController.StateListener stateListener = - mStateListenerArgumentCaptor.getValue(); - stateListener.onStateChanged(SHADE); - mController.getView().removeAllViews(); - - mController.setQsFullScreen(false); - reset(mNotificationStackScrollLayout); - mController.updateShowEmptyShadeView(); - verify(mNotificationStackScrollLayout).updateEmptyShadeView( - /* visible= */ true, - /* notifVisibleInShade= */ false); - - mController.setQsFullScreen(true); - reset(mNotificationStackScrollLayout); - mController.updateShowEmptyShadeView(); - verify(mNotificationStackScrollLayout).updateEmptyShadeView( - /* visible= */ true, - /* notifVisibleInShade= */ false); - } - - @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void testUpdateEmptyShadeView_bouncerShowing_hideEmptyView() { - when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false); - initController(/* viewIsAttached= */ true); - - when(mPrimaryBouncerInteractor.isBouncerShowing()).thenReturn(true); - - setupShowEmptyShadeViewState(true); - reset(mNotificationStackScrollLayout); - mController.updateShowEmptyShadeView(); - - // THEN the PrimaryBouncerInteractor value is used. Since the bouncer is showing, we - // hide the empty view. - verify(mNotificationStackScrollLayout).updateEmptyShadeView( - /* visible= */ false, - /* areNotificationsHiddenInShade= */ false); - } - - @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void testUpdateEmptyShadeView_bouncerNotShowing_showEmptyView() { - when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false); - initController(/* viewIsAttached= */ true); - - when(mPrimaryBouncerInteractor.isBouncerShowing()).thenReturn(false); - - setupShowEmptyShadeViewState(true); - reset(mNotificationStackScrollLayout); - mController.updateShowEmptyShadeView(); - - // THEN the PrimaryBouncerInteractor value is used. Since the bouncer isn't showing, we - // can show the empty view. - verify(mNotificationStackScrollLayout).updateEmptyShadeView( - /* visible= */ true, - /* areNotificationsHiddenInShade= */ false); - } - - @Test public void testOnUserChange_verifyNotSensitive() { when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); initController(/* viewIsAttached= */ true); @@ -788,31 +656,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void testUpdateFooter_remoteInput() { - ArgumentCaptor<RemoteInputController.Callback> callbackCaptor = - ArgumentCaptor.forClass(RemoteInputController.Callback.class); - doNothing().when(mRemoteInputManager).addControllerCallback(callbackCaptor.capture()); - when(mRemoteInputManager.isRemoteInputActive()).thenReturn(false); - initController(/* viewIsAttached= */ true); - verify(mNotificationStackScrollLayout).setIsRemoteInputActive(false); - RemoteInputController.Callback callback = callbackCaptor.getValue(); - callback.onRemoteInputActive(true); - verify(mNotificationStackScrollLayout).setIsRemoteInputActive(true); - } - - @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void testSetNotifStats_updatesHasFilteredOutSeenNotifications() { - initController(/* viewIsAttached= */ true); - mSeenNotificationsInteractor.setHasFilteredOutSeenNotifications(true); - mController.getNotifStackController().setNotifStats(NotifStats.getEmpty()); - verify(mNotificationStackScrollLayout).setHasFilteredOutSeenNotifications(true); - verify(mNotificationStackScrollLayout).updateFooter(); - verify(mNotificationStackScrollLayout).updateEmptyShadeView(anyBoolean(), anyBoolean()); - } - - @Test public void testAttach_updatesViewStatusBarState() { // GIVEN: Controller is attached initController(/* viewIsAttached= */ true); @@ -844,98 +687,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void updateImportantForAccessibility_noChild_onKeyGuard_notImportantForA11y() { - // GIVEN: Controller is attached, active notifications is empty, - // and mNotificationStackScrollLayout.onKeyguard() is true - initController(/* viewIsAttached= */ true); - when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(true); - mController.getNotifStackController().setNotifStats(NotifStats.getEmpty()); - - // THEN: mNotificationStackScrollLayout should not be important for A11y - verify(mNotificationStackScrollLayout) - .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - } - - @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void updateImportantForAccessibility_hasChild_onKeyGuard_importantForA11y() { - // GIVEN: Controller is attached, active notifications is not empty, - // and mNotificationStackScrollLayout.onKeyguard() is true - initController(/* viewIsAttached= */ true); - when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(true); - mController.getNotifStackController().setNotifStats( - new NotifStats( - /* numActiveNotifs = */ 1, - /* hasNonClearableAlertingNotifs = */ false, - /* hasClearableAlertingNotifs = */ false, - /* hasNonClearableSilentNotifs = */ false, - /* hasClearableSilentNotifs = */ false) - ); - - // THEN: mNotificationStackScrollLayout should be important for A11y - verify(mNotificationStackScrollLayout) - .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - - @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void updateImportantForAccessibility_hasChild_notOnKeyGuard_importantForA11y() { - // GIVEN: Controller is attached, active notifications is not empty, - // and mNotificationStackScrollLayout.onKeyguard() is false - initController(/* viewIsAttached= */ true); - when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(false); - mController.getNotifStackController().setNotifStats( - new NotifStats( - /* numActiveNotifs = */ 1, - /* hasNonClearableAlertingNotifs = */ false, - /* hasClearableAlertingNotifs = */ false, - /* hasNonClearableSilentNotifs = */ false, - /* hasClearableSilentNotifs = */ false) - ); - - // THEN: mNotificationStackScrollLayout should be important for A11y - verify(mNotificationStackScrollLayout) - .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - - @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void updateImportantForAccessibility_noChild_notOnKeyGuard_importantForA11y() { - // GIVEN: Controller is attached, active notifications is empty, - // and mNotificationStackScrollLayout.onKeyguard() is false - initController(/* viewIsAttached= */ true); - when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(false); - mController.getNotifStackController().setNotifStats(NotifStats.getEmpty()); - - // THEN: mNotificationStackScrollLayout should be important for A11y - verify(mNotificationStackScrollLayout) - .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - - @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void updateEmptyShadeView_onKeyguardTransitionToAod_hidesView() { - initController(/* viewIsAttached= */ true); - mController.onKeyguardTransitionChanged( - new TransitionStep( - /* from= */ KeyguardState.GONE, - /* to= */ KeyguardState.AOD)); - verify(mNotificationStackScrollLayout).updateEmptyShadeView(eq(false), anyBoolean()); - } - - @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) - public void updateEmptyShadeView_onKeyguardOccludedTransitionToAod_hidesView() { - initController(/* viewIsAttached= */ true); - mController.onKeyguardTransitionChanged( - new TransitionStep( - /* from= */ KeyguardState.OCCLUDED, - /* to= */ KeyguardState.AOD)); - verify(mNotificationStackScrollLayout).updateEmptyShadeView(eq(false), anyBoolean()); - } - - @Test @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) public void sensitiveNotificationProtectionControllerListenerNotRegistered() { initController(/* viewIsAttached= */ true); @@ -996,24 +747,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { return argThat(new LogMatcher(category, type)); } - private void setupShowEmptyShadeViewState(boolean toShow) { - if (toShow) { - mController.onKeyguardTransitionChanged( - new TransitionStep( - /* from= */ KeyguardState.LOCKSCREEN, - /* to= */ KeyguardState.GONE)); - mController.setQsFullScreen(false); - mController.getView().removeAllViews(); - } else { - mController.onKeyguardTransitionChanged( - new TransitionStep( - /* from= */ KeyguardState.GONE, - /* to= */ KeyguardState.AOD)); - mController.setQsFullScreen(true); - mController.getView().addContainerView(mock(ExpandableNotificationRow.class)); - } - } - private void initController(boolean viewIsAttached) { when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(viewIsAttached); ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); @@ -1033,16 +766,12 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mStatusBarService, mNotificationRoundnessManager, mTunerService, - mDeviceProvisionedController, mDynamicPrivacyController, mConfigurationController, mSysuiStatusBarStateController, mKeyguardMediaController, mKeyguardBypassController, mPowerInteractor, - mPrimaryBouncerInteractor, - mKeyguardTransitionRepo, - mZenModeController, mNotificationLockscreenUserManager, mMetricsLogger, mColorUpdateLogger, @@ -1051,14 +780,11 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { new FalsingManagerFake(), mNotificationSwipeHelperBuilder, mGroupExpansionManager, - mSilentHeaderController, mNotifPipeline, mNotifCollection, mLockscreenShadeTransitionController, mUiEventLogger, - mRemoteInputManager, mVisibilityLocationProviderDelegator, - mSeenNotificationsInteractor, mViewBinder, mShadeController, mWindowRootView, @@ -1076,7 +802,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } static class LogMatcher implements ArgumentMatcher<LogMaker> { - private int mCategory, mType; + private final int mCategory, mType; LogMatcher(int category, int type) { mCategory = category; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index dcac2941b48b..39cff63f363e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -2,12 +2,10 @@ package com.android.systemui.statusbar.notification.stack import android.annotation.DimenRes import android.content.pm.PackageManager -import android.platform.test.annotations.DisableFlags import android.platform.test.flag.junit.FlagsParameterization import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress -import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation.getContentAlpha import com.android.systemui.dump.DumpManager @@ -740,20 +738,6 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat((footerView.viewState as FooterViewState).hideContent).isTrue() } - @DisableFlags(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR) - @Test - fun resetViewStates_clearAllInProgress_allRowsRemoved_emptyShade_footerHidden() { - ambientState.isClearAllInProgress = true - ambientState.isShadeExpanded = true - ambientState.stackEndHeight = maxPanelHeight // plenty space for the footer in the stack - hostView.removeAllViews() // remove all rows - hostView.addView(footerView) - - stackScrollAlgorithm.resetViewStates(ambientState, 0) - - assertThat((footerView.viewState as FooterViewState).hideContent).isTrue() - } - @Test fun getGapForLocation_onLockscreen_returnsSmallGap() { val gap = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index e592e4b319e3..1b4f9a79557d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel -import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -41,7 +40,6 @@ import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRo import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository @@ -63,7 +61,6 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) -@EnableFlags(FooterViewRefactor.FLAG_NAME) class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos().apply { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 459778868ccd..a045b37a8119 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -70,6 +70,7 @@ import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel.HorizontalPosition import com.android.systemui.testKosmos +import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlin.test.assertIs @@ -1395,6 +1396,19 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S assertThat(stackAbsoluteBottom).isEqualTo(100F) } + @Test + fun blurRadius_emitsValues_fromPrimaryBouncerTransitions() = + testScope.runTest { + val blurRadius by collectLastValue(underTest.blurRadius) + assertThat(blurRadius).isEqualTo(0.0f) + + kosmos.fakeBouncerTransitions.first().notificationBlurRadius.value = 30.0f + assertThat(blurRadius).isEqualTo(30.0f) + + kosmos.fakeBouncerTransitions.last().notificationBlurRadius.value = 40.0f + assertThat(blurRadius).isEqualTo(40.0f) + } + private suspend fun TestScope.showLockscreen() { shadeTestUtil.setQsExpansion(0f) shadeTestUtil.setLockscreenShadeExpansion(0f) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 9eafcdbadfa5..e2330f448a1b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.hardware.biometrics.BiometricSourceType; +import android.hardware.fingerprint.FingerprintManager; import android.os.Handler; import android.os.PowerManager; import android.os.UserHandle; @@ -431,9 +432,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test - public void onUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() { - // GIVEN UDFPS is supported - when(mUpdateMonitor.isUdfpsSupported()).thenReturn(true); + public void onOpticalUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() { + // GIVEN optical UDFPS is supported + when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(true); // WHEN udfps fails once - then don't show the bouncer yet mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); @@ -451,6 +452,25 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test + public void onUltrasonicUdfpsLockout_showPrimaryBouncer() { + // GIVEN ultrasonic UDFPS is supported + when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(false); + + // WHEN udfps fails three times, don't show bouncer + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); + + // WHEN lockout is received + mBiometricUnlockController.onBiometricError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + "Lockout", BiometricSourceType.FINGERPRINT); + + // THEN show bouncer + verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true); + } + + @Test public void onFinishedGoingToSleep_authenticatesWhenPending() { when(mUpdateMonitor.isGoingToSleep()).thenReturn(true); mBiometricUnlockController.mWakefulnessObserver.onFinishedGoingToSleep(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt index ffd349d744a8..43ad042ecf78 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt @@ -53,7 +53,7 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler -import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider +import com.android.systemui.statusbar.layout.mockStatusBarContentInsetsProvider import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager import com.android.systemui.statusbar.policy.BatteryController @@ -153,7 +153,8 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { shadeViewStateProvider = TestShadeViewStateProvider() Mockito.`when`( - kosmos.statusBarContentInsetsProvider.getStatusBarContentInsetsForCurrentRotation() + kosmos.mockStatusBarContentInsetsProvider + .getStatusBarContentInsetsForCurrentRotation() ) .thenReturn(Insets.of(0, 0, 0, 0)) @@ -162,7 +163,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { Mockito.`when`(iconManagerFactory.create(ArgumentMatchers.any(), ArgumentMatchers.any())) .thenReturn(iconManager) Mockito.`when`(statusBarContentInsetsProviderStore.defaultDisplay) - .thenReturn(kosmos.statusBarContentInsetsProvider) + .thenReturn(kosmos.mockStatusBarContentInsetsProvider) allowTestableLooperAsMainThread() looper.runWithLooper { keyguardStatusBarView = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index d174484219ff..2e12336f6e93 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -40,7 +40,6 @@ import static org.mockito.Mockito.when; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.TestableLooper; @@ -610,7 +609,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test - @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER) public void testPredictiveBackCallback_registration() { /* verify that a predictive back callback is registered when the bouncer becomes visible */ mBouncerExpansionCallback.onVisibilityChanged(true); @@ -625,7 +623,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test - @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER) public void testPredictiveBackCallback_invocationHidesBouncer() { mBouncerExpansionCallback.onVisibilityChanged(true); /* capture the predictive back callback during registration */ @@ -643,7 +640,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test - @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER) public void testPredictiveBackCallback_noBackAnimationForFullScreenBouncer() { when(mKeyguardSecurityModel.getSecurityMode(anyInt())) .thenReturn(KeyguardSecurityModel.SecurityMode.SimPin); @@ -663,7 +659,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test - @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER) public void testPredictiveBackCallback_forwardsBackDispatches() { mBouncerExpansionCallback.onVisibilityChanged(true); /* capture the predictive back callback during registration */ diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java index 0652a835cb7c..650fa7ce46de 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java @@ -31,7 +31,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; -import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.ravenwood.RavenwoodRule; @@ -41,7 +40,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.systemui.Dependency; -import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.animation.back.BackAnimationSpec; @@ -137,7 +135,6 @@ public class SystemUIDialogTest extends SysuiTestCase { } @Test - @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS) public void usePredictiveBackAnimFlag() { final SystemUIDialog dialog = new SystemUIDialog(mContext); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt index cf512cdee800..b98409906f8d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt @@ -28,9 +28,7 @@ import android.view.View import android.widget.LinearLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP import com.android.systemui.SysuiTestCase import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.dump.DumpManager @@ -42,7 +40,6 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -76,9 +73,8 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @OptIn(ExperimentalCoroutinesApi::class) -@EnableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP) @DisableFlags(StatusBarChipsModernization.FLAG_NAME) -class OngoingCallControllerViaRepoTest : SysuiTestCase() { +class OngoingCallControllerTest : SysuiTestCase() { private val kosmos = Kosmos() private val clock = kosmos.fakeSystemClock @@ -114,7 +110,6 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() { testScope.backgroundScope, context, ongoingCallRepository, - mock<CommonNotifCollection>(), kosmos.activeNotificationsInteractor, clock, mockActivityStarter, @@ -162,28 +157,7 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun interactorHasOngoingCallNotif_notifIconFlagOff_repoHasNoNotifIcon() = - testScope.runTest { - val icon = mock<StatusBarIconView>() - setNotifOnRepo( - activeNotificationModel( - key = "ongoingNotif", - callType = CallType.Ongoing, - uid = CALL_UID, - statusBarChipIcon = icon, - whenTime = 567, - ) - ) - - val repoState = ongoingCallRepository.ongoingCallState.value - assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java) - assertThat((repoState as OngoingCallModel.InCall).notificationIconView).isNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun interactorHasOngoingCallNotif_notifIconFlagOn_repoHasNotifIcon() = + fun interactorHasOngoingCallNotif_repoHasNotifIcon() = testScope.runTest { val icon = mock<StatusBarIconView>() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt deleted file mode 100644 index cd3539d6b9a5..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt +++ /dev/null @@ -1,694 +0,0 @@ -/* - * Copyright (C) 2021 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.ongoingcall - -import android.app.ActivityManager -import android.app.IActivityManager -import android.app.IUidObserver -import android.app.Notification -import android.app.PendingIntent -import android.app.Person -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags -import android.service.notification.NotificationListenerService.REASON_USER_STOPPED -import android.testing.TestableLooper -import android.view.LayoutInflater -import android.view.View -import android.widget.LinearLayout -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP -import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.log.logcatLogBuffer -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.res.R -import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository -import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor -import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel -import com.android.systemui.statusbar.window.StatusBarWindowController -import com.android.systemui.statusbar.window.StatusBarWindowControllerStore -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.ArgumentMatchers.anyString -import org.mockito.ArgumentMatchers.nullable -import org.mockito.Mock -import org.mockito.Mockito.eq -import org.mockito.Mockito.mock -import org.mockito.Mockito.never -import org.mockito.Mockito.reset -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.whenever - -private const val CALL_UID = 900 - -// A process state that represents the process being visible to the user. -private const val PROC_STATE_VISIBLE = ActivityManager.PROCESS_STATE_TOP - -// A process state that represents the process being invisible to the user. -private const val PROC_STATE_INVISIBLE = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE - -@SmallTest -@RunWith(AndroidJUnit4::class) -@TestableLooper.RunWithLooper -@OptIn(ExperimentalCoroutinesApi::class) -@DisableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, StatusBarChipsModernization.FLAG_NAME) -class OngoingCallControllerViaListenerTest : SysuiTestCase() { - private val kosmos = Kosmos() - - private val clock = FakeSystemClock() - private val mainExecutor = FakeExecutor(clock) - private val testScope = TestScope() - private val statusBarModeRepository = FakeStatusBarModeRepository() - private val ongoingCallRepository = kosmos.ongoingCallRepository - - private lateinit var controller: OngoingCallController - private lateinit var notifCollectionListener: NotifCollectionListener - - @Mock - private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler - @Mock private lateinit var mockOngoingCallListener: OngoingCallListener - @Mock private lateinit var mockActivityStarter: ActivityStarter - @Mock private lateinit var mockIActivityManager: IActivityManager - @Mock private lateinit var mockStatusBarWindowController: StatusBarWindowController - @Mock private lateinit var mockStatusBarWindowControllerStore: StatusBarWindowControllerStore - - private lateinit var chipView: View - - @Before - fun setUp() { - allowTestableLooperAsMainThread() - TestableLooper.get(this).runWithLooper { - chipView = - LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null) - } - - MockitoAnnotations.initMocks(this) - val notificationCollection = mock(CommonNotifCollection::class.java) - whenever(mockStatusBarWindowControllerStore.defaultDisplay) - .thenReturn(mockStatusBarWindowController) - - controller = - OngoingCallController( - testScope.backgroundScope, - context, - ongoingCallRepository, - notificationCollection, - kosmos.activeNotificationsInteractor, - clock, - mockActivityStarter, - mainExecutor, - mockIActivityManager, - DumpManager(), - mockStatusBarWindowControllerStore, - mockSwipeStatusBarAwayGestureHandler, - statusBarModeRepository, - logcatLogBuffer("OngoingCallControllerViaListenerTest"), - ) - controller.start() - controller.addCallback(mockOngoingCallListener) - controller.setChipView(chipView) - onTeardown { controller.tearDownChipView() } - - val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java) - verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture()) - notifCollectionListener = collectionListenerCaptor.value!! - - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - } - - @Test - fun onEntryUpdated_isOngoingCallNotif_listenerAndRepoNotified() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(567) - notifCollectionListener.onEntryUpdated(notification.build()) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - val repoState = ongoingCallRepository.ongoingCallState.value - assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java) - assertThat((repoState as OngoingCallModel.InCall).startTimeMs).isEqualTo(567) - } - - @Test - fun onEntryUpdated_isOngoingCallNotif_windowControllerUpdated() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true) - } - - @Test - fun onEntryUpdated_notOngoingCallNotif_listenerAndRepoNotNotified() { - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_listenerNotifiedTwice() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - verify(mockOngoingCallListener, times(2)).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_repoUpdated() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - /** Regression test for b/191472854. */ - @Test - fun onEntryUpdated_notifHasNullContentIntent_noCrash() { - notifCollectionListener.onEntryUpdated( - createCallNotifEntry(ongoingCallStyle, nullContentIntent = true) - ) - } - - /** Regression test for b/192379214. */ - @Test - @DisableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME, FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsZero_timeHidden() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(0) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isEqualTo(0) - } - - @Test - @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME) - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsZero_timeShown() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(0) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isGreaterThan(0) - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsValid_timeShown() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(clock.currentTimeMillis()) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isGreaterThan(0) - } - - /** Regression test for b/194731244. */ - @Test - fun onEntryUpdated_calledManyTimes_uidObserverOnlyRegisteredOnce() { - for (i in 0 until 4) { - // Re-create the notification each time so that it's considered a different object and - // will re-trigger the whole flow. - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - verify(mockIActivityManager, times(1)).registerUidObserver(any(), any(), any(), any()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_getUidProcessStateThrowsException_noCrash() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenThrow(SecurityException()) - - // No assert required, just check no crash - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_registerUidObserverThrowsException_noCrash() { - `when`( - mockIActivityManager.registerUidObserver( - any(), - any(), - any(), - nullable(String::class.java), - ) - ) - .thenThrow(SecurityException()) - - // No assert required, just check no crash - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_packageNameProvidedToActivityManager() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - val packageNameCaptor = ArgumentCaptor.forClass(String::class.java) - verify(mockIActivityManager) - .registerUidObserver(any(), any(), any(), packageNameCaptor.capture()) - assertThat(packageNameCaptor.value).isNotNull() - } - - /** - * If a call notification is never added before #onEntryRemoved is called, then the listener - * should never be notified. - */ - @Test - fun onEntryRemoved_callNotifNeverAddedBeforehand_listenerNotNotified() { - notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_callNotifAddedThenRemoved_listenerNotified() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockOngoingCallListener) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_callNotifAddedThenRemoved_repoUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryUpdated_callNotifAddedThenRemoved_windowControllerUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false) - } - - /** Regression test for b/188491504. */ - @Test - fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_listenerNotified() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockOngoingCallListener) - - // Create another notification based on the ongoing call one, but remove the features that - // made it a call notification. - val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry) - removedEntryBuilder.modifyNotification(context).style = null - - notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - /** Regression test for b/188491504. */ - @Test - fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_repoUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - - // Create another notification based on the ongoing call one, but remove the features that - // made it a call notification. - val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry) - removedEntryBuilder.modifyNotification(context).style = null - - notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_listenerNotNotified() { - notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_repoNotUpdated() { - notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry()) - - notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - } - - @Test - fun hasOngoingCall_noOngoingCallNotifSent_returnsFalse() { - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_unrelatedNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_screeningCallNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isTrue() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_VISIBLE) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() { - val invalidChipView = LinearLayout(context) - controller.setChipView(invalidChipView) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenRemoved_returnsFalse() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - - notifCollectionListener.onEntryUpdated(ongoingCallNotifEntry) - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenScreeningCallNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenUnrelatedNotifSent_returnsTrue() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isTrue() - } - - /** - * This test fakes a theme change during an ongoing call. - * - * When a theme change happens, [CollapsedStatusBarFragment] and its views get re-created, so - * [OngoingCallController.setChipView] gets called with a new view. If there's an active ongoing - * call when the theme changes, the new view needs to be updated with the call information. - */ - @Test - fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() { - // Start an ongoing call. - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - lateinit var newChipView: View - TestableLooper.get(this).runWithLooper { - newChipView = - LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null) - } - - // Change the chip view associated with the controller. - controller.setChipView(newChipView) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun callProcessChangesToVisible_listenerNotified() { - // Start the call while the process is invisible. - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) - verify(mockIActivityManager) - .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java)) - val uidObserver = captor.value - - // Update the process to visible. - uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0) - mainExecutor.advanceClockToLast() - mainExecutor.runAllReady() - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun callProcessChangesToInvisible_listenerNotified() { - // Start the call while the process is visible. - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_VISIBLE) - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) - verify(mockIActivityManager) - .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java)) - val uidObserver = captor.value - - // Update the process to invisible. - uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0) - mainExecutor.advanceClockToLast() - mainExecutor.runAllReady() - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - /** Regression test for b/212467440. */ - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun chipClicked_activityStarterTriggeredWithUnmodifiedIntent() { - val notifEntry = createOngoingCallNotifEntry() - val pendingIntent = notifEntry.sbn.notification.contentIntent - notifCollectionListener.onEntryUpdated(notifEntry) - - chipView.performClick() - - // Ensure that the sysui didn't modify the notification's intent -- see b/212467440. - verify(mockActivityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any()) - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun callNotificationAdded_chipIsClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(chipView.hasOnClickListeners()).isTrue() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun callNotificationAdded_newChipsEnabled_chipNotClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(chipView.hasOnClickListeners()).isFalse() - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun fullscreenIsTrue_chipStillClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - assertThat(chipView.hasOnClickListeners()).isTrue() - } - - // Swipe gesture tests - - @Test - fun callStartedInImmersiveMode_swipeGestureCallbackAdded() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockSwipeStatusBarAwayGestureHandler) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false - testScope.runCurrent() - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockSwipeStatusBarAwayGestureHandler, never()) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun transitionToImmersiveMode_swipeGestureCallbackAdded() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - verify(mockSwipeStatusBarAwayGestureHandler) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockSwipeStatusBarAwayGestureHandler) - - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false - testScope.runCurrent() - - verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString()) - } - - @Test - fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockSwipeStatusBarAwayGestureHandler) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString()) - } - - // TODO(b/195839150): Add test - // swipeGesturedTriggeredPreviously_entersImmersiveModeAgain_callbackNotAdded(). That's - // difficult to add now because we have no way to trigger [SwipeStatusBarAwayGestureHandler]'s - // callbacks in test. - - // END swipe gesture tests - - private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle) - - private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle) - - private fun createCallNotifEntry( - callStyle: Notification.CallStyle, - nullContentIntent: Boolean = false, - ): NotificationEntry { - val notificationEntryBuilder = NotificationEntryBuilder() - notificationEntryBuilder.modifyNotification(context).style = callStyle - notificationEntryBuilder.setUid(CALL_UID) - - if (nullContentIntent) { - notificationEntryBuilder.modifyNotification(context).setContentIntent(null) - } else { - val contentIntent = mock(PendingIntent::class.java) - notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent) - } - - return notificationEntryBuilder.build() - } - - private fun createNotCallNotifEntry() = NotificationEntryBuilder().build() -} - -private val person = Person.Builder().setName("name").build() -private val hangUpIntent = mock(PendingIntent::class.java) - -private val ongoingCallStyle = Notification.CallStyle.forOngoingCall(person, hangUpIntent) -private val screeningCallStyle = - Notification.CallStyle.forScreeningCall( - person, - hangUpIntent, - /* answerIntent= */ mock(PendingIntent::class.java), - ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt index a9db0b70dd4d..6feada1c9769 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeHomeStatusBarViewModel( override val operatorNameViewModel: StatusBarOperatorNameViewModel ) : HomeStatusBarViewModel { - private val areNotificationLightsOut = MutableStateFlow(false) + override val areNotificationsLightsOut = MutableStateFlow(false) override val isTransitioningFromLockscreenToOccluded = MutableStateFlow(false) @@ -77,14 +77,14 @@ class FakeHomeStatusBarViewModel( override val iconBlockList: MutableStateFlow<List<String>> = MutableStateFlow(listOf()) - override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = areNotificationLightsOut + override val contentArea = MutableStateFlow(Rect(0, 0, 1, 1)) val darkRegions = mutableListOf<Rect>() var darkIconTint = Color.BLACK var lightIconTint = Color.WHITE - override fun areaTint(displayId: Int): Flow<StatusBarTintColor> = + override val areaTint: Flow<StatusBarTintColor> = MutableStateFlow( StatusBarTintColor { viewBounds -> if (DarkIconDispatcher.isInAreas(darkRegions, viewBounds)) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt index e91875cd0885..e95bc3378423 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt @@ -22,13 +22,17 @@ import android.app.StatusBarManager.DISABLE_CLOCK import android.app.StatusBarManager.DISABLE_NONE import android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS import android.app.StatusBarManager.DISABLE_SYSTEM_INFO +import android.content.testableContext import android.graphics.Rect import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.view.Display.DEFAULT_DISPLAY import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.display.data.repository.fake import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -59,7 +63,6 @@ import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip import com.android.systemui.statusbar.data.model.StatusBarMode -import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel @@ -85,6 +88,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.junit.Before import org.junit.Test @@ -104,6 +108,9 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { setUpPackageManagerForMediaProjection(kosmos) } + @Before + fun addDisplays() = runBlocking { kosmos.displayRepository.fake.addDisplay(DEFAULT_DISPLAY) } + @Test fun isTransitioningFromLockscreenToOccluded_started_isTrue() = kosmos.runTest { @@ -363,7 +370,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(testNotifications) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isTrue() } @@ -377,7 +384,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(emptyList()) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -391,7 +398,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(emptyList()) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -405,7 +412,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(testNotifications) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -415,7 +422,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { fun areNotificationsLightsOut_requiresFlagEnabled() = kosmos.runTest { assertLogsWtf { - val flow = underTest.areNotificationsLightsOut(DISPLAY_ID) + val flow = underTest.areNotificationsLightsOut assertThat(flow).isEqualTo(emptyFlow<Boolean>()) } } @@ -1005,11 +1012,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsInDarkBounds_getsDarkTint() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) val tint = areaTint?.tint(Rect(1, 1, 3, 3)) @@ -1019,11 +1026,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsNotInDarkBounds_getsDefaultTint() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) val tint = areaTint?.tint(Rect(6, 6, 7, 7)) @@ -1033,11 +1040,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsInDarkBounds_darkBoundsChange_viewUpdates() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) var tint = areaTint?.tint(Rect(1, 1, 3, 3)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt index 3d6882c3fdaf..7802b921eae0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt @@ -18,9 +18,8 @@ package com.android.systemui.statusbar.policy.domain.interactor import android.app.AutomaticZenRule import android.app.Flags -import android.app.NotificationManager.INTERRUPTION_FILTER_NONE -import android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY import android.app.NotificationManager.Policy +import android.media.AudioManager import android.platform.test.annotations.EnableFlags import android.provider.Settings import android.provider.Settings.Secure.ZEN_DURATION @@ -34,6 +33,8 @@ import androidx.test.filters.SmallTest import com.android.internal.R import com.android.settingslib.notification.data.repository.updateNotificationPolicy import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND +import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope @@ -204,7 +205,7 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun shouldAskForZenDuration_changesWithSetting() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE + val manualDnd = TestModeBuilder().makeManualDnd().setActive(true).build() settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() @@ -233,29 +234,27 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun activateMode_usesCorrectDuration() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE - zenModeRepository.addModes(listOf(manualDnd)) settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() - underTest.activateMode(manualDnd) - assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id)).isNull() + underTest.activateMode(MANUAL_DND) + assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id)).isNull() - zenModeRepository.deactivateMode(manualDnd.id) + zenModeRepository.deactivateMode(MANUAL_DND) settingsRepository.setInt(ZEN_DURATION, 60) runCurrent() - underTest.activateMode(manualDnd) - assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id)) + underTest.activateMode(MANUAL_DND) + assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id)) .isEqualTo(Duration.ofMinutes(60)) } @Test fun deactivateAllModes_updatesCorrectModes() = testScope.runTest { + zenModeRepository.activateMode(MANUAL_DND) zenModeRepository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder().setName("Inactive").setActive(false).build(), TestModeBuilder().setName("Active").setActive(true).build(), ) @@ -389,12 +388,9 @@ class ZenModeInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(underTest.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) - runCurrent() - assertThat(dndMode!!.isActive).isFalse() - zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND_INACTIVE.id) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() assertThat(dndMode!!.isActive).isTrue() @@ -402,115 +398,124 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_MODES_UI) - fun activeModesBlockingEverything_hasModesWithFilterNone() = + fun activeModesBlockingMedia_hasModesWithPolicyBlockingMedia() = testScope.runTest { - val blockingEverything by collectLastValue(underTest.activeModesBlockingEverything) + val blockingMedia by + collectLastValue( + underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_MUSIC)) + ) zenModeRepository.addModes( listOf( TestModeBuilder() - .setName("Filter=None, Not active") - .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setName("Blocks media, Not active") + .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build()) .setActive(false) .build(), TestModeBuilder() - .setName("Filter=Priority, Active") - .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setName("Allows media, Active") + .setZenPolicy(ZenPolicy.Builder().allowMedia(true).build()) .setActive(true) .build(), TestModeBuilder() - .setName("Filter=None, Active") - .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setName("Blocks media, Active") + .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build()) .setActive(true) .build(), TestModeBuilder() - .setName("Filter=None, Active Too") - .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setName("Blocks media, Active Too") + .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build()) .setActive(true) .build(), ) ) runCurrent() - assertThat(blockingEverything!!.mainMode!!.name).isEqualTo("Filter=None, Active") - assertThat(blockingEverything!!.modeNames) - .containsExactly("Filter=None, Active", "Filter=None, Active Too") + assertThat(blockingMedia!!.mainMode!!.name).isEqualTo("Blocks media, Active") + assertThat(blockingMedia!!.modeNames) + .containsExactly("Blocks media, Active", "Blocks media, Active Too") .inOrder() } @Test @EnableFlags(Flags.FLAG_MODES_UI) - fun activeModesBlockingMedia_hasModesWithPolicyBlockingMedia() = + fun activeModesBlockingAlarms_hasModesWithPolicyBlockingAlarms() = testScope.runTest { - val blockingMedia by collectLastValue(underTest.activeModesBlockingMedia) + val blockingAlarms by + collectLastValue( + underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_ALARM)) + ) zenModeRepository.addModes( listOf( TestModeBuilder() - .setName("Blocks media, Not active") - .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build()) + .setName("Blocks alarms, Not active") + .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build()) .setActive(false) .build(), TestModeBuilder() - .setName("Allows media, Active") - .setZenPolicy(ZenPolicy.Builder().allowMedia(true).build()) + .setName("Allows alarms, Active") + .setZenPolicy(ZenPolicy.Builder().allowAlarms(true).build()) .setActive(true) .build(), TestModeBuilder() - .setName("Blocks media, Active") - .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build()) + .setName("Blocks alarms, Active") + .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build()) .setActive(true) .build(), TestModeBuilder() - .setName("Blocks media, Active Too") - .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build()) + .setName("Blocks alarms, Active Too") + .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build()) .setActive(true) .build(), ) ) runCurrent() - assertThat(blockingMedia!!.mainMode!!.name).isEqualTo("Blocks media, Active") - assertThat(blockingMedia!!.modeNames) - .containsExactly("Blocks media, Active", "Blocks media, Active Too") + assertThat(blockingAlarms!!.mainMode!!.name).isEqualTo("Blocks alarms, Active") + assertThat(blockingAlarms!!.modeNames) + .containsExactly("Blocks alarms, Active", "Blocks alarms, Active Too") .inOrder() } @Test @EnableFlags(Flags.FLAG_MODES_UI) - fun activeModesBlockingAlarms_hasModesWithPolicyBlockingAlarms() = + fun activeModesBlockingAlarms_hasModesWithPolicyBlockingSystem() = testScope.runTest { - val blockingAlarms by collectLastValue(underTest.activeModesBlockingAlarms) + val blockingSystem by + collectLastValue( + underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_SYSTEM)) + ) zenModeRepository.addModes( listOf( TestModeBuilder() - .setName("Blocks alarms, Not active") - .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build()) + .setName("Blocks system, Not active") + .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build()) .setActive(false) .build(), TestModeBuilder() - .setName("Allows alarms, Active") - .setZenPolicy(ZenPolicy.Builder().allowAlarms(true).build()) + .setName("Allows system, Active") + .setZenPolicy(ZenPolicy.Builder().allowSystem(true).build()) .setActive(true) .build(), TestModeBuilder() - .setName("Blocks alarms, Active") - .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build()) + .setName("Blocks system, Active") + .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build()) .setActive(true) .build(), TestModeBuilder() - .setName("Blocks alarms, Active Too") - .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build()) + .setName("Blocks system, Active Too") + .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build()) .setActive(true) .build(), ) ) runCurrent() - assertThat(blockingAlarms!!.mainMode!!.name).isEqualTo("Blocks alarms, Active") - assertThat(blockingAlarms!!.modeNames) - .containsExactly("Blocks alarms, Active", "Blocks alarms, Active Too") + assertThat(blockingSystem!!.mainMode!!.name).isEqualTo("Blocks system, Active") + assertThat(blockingSystem!!.modeNames) + .containsExactly("Blocks system, Active", "Blocks system, Active Too") .inOrder() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt index 07d088bbb3d6..856de8ee1c80 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -28,6 +28,7 @@ import android.service.notification.ZenModeConfig.ScheduleInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.settingslib.notification.modes.ZenMode import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -111,7 +112,6 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setName("Disabled by other") .setEnabled(false, /* byUser= */ false) .build(), - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder() .setName("Enabled") .setEnabled(true) @@ -128,14 +128,14 @@ class ModesDialogViewModelTest : SysuiTestCase() { assertThat(tiles).hasSize(3) with(tiles?.elementAt(0)!!) { - assertThat(this.text).isEqualTo("Disabled by other") - assertThat(this.subtext).isEqualTo("Not set") + assertThat(this.text).isEqualTo("Do Not Disturb") + assertThat(this.subtext).isEqualTo("Off") assertThat(this.enabled).isEqualTo(false) } with(tiles?.elementAt(1)!!) { - assertThat(this.text).isEqualTo("Do Not Disturb") - assertThat(this.subtext).isEqualTo("On") - assertThat(this.enabled).isEqualTo(true) + assertThat(this.text).isEqualTo("Disabled by other") + assertThat(this.subtext).isEqualTo("Not set") + assertThat(this.enabled).isEqualTo(false) } with(tiles?.elementAt(2)!!) { assertThat(this.text).isEqualTo("Enabled") @@ -176,18 +176,24 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Do Not Disturb") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("On") assertThat(this.enabled).isEqualTo(true) } - with(tiles?.elementAt(1)!!) { + with(tiles?.elementAt(2)!!) { assertThat(this.text).isEqualTo("Active with manual") assertThat(this.subtext).isEqualTo("On • trigger description") assertThat(this.enabled).isEqualTo(true) } - with(tiles?.elementAt(2)!!) { + with(tiles?.elementAt(3)!!) { assertThat(this.text).isEqualTo("Inactive with manual") assertThat(this.subtext).isEqualTo("Off") assertThat(this.enabled).isEqualTo(false) @@ -226,10 +232,11 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) // Check that tile is initially present - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("On") assertThat(this.enabled).isEqualTo(true) @@ -239,8 +246,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { runCurrent() } // Check that tile is still present at the same location, but turned off - assertThat(tiles).hasSize(3) - with(tiles?.elementAt(0)!!) { + assertThat(tiles).hasSize(4) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -252,9 +259,9 @@ class ModesDialogViewModelTest : SysuiTestCase() { runCurrent() // Check that tile is now gone - assertThat(tiles2).hasSize(2) - assertThat(tiles2?.elementAt(0)!!.text).isEqualTo("Active with manual") - assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Inactive with manual") + assertThat(tiles2).hasSize(3) + assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Active with manual") + assertThat(tiles2?.elementAt(2)!!.text).isEqualTo("Inactive with manual") } @Test @@ -287,22 +294,23 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) repository.removeMode("A") runCurrent() - assertThat(tiles).hasSize(2) + assertThat(tiles).hasSize(3) repository.removeMode("B") runCurrent() - assertThat(tiles).hasSize(1) + assertThat(tiles).hasSize(2) repository.removeMode("C") runCurrent() - assertThat(tiles).hasSize(0) + assertThat(tiles).hasSize(1) } @Test @@ -353,14 +361,15 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles!!).hasSize(7) - assertThat(tiles!![0].subtext).isEqualTo("When the going gets tough") - assertThat(tiles!![1].subtext).isEqualTo("On • When in Rome") - assertThat(tiles!![2].subtext).isEqualTo("Not set") - assertThat(tiles!![3].subtext).isEqualTo("Off") - assertThat(tiles!![4].subtext).isEqualTo("On") - assertThat(tiles!![5].subtext).isEqualTo("Not set") - assertThat(tiles!![6].subtext).isEqualTo(timeScheduleMode.triggerDescription) + // Manual DND is included by default + assertThat(tiles!!).hasSize(8) + assertThat(tiles!![1].subtext).isEqualTo("When the going gets tough") + assertThat(tiles!![2].subtext).isEqualTo("On • When in Rome") + assertThat(tiles!![3].subtext).isEqualTo("Not set") + assertThat(tiles!![4].subtext).isEqualTo("Off") + assertThat(tiles!![5].subtext).isEqualTo("On") + assertThat(tiles!![6].subtext).isEqualTo("Not set") + assertThat(tiles!![7].subtext).isEqualTo(timeScheduleMode.triggerDescription) } @Test @@ -411,32 +420,33 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles!!).hasSize(7) - with(tiles?.elementAt(0)!!) { + // Manual DND is included by default + assertThat(tiles!!).hasSize(8) + with(tiles?.elementAt(1)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("When the going gets tough") } - with(tiles?.elementAt(1)!!) { + with(tiles?.elementAt(2)!!) { assertThat(this.stateDescription).isEqualTo("On") assertThat(this.subtextDescription).isEqualTo("When in Rome") } - with(tiles?.elementAt(2)!!) { + with(tiles?.elementAt(3)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("Not set") } - with(tiles?.elementAt(3)!!) { + with(tiles?.elementAt(4)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEmpty() } - with(tiles?.elementAt(4)!!) { + with(tiles?.elementAt(5)!!) { assertThat(this.stateDescription).isEqualTo("On") assertThat(this.subtextDescription).isEmpty() } - with(tiles?.elementAt(5)!!) { + with(tiles?.elementAt(6)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("Not set") } - with(tiles?.elementAt(6)!!) { + with(tiles?.elementAt(7)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription) .isEqualTo( @@ -456,31 +466,30 @@ class ModesDialogViewModelTest : SysuiTestCase() { val tiles by collectLastValue(underTest.tiles) val modeId = "id" - repository.addModes( - listOf( - TestModeBuilder() - .setId(modeId) - .setName("Test") - .setManualInvocationAllowed(true) - .build() - ) + repository.addMode( + TestModeBuilder() + .setId(modeId) + .setName("Test") + .setManualInvocationAllowed(true) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) - assertThat(tiles?.elementAt(0)?.enabled).isFalse() + // Manual DND is included by default + assertThat(tiles).hasSize(2) + assertThat(tiles?.elementAt(1)?.enabled).isFalse() // Trigger onClick - tiles?.first()?.onClick?.let { it() } + tiles?.elementAt(1)?.onClick?.let { it() } runCurrent() - assertThat(tiles?.first()?.enabled).isTrue() + assertThat(tiles?.elementAt(1)?.enabled).isTrue() // Trigger onClick - tiles?.first()?.onClick?.let { it() } + tiles?.elementAt(1)?.onClick?.let { it() } runCurrent() - assertThat(tiles?.first()?.enabled).isFalse() + assertThat(tiles?.elementAt(1)?.enabled).isFalse() } @Test @@ -489,25 +498,24 @@ class ModesDialogViewModelTest : SysuiTestCase() { val job = Job() val tiles by collectLastValue(underTest.tiles, context = job) - repository.addModes( - listOf( - TestModeBuilder() - .setName("Active without manual") - .setActive(true) - .setManualInvocationAllowed(false) - .build() - ) + repository.addMode( + TestModeBuilder() + .setName("Active without manual") + .setActive(true) + .setManualInvocationAllowed(false) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) + // Manual DND is included by default + assertThat(tiles).hasSize(2) // Click tile to toggle it off - tiles?.elementAt(0)!!.onClick() + tiles?.elementAt(1)!!.onClick() runCurrent() - assertThat(tiles).hasSize(1) - with(tiles?.elementAt(0)!!) { + assertThat(tiles).hasSize(2) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -518,7 +526,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { } // Check that nothing happened - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -530,19 +538,18 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) - repository.addModes( - listOf( - TestModeBuilder() - .setId("ID") - .setName("Disabled by other") - .setEnabled(false, /* byUser= */ false) - .build() - ) + repository.addMode( + TestModeBuilder() + .setId("ID") + .setName("Disabled by other") + .setEnabled(false, /* byUser= */ false) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) - with(tiles?.elementAt(0)!!) { + // Manual DND is included by default + assertThat(tiles).hasSize(2) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Disabled by other") assertThat(this.subtext).isEqualTo("Not set") assertThat(this.enabled).isEqualTo(false) @@ -561,7 +568,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { .isEqualTo("ID") // Check that nothing happened to the tile - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Disabled by other") assertThat(this.subtext).isEqualTo("Not set") assertThat(this.enabled).isEqualTo(false) @@ -593,10 +600,11 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(2) + // Manual DND is included by default + assertThat(tiles).hasSize(3) // Trigger onLongClick for A - tiles?.first()?.onLongClick?.let { it() } + tiles?.elementAt(1)?.onLongClick?.let { it() } runCurrent() // Check that it launched the correct intent @@ -625,9 +633,9 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) + repository.activateMode(MANUAL_DND) repository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder() .setId("id1") .setName("Inactive Mode One") @@ -644,6 +652,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() + // Manual DND is included by default assertThat(tiles).hasSize(3) // Trigger onClick for each tile in sequence @@ -672,19 +681,17 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) - repository.addModes( - listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, - TestModeBuilder() - .setId("id1") - .setName("Inactive Mode One") - .setActive(false) - .setManualInvocationAllowed(true) - .build(), - ) + repository.addMode( + TestModeBuilder() + .setId("id1") + .setName("Inactive Mode One") + .setActive(false) + .setManualInvocationAllowed(true) + .build() ) runCurrent() + // Manual DND is included by default assertThat(tiles).hasSize(2) val modeCaptor = argumentCaptor<ZenMode>() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt index 61c719319de8..824955de83e2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.policy.statusBarConfigurationController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any @@ -41,13 +42,18 @@ class StatusBarWindowControllerImplTest : SysuiTestCase() { private val kosmos = testKosmos().also { it.statusBarWindowViewInflater = it.fakeStatusBarWindowViewInflater } - private val underTest = kosmos.statusBarWindowControllerImpl + private lateinit var underTest: StatusBarWindowControllerImpl private val fakeExecutor = kosmos.fakeExecutor private val fakeWindowManager = kosmos.fakeWindowManager private val mockFragmentService = kosmos.fragmentService private val fakeStatusBarWindowViewInflater = kosmos.fakeStatusBarWindowViewInflater private val statusBarConfigurationController = kosmos.statusBarConfigurationController + @Before + fun setUp() { + underTest = kosmos.statusBarWindowControllerImpl + } + @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun attach_connectedDisplaysFlagEnabled_setsConfigControllerOnWindowView() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt deleted file mode 100644 index 2ad1124d72d4..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.touchpad.tutorial.ui - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState -import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error -import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished -import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress -import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError -import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted -import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState -import com.android.systemui.touchpad.tutorial.ui.composable.toTutorialActionState -import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class StateTransitionsTest : SysuiTestCase() { - - companion object { - private const val START_MARKER = "startMarker" - private const val END_MARKER = "endMarker" - private const val SUCCESS_ANIMATION = 0 - } - - // needed to simulate caching last state as it's used to create new state - private var lastState: TutorialActionState = NotStarted - - private fun GestureState.toTutorialActionState(): TutorialActionState { - val newState = - this.toGestureUiState( - progressStartMarker = START_MARKER, - progressEndMarker = END_MARKER, - successAnimation = SUCCESS_ANIMATION, - ) - .toTutorialActionState(lastState) - lastState = newState - return lastState - } - - @Test - fun gestureStateProducesEquivalentTutorialActionStateInHappyPath() { - val happyPath = - listOf( - GestureState.NotStarted, - GestureState.InProgress(0f), - GestureState.InProgress(0.5f), - GestureState.InProgress(1f), - GestureState.Finished, - ) - - val resultingStates = mutableListOf<TutorialActionState>() - happyPath.forEach { resultingStates.add(it.toTutorialActionState()) } - - assertThat(resultingStates) - .containsExactly( - NotStarted, - InProgress(0f, START_MARKER, END_MARKER), - InProgress(0.5f, START_MARKER, END_MARKER), - InProgress(1f, START_MARKER, END_MARKER), - Finished(SUCCESS_ANIMATION), - ) - .inOrder() - } - - @Test - fun gestureStateProducesEquivalentTutorialActionStateInErrorPath() { - val errorPath = - listOf( - GestureState.NotStarted, - GestureState.InProgress(0f), - GestureState.Error, - GestureState.InProgress(0.5f), - GestureState.InProgress(1f), - GestureState.Finished, - ) - - val resultingStates = mutableListOf<TutorialActionState>() - errorPath.forEach { resultingStates.add(it.toTutorialActionState()) } - - assertThat(resultingStates) - .containsExactly( - NotStarted, - InProgress(0f, START_MARKER, END_MARKER), - Error, - InProgressAfterError(InProgress(0.5f, START_MARKER, END_MARKER)), - InProgressAfterError(InProgress(1f, START_MARKER, END_MARKER)), - Finished(SUCCESS_ANIMATION), - ) - .inOrder() - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt index 4aec88e8497b..d752046f4791 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt @@ -23,16 +23,16 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.res.R import com.android.systemui.testKosmos -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Error -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.InProgress import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE import com.android.systemui.touchpad.tutorial.ui.gesture.ThreeFingerGesture import com.android.systemui.touchpad.ui.gesture.touchpadGestureResources @@ -71,8 +71,8 @@ class BackGestureScreenViewModelTest : SysuiTestCase() { expected = InProgress( progress = 1f, - progressStartMarker = "gesture to L", - progressEndMarker = "end progress L", + startMarker = "gesture to L", + endMarker = "end progress L", ), ) } @@ -85,8 +85,8 @@ class BackGestureScreenViewModelTest : SysuiTestCase() { expected = InProgress( progress = 1f, - progressStartMarker = "gesture to R", - progressEndMarker = "end progress R", + startMarker = "gesture to R", + endMarker = "end progress R", ), ) } @@ -114,7 +114,7 @@ class BackGestureScreenViewModelTest : SysuiTestCase() { kosmos.runTest { fun performBackGesture() = ThreeFingerGesture.swipeLeft().forEach { viewModel.handleEvent(it) } - val state by collectLastValue(viewModel.gestureUiState) + val state by collectLastValue(viewModel.tutorialState) performBackGesture() assertThat(state).isInstanceOf(Finished::class.java) @@ -134,15 +134,21 @@ class BackGestureScreenViewModelTest : SysuiTestCase() { fakeConfigRepository.onAnyConfigurationChange() } - private fun Kosmos.assertProgressWhileMovingFingers(deltaX: Float, expected: GestureUiState) { + private fun Kosmos.assertProgressWhileMovingFingers( + deltaX: Float, + expected: TutorialActionState, + ) { assertStateAfterEvents( events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaX = deltaX) }, expected = expected, ) } - private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: GestureUiState) { - val state by collectLastValue(viewModel.gestureUiState) + private fun Kosmos.assertStateAfterEvents( + events: List<MotionEvent>, + expected: TutorialActionState, + ) { + val state by collectLastValue(viewModel.tutorialState) events.forEach { viewModel.handleEvent(it) } assertThat(state).isEqualTo(expected) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt index 65a995dcd043..7862fd32ca04 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt @@ -23,16 +23,16 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.res.R import com.android.systemui.testKosmos -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Error -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.InProgress import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE import com.android.systemui.touchpad.tutorial.ui.gesture.ThreeFingerGesture import com.android.systemui.touchpad.tutorial.ui.gesture.Velocity @@ -86,8 +86,8 @@ class HomeGestureScreenViewModelTest : SysuiTestCase() { expected = InProgress( progress = 1f, - progressStartMarker = "drag with gesture", - progressEndMarker = "release playback realtime", + startMarker = "drag with gesture", + endMarker = "release playback realtime", ), ) } @@ -108,7 +108,7 @@ class HomeGestureScreenViewModelTest : SysuiTestCase() { @Test fun gestureRecognitionTakesLatestDistanceThresholdIntoAccount() = kosmos.runTest { - val state by collectLastValue(viewModel.gestureUiState) + val state by collectLastValue(viewModel.tutorialState) performHomeGesture() assertThat(state).isInstanceOf(Finished::class.java) @@ -121,7 +121,7 @@ class HomeGestureScreenViewModelTest : SysuiTestCase() { @Test fun gestureRecognitionTakesLatestVelocityThresholdIntoAccount() = kosmos.runTest { - val state by collectLastValue(viewModel.gestureUiState) + val state by collectLastValue(viewModel.tutorialState) performHomeGesture() assertThat(state).isInstanceOf(Finished::class.java) @@ -147,8 +147,11 @@ class HomeGestureScreenViewModelTest : SysuiTestCase() { fakeConfigRepository.onAnyConfigurationChange() } - private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: GestureUiState) { - val state by collectLastValue(viewModel.gestureUiState) + private fun Kosmos.assertStateAfterEvents( + events: List<MotionEvent>, + expected: TutorialActionState, + ) { + val state by collectLastValue(viewModel.tutorialState) events.forEach { viewModel.handleEvent(it) } assertThat(state).isEqualTo(expected) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt index 1bc60b67095e..6180fa98b1cd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt @@ -23,16 +23,16 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.res.R import com.android.systemui.testKosmos -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Error -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.InProgress import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE import com.android.systemui.touchpad.tutorial.ui.gesture.ThreeFingerGesture import com.android.systemui.touchpad.tutorial.ui.gesture.Velocity @@ -89,8 +89,8 @@ class RecentAppsGestureScreenViewModelTest : SysuiTestCase() { expected = InProgress( progress = 1f, - progressStartMarker = "drag with gesture", - progressEndMarker = "onPause", + startMarker = "drag with gesture", + endMarker = "onPause", ), ) } @@ -111,7 +111,7 @@ class RecentAppsGestureScreenViewModelTest : SysuiTestCase() { @Test fun gestureRecognitionTakesLatestDistanceThresholdIntoAccount() = kosmos.runTest { - val state by collectLastValue(viewModel.gestureUiState) + val state by collectLastValue(viewModel.tutorialState) performRecentAppsGesture() assertThat(state).isInstanceOf(Finished::class.java) @@ -124,7 +124,7 @@ class RecentAppsGestureScreenViewModelTest : SysuiTestCase() { @Test fun gestureRecognitionTakesLatestVelocityThresholdIntoAccount() = kosmos.runTest { - val state by collectLastValue(viewModel.gestureUiState) + val state by collectLastValue(viewModel.tutorialState) performRecentAppsGesture() assertThat(state).isInstanceOf(Finished::class.java) @@ -150,8 +150,11 @@ class RecentAppsGestureScreenViewModelTest : SysuiTestCase() { fakeConfigRepository.onAnyConfigurationChange() } - private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: GestureUiState) { - val state by collectLastValue(viewModel.gestureUiState) + private fun Kosmos.assertStateAfterEvents( + events: List<MotionEvent>, + expected: TutorialActionState, + ) { + val state by collectLastValue(viewModel.tutorialState) events.forEach { viewModel.handleEvent(it) } assertThat(state).isEqualTo(expected) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModelTest.kt new file mode 100644 index 000000000000..c113dd9e1eff --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModelTest.kt @@ -0,0 +1,117 @@ +/* + * 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.touchpad.tutorial.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted +import com.android.systemui.kosmos.collectValues +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class TouchpadTutorialScreenViewModelTest : SysuiTestCase() { + + companion object { + private const val START_MARKER = "startMarker" + private const val END_MARKER = "endMarker" + private const val SUCCESS_ANIMATION = 0 + } + + private val kosmos = testKosmos() + private val animationProperties = + TutorialAnimationProperties( + progressStartMarker = START_MARKER, + progressEndMarker = END_MARKER, + successAnimation = SUCCESS_ANIMATION, + ) + + @Before + fun before() { + kosmos.useUnconfinedTestDispatcher() + } + + @Test + fun gestureStateProducesEquivalentTutorialActionStateInHappyPath() = + kosmos.runTest { + val happyPath: Flow<Pair<GestureState, TutorialAnimationProperties>> = + listOf( + GestureState.NotStarted, + GestureState.InProgress(0f), + GestureState.InProgress(0.5f), + GestureState.InProgress(1f), + GestureState.Finished, + ) + .map { it to animationProperties } + .asFlow() + + val resultingStates by collectValues(happyPath.mapToTutorialState()) + + assertThat(resultingStates) + .containsExactly( + NotStarted, + InProgress(0f, START_MARKER, END_MARKER), + InProgress(0.5f, START_MARKER, END_MARKER), + InProgress(1f, START_MARKER, END_MARKER), + Finished(SUCCESS_ANIMATION), + ) + .inOrder() + } + + @Test + fun gestureStateProducesEquivalentTutorialActionStateInErrorPath() = + kosmos.runTest { + val errorPath: Flow<Pair<GestureState, TutorialAnimationProperties>> = + listOf( + GestureState.NotStarted, + GestureState.InProgress(0f), + GestureState.Error, + GestureState.InProgress(0.5f), + GestureState.InProgress(1f), + GestureState.Finished, + ) + .map { it to animationProperties } + .asFlow() + + val resultingStates by collectValues(errorPath.mapToTutorialState()) + + assertThat(resultingStates) + .containsExactly( + NotStarted, + InProgress(0f, START_MARKER, END_MARKER), + Error, + InProgressAfterError(InProgress(0.5f, START_MARKER, END_MARKER)), + InProgressAfterError(InProgress(1f, START_MARKER, END_MARKER)), + Finished(SUCCESS_ANIMATION), + ) + .inOrder() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt index d3071f87f744..51cac6976362 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt @@ -23,66 +23,40 @@ import android.platform.test.annotations.EnableFlags import android.service.notification.ZenPolicy import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.internal.logging.uiEventLogger import com.android.settingslib.notification.modes.TestModeBuilder import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository -import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.testKosmos -import com.android.systemui.volume.domain.interactor.audioVolumeInteractor -import com.android.systemui.volume.shared.volumePanelLogger import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class AudioStreamSliderViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() - private val testScope = kosmos.testScope private val zenModeRepository = kosmos.fakeZenModeRepository - private lateinit var mediaStream: AudioStreamSliderViewModel - private lateinit var alarmsStream: AudioStreamSliderViewModel - private lateinit var notificationStream: AudioStreamSliderViewModel - private lateinit var otherStream: AudioStreamSliderViewModel - - @Before - fun setUp() { - mediaStream = audioStreamSliderViewModel(AudioManager.STREAM_MUSIC) - alarmsStream = audioStreamSliderViewModel(AudioManager.STREAM_ALARM) - notificationStream = audioStreamSliderViewModel(AudioManager.STREAM_NOTIFICATION) - otherStream = audioStreamSliderViewModel(AudioManager.STREAM_VOICE_CALL) - } - - private fun audioStreamSliderViewModel(stream: Int): AudioStreamSliderViewModel { - return AudioStreamSliderViewModel( + private fun Kosmos.audioStreamSliderViewModel(stream: Int): AudioStreamSliderViewModel { + return audioStreamSliderViewModelFactory.create( AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)), - testScope.backgroundScope, - context, - kosmos.audioVolumeInteractor, - kosmos.zenModeInteractor, - kosmos.uiEventLogger, - kosmos.volumePanelLogger, - kosmos.sliderHapticsViewModelFactory, + applicationCoroutineScope, ) } @Test @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS) fun slider_media_hasDisabledByModesText() = - testScope.runTest { - val mediaSlider by collectLastValue(mediaStream.slider) + kosmos.runTest { + val mediaSlider by + collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_MUSIC).slider) zenModeRepository.addMode( TestModeBuilder() @@ -112,8 +86,9 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS) fun slider_alarms_hasDisabledByModesText() = - testScope.runTest { - val alarmsSlider by collectLastValue(alarmsStream.slider) + kosmos.runTest { + val alarmsSlider by + collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_ALARM).slider) zenModeRepository.addMode( TestModeBuilder() @@ -141,9 +116,10 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS) - fun slider_other_hasDisabledByModesText() = - testScope.runTest { - val otherSlider by collectLastValue(otherStream.slider) + fun slider_other_hasDisabledText() = + kosmos.runTest { + val otherSlider by + collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_VOICE_CALL).slider) zenModeRepository.addMode( TestModeBuilder() @@ -154,20 +130,17 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(otherSlider!!.disabledMessage) - .isEqualTo("Unavailable because Everything blocked is on") - - zenModeRepository.clearModes() - runCurrent() - assertThat(otherSlider!!.disabledMessage).isEqualTo("Unavailable") } @Test @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS) fun slider_notification_hasSpecialDisabledText() = - testScope.runTest { - val notificationSlider by collectLastValue(notificationStream.slider) + kosmos.runTest { + val notificationSlider by + collectLastValue( + audioStreamSliderViewModel(AudioManager.STREAM_NOTIFICATION).slider + ) runCurrent() assertThat(notificationSlider!!.disabledMessage) diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt index d84d89087349..812a964bf8bc 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt @@ -30,10 +30,6 @@ data class ClockConfig( /** Transition to AOD should move smartspace like large clock instead of small clock */ val useAlternateSmartspaceAODTransition: Boolean = false, - /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */ - @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone") - val isReactiveToTone: Boolean = true, - /** True if the clock is large frame clock, which will use weather in compose. */ val useCustomClockScene: Boolean = false, ) diff --git a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt index d93f7d3093b8..81156d9698d8 100644 --- a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt +++ b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt @@ -24,6 +24,7 @@ import javax.annotation.processing.RoundEnvironment import javax.lang.model.element.Element import javax.lang.model.element.ElementKind import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier import javax.lang.model.element.PackageElement import javax.lang.model.element.TypeElement import javax.lang.model.type.TypeKind @@ -183,11 +184,17 @@ class ProtectedPluginProcessor : AbstractProcessor() { // Method implementations for (method in methods) { val methodName = method.simpleName + if (methods.any { methodName.startsWith("${it.simpleName}\$") }) { + continue + } val returnTypeName = method.returnType.toString() val callArgs = StringBuilder() var isFirst = true + val isStatic = method.modifiers.contains(Modifier.STATIC) - line("@Override") + if (!isStatic) { + line("@Override") + } parenBlock("public $returnTypeName $methodName") { // While copying the method signature for the proxy type, we also // accumulate arguments for the nested callsite. @@ -202,7 +209,8 @@ class ProtectedPluginProcessor : AbstractProcessor() { } val isVoid = method.returnType.kind == TypeKind.VOID - val nestedCall = "mInstance.$methodName($callArgs)" + val methodContainer = if (isStatic) sourceName else "mInstance" + val nestedCall = "$methodContainer.$methodName($callArgs)" val callStatement = when { isVoid -> "$nestedCall;" diff --git a/packages/SystemUI/proguard_common.flags b/packages/SystemUI/proguard_common.flags index 162d8aebfc62..02b2bcf8e40d 100644 --- a/packages/SystemUI/proguard_common.flags +++ b/packages/SystemUI/proguard_common.flags @@ -1,5 +1,11 @@ -include proguard_kotlin.flags --keep class com.android.systemui.VendorServices + +# VendorServices implements CoreStartable and may be instantiated reflectively in +# SystemUIApplication#startAdditionalStartable. +# TODO(b/373579455): Rewrite this to a @UsesReflection keep annotation. +-keep class com.android.systemui.VendorServices { + public void <init>(); +} # Needed to ensure callback field references are kept in their respective # owning classes when the downstream callback registrars only store weak refs. diff --git a/packages/SystemUI/res/color/active_track_color.xml b/packages/SystemUI/res/color/active_track_color.xml new file mode 100644 index 000000000000..232555553d12 --- /dev/null +++ b/packages/SystemUI/res/color/active_track_color.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:color="@androidprv:color/materialColorPrimary" android:state_enabled="true" /> + <item android:alpha="0.38" android:color="@androidprv:color/materialColorOnSurface" /> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/color/inactive_track_color.xml b/packages/SystemUI/res/color/inactive_track_color.xml new file mode 100644 index 000000000000..2ba5ebd8818a --- /dev/null +++ b/packages/SystemUI/res/color/inactive_track_color.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:color="@androidprv:color/materialColorSurfaceContainerHighest" android:state_enabled="true" /> + <item android:alpha="0.12" android:color="@androidprv:color/materialColorOnSurface" /> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/color/on_active_track_color.xml b/packages/SystemUI/res/color/on_active_track_color.xml new file mode 100644 index 000000000000..7ca79a9e95af --- /dev/null +++ b/packages/SystemUI/res/color/on_active_track_color.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:color="@androidprv:color/materialColorOnPrimary" android:state_enabled="true" /> + <item android:color="@androidprv:color/materialColorOnSurfaceVariant" /> +</selector> diff --git a/packages/SystemUI/res/color/on_inactive_track_color.xml b/packages/SystemUI/res/color/on_inactive_track_color.xml new file mode 100644 index 000000000000..0eb4bfa106fb --- /dev/null +++ b/packages/SystemUI/res/color/on_inactive_track_color.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:color="@androidprv:color/materialColorPrimary" android:state_enabled="true" /> + <item android:color="@androidprv:color/materialColorOnSurfaceVariant" /> +</selector> diff --git a/packages/SystemUI/res/color/thumb_color.xml b/packages/SystemUI/res/color/thumb_color.xml new file mode 100644 index 000000000000..2b0e3a9a072b --- /dev/null +++ b/packages/SystemUI/res/color/thumb_color.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:color="@androidprv:color/materialColorPrimary" android:state_enabled="true" /> + <item android:alpha="0.38" android:color="@androidprv:color/materialColorOnSurface" /> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml new file mode 100644 index 000000000000..f8c0fa04cd39 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml @@ -0,0 +1,199 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="83" + android:propertyName="scaleX" + android:startOffset="1000" + android:valueFrom="0.45561" + android:valueTo="0.69699" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="83" + android:propertyName="scaleY" + android:startOffset="1000" + android:valueFrom="0.6288400000000001" + android:valueTo="0.81618" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleX" + android:startOffset="1083" + android:valueFrom="0.69699" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleY" + android:startOffset="1083" + android:valueFrom="0.81618" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="500" + android:propertyName="rotation" + android:startOffset="0" + android:valueFrom="90" + android:valueTo="135" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="500" + android:propertyName="rotation" + android:startOffset="500" + android:valueFrom="135" + android:valueTo="180" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="83" + android:propertyName="scaleX" + android:startOffset="1000" + android:valueFrom="0.0434" + android:valueTo="0.05063" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="83" + android:propertyName="scaleY" + android:startOffset="1000" + android:valueFrom="0.0434" + android:valueTo="0.042350000000000006" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleX" + android:startOffset="1083" + android:valueFrom="0.05063" + android:valueTo="0.06146" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleY" + android:startOffset="1083" + android:valueFrom="0.042350000000000006" + android:valueTo="0.040780000000000004" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="1017" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="88dp" + android:height="56dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972" + android:translateX="43.528999999999996" + android:translateY="27.898"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " /> + </group> + <group + android:name="_R_G_L_0_G" + android:rotation="0" + android:scaleX="0.06146" + android:scaleY="0.040780000000000004" + android:translateX="44" + android:translateY="28"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M-0.65 -437.37 C-0.65,-437.37 8.33,-437.66 8.33,-437.66 C8.33,-437.66 17.31,-437.95 17.31,-437.95 C17.31,-437.95 26.25,-438.78 26.25,-438.78 C26.25,-438.78 35.16,-439.95 35.16,-439.95 C35.16,-439.95 44.07,-441.11 44.07,-441.11 C44.07,-441.11 52.85,-443 52.85,-443 C52.85,-443 61.6,-445.03 61.6,-445.03 C61.6,-445.03 70.35,-447.09 70.35,-447.09 C70.35,-447.09 78.91,-449.83 78.91,-449.83 C78.91,-449.83 87.43,-452.67 87.43,-452.67 C87.43,-452.67 95.79,-455.97 95.79,-455.97 C95.79,-455.97 104.11,-459.35 104.11,-459.35 C104.11,-459.35 112.36,-462.93 112.36,-462.93 C112.36,-462.93 120.6,-466.51 120.6,-466.51 C120.6,-466.51 128.84,-470.09 128.84,-470.09 C128.84,-470.09 137.09,-473.67 137.09,-473.67 C137.09,-473.67 145.49,-476.84 145.49,-476.84 C145.49,-476.84 153.9,-480.01 153.9,-480.01 C153.9,-480.01 162.31,-483.18 162.31,-483.18 C162.31,-483.18 170.98,-485.54 170.98,-485.54 C170.98,-485.54 179.66,-487.85 179.66,-487.85 C179.66,-487.85 188.35,-490.15 188.35,-490.15 C188.35,-490.15 197.22,-491.58 197.22,-491.58 C197.22,-491.58 206.09,-493.01 206.09,-493.01 C206.09,-493.01 214.98,-494.28 214.98,-494.28 C214.98,-494.28 223.95,-494.81 223.95,-494.81 C223.95,-494.81 232.93,-495.33 232.93,-495.33 C232.93,-495.33 241.9,-495.5 241.9,-495.5 C241.9,-495.5 250.88,-495.13 250.88,-495.13 C250.88,-495.13 259.86,-494.75 259.86,-494.75 C259.86,-494.75 268.78,-493.78 268.78,-493.78 C268.78,-493.78 277.68,-492.52 277.68,-492.52 C277.68,-492.52 286.57,-491.26 286.57,-491.26 C286.57,-491.26 295.31,-489.16 295.31,-489.16 C295.31,-489.16 304.04,-487.04 304.04,-487.04 C304.04,-487.04 312.7,-484.65 312.7,-484.65 C312.7,-484.65 321.19,-481.72 321.19,-481.72 C321.19,-481.72 329.68,-478.78 329.68,-478.78 C329.68,-478.78 337.96,-475.31 337.96,-475.31 C337.96,-475.31 346.14,-471.59 346.14,-471.59 C346.14,-471.59 354.3,-467.82 354.3,-467.82 C354.3,-467.82 362.11,-463.38 362.11,-463.38 C362.11,-463.38 369.92,-458.93 369.92,-458.93 C369.92,-458.93 377.53,-454.17 377.53,-454.17 C377.53,-454.17 384.91,-449.04 384.91,-449.04 C384.91,-449.04 392.29,-443.91 392.29,-443.91 C392.29,-443.91 399.26,-438.24 399.26,-438.24 C399.26,-438.24 406.15,-432.48 406.15,-432.48 C406.15,-432.48 412.92,-426.57 412.92,-426.57 C412.92,-426.57 419.27,-420.22 419.27,-420.22 C419.27,-420.22 425.62,-413.87 425.62,-413.87 C425.62,-413.87 431.61,-407.18 431.61,-407.18 C431.61,-407.18 437.38,-400.29 437.38,-400.29 C437.38,-400.29 443.14,-393.39 443.14,-393.39 C443.14,-393.39 448.27,-386.01 448.27,-386.01 C448.27,-386.01 453.4,-378.64 453.4,-378.64 C453.4,-378.64 458.26,-371.09 458.26,-371.09 C458.26,-371.09 462.71,-363.28 462.71,-363.28 C462.71,-363.28 467.16,-355.47 467.16,-355.47 C467.16,-355.47 471.03,-347.37 471.03,-347.37 C471.03,-347.37 474.75,-339.19 474.75,-339.19 C474.75,-339.19 478.34,-330.95 478.34,-330.95 C478.34,-330.95 481.28,-322.46 481.28,-322.46 C481.28,-322.46 484.21,-313.97 484.21,-313.97 C484.21,-313.97 486.72,-305.35 486.72,-305.35 C486.72,-305.35 488.84,-296.62 488.84,-296.62 C488.84,-296.62 490.96,-287.88 490.96,-287.88 C490.96,-287.88 492.33,-279.01 492.33,-279.01 C492.33,-279.01 493.59,-270.11 493.59,-270.11 C493.59,-270.11 494.69,-261.2 494.69,-261.2 C494.69,-261.2 495.07,-252.22 495.07,-252.22 C495.07,-252.22 495.44,-243.24 495.44,-243.24 C495.44,-243.24 495.41,-234.27 495.41,-234.27 C495.41,-234.27 494.88,-225.29 494.88,-225.29 C494.88,-225.29 494.35,-216.32 494.35,-216.32 C494.35,-216.32 493.22,-207.42 493.22,-207.42 C493.22,-207.42 491.79,-198.55 491.79,-198.55 C491.79,-198.55 490.36,-189.68 490.36,-189.68 C490.36,-189.68 488.19,-180.96 488.19,-180.96 C488.19,-180.96 485.88,-172.28 485.88,-172.28 C485.88,-172.28 483.56,-163.6 483.56,-163.6 C483.56,-163.6 480.48,-155.16 480.48,-155.16 C480.48,-155.16 477.31,-146.75 477.31,-146.75 C477.31,-146.75 474.14,-138.34 474.14,-138.34 C474.14,-138.34 470.62,-130.07 470.62,-130.07 C470.62,-130.07 467.04,-121.83 467.04,-121.83 C467.04,-121.83 463.46,-113.59 463.46,-113.59 C463.46,-113.59 459.88,-105.35 459.88,-105.35 C459.88,-105.35 456.54,-97.01 456.54,-97.01 C456.54,-97.01 453.37,-88.6 453.37,-88.6 C453.37,-88.6 450.21,-80.19 450.21,-80.19 C450.21,-80.19 447.68,-71.57 447.68,-71.57 C447.68,-71.57 445.36,-62.89 445.36,-62.89 C445.36,-62.89 443.04,-54.21 443.04,-54.21 C443.04,-54.21 441.54,-45.35 441.54,-45.35 C441.54,-45.35 440.09,-36.48 440.09,-36.48 C440.09,-36.48 438.78,-27.6 438.78,-27.6 C438.78,-27.6 438.19,-18.63 438.19,-18.63 C438.19,-18.63 437.61,-9.66 437.61,-9.66 C437.61,-9.66 437.36,-0.69 437.36,-0.69 C437.36,-0.69 437.65,8.29 437.65,8.29 C437.65,8.29 437.95,17.27 437.95,17.27 C437.95,17.27 438.77,26.21 438.77,26.21 C438.77,26.21 439.94,35.12 439.94,35.12 C439.94,35.12 441.11,44.03 441.11,44.03 C441.11,44.03 442.99,52.81 442.99,52.81 C442.99,52.81 445.02,61.57 445.02,61.57 C445.02,61.57 447.07,70.31 447.07,70.31 C447.07,70.31 449.82,78.87 449.82,78.87 C449.82,78.87 452.65,87.4 452.65,87.4 C452.65,87.4 455.96,95.75 455.96,95.75 C455.96,95.75 459.33,104.08 459.33,104.08 C459.33,104.08 462.91,112.32 462.91,112.32 C462.91,112.32 466.49,120.57 466.49,120.57 C466.49,120.57 470.07,128.81 470.07,128.81 C470.07,128.81 473.65,137.05 473.65,137.05 C473.65,137.05 476.82,145.46 476.82,145.46 C476.82,145.46 479.99,153.87 479.99,153.87 C479.99,153.87 483.17,162.28 483.17,162.28 C483.17,162.28 485.52,170.94 485.52,170.94 C485.52,170.94 487.84,179.63 487.84,179.63 C487.84,179.63 490.14,188.31 490.14,188.31 C490.14,188.31 491.57,197.18 491.57,197.18 C491.57,197.18 493,206.06 493,206.06 C493,206.06 494.27,214.95 494.27,214.95 C494.27,214.95 494.8,223.92 494.8,223.92 C494.8,223.92 495.33,232.89 495.33,232.89 C495.33,232.89 495.5,241.86 495.5,241.86 C495.5,241.86 495.12,250.84 495.12,250.84 C495.12,250.84 494.75,259.82 494.75,259.82 C494.75,259.82 493.78,268.74 493.78,268.74 C493.78,268.74 492.52,277.64 492.52,277.64 C492.52,277.64 491.27,286.54 491.27,286.54 C491.27,286.54 489.16,295.27 489.16,295.27 C489.16,295.27 487.05,304.01 487.05,304.01 C487.05,304.01 484.66,312.66 484.66,312.66 C484.66,312.66 481.73,321.16 481.73,321.16 C481.73,321.16 478.79,329.65 478.79,329.65 C478.79,329.65 475.32,337.93 475.32,337.93 C475.32,337.93 471.6,346.11 471.6,346.11 C471.6,346.11 467.84,354.27 467.84,354.27 C467.84,354.27 463.39,362.08 463.39,362.08 C463.39,362.08 458.94,369.89 458.94,369.89 C458.94,369.89 454.19,377.5 454.19,377.5 C454.19,377.5 449.06,384.88 449.06,384.88 C449.06,384.88 443.93,392.26 443.93,392.26 C443.93,392.26 438.26,399.23 438.26,399.23 C438.26,399.23 432.5,406.12 432.5,406.12 C432.5,406.12 426.6,412.89 426.6,412.89 C426.6,412.89 420.24,419.24 420.24,419.24 C420.24,419.24 413.89,425.6 413.89,425.6 C413.89,425.6 407.2,431.59 407.2,431.59 C407.2,431.59 400.31,437.36 400.31,437.36 C400.31,437.36 393.42,443.12 393.42,443.12 C393.42,443.12 386.04,448.25 386.04,448.25 C386.04,448.25 378.66,453.38 378.66,453.38 C378.66,453.38 371.11,458.24 371.11,458.24 C371.11,458.24 363.31,462.69 363.31,462.69 C363.31,462.69 355.5,467.14 355.5,467.14 C355.5,467.14 347.4,471.02 347.4,471.02 C347.4,471.02 339.22,474.73 339.22,474.73 C339.22,474.73 330.99,478.33 330.99,478.33 C330.99,478.33 322.49,481.27 322.49,481.27 C322.49,481.27 314,484.2 314,484.2 C314,484.2 305.38,486.71 305.38,486.71 C305.38,486.71 296.65,488.83 296.65,488.83 C296.65,488.83 287.91,490.95 287.91,490.95 C287.91,490.95 279.04,492.33 279.04,492.33 C279.04,492.33 270.14,493.59 270.14,493.59 C270.14,493.59 261.23,494.69 261.23,494.69 C261.23,494.69 252.25,495.07 252.25,495.07 C252.25,495.07 243.28,495.44 243.28,495.44 C243.28,495.44 234.3,495.41 234.3,495.41 C234.3,495.41 225.33,494.88 225.33,494.88 C225.33,494.88 216.36,494.35 216.36,494.35 C216.36,494.35 207.45,493.23 207.45,493.23 C207.45,493.23 198.58,491.8 198.58,491.8 C198.58,491.8 189.71,490.37 189.71,490.37 C189.71,490.37 180.99,488.21 180.99,488.21 C180.99,488.21 172.31,485.89 172.31,485.89 C172.31,485.89 163.63,483.57 163.63,483.57 C163.63,483.57 155.19,480.5 155.19,480.5 C155.19,480.5 146.78,477.32 146.78,477.32 C146.78,477.32 138.37,474.15 138.37,474.15 C138.37,474.15 130.11,470.63 130.11,470.63 C130.11,470.63 121.86,467.06 121.86,467.06 C121.86,467.06 113.62,463.48 113.62,463.48 C113.62,463.48 105.38,459.9 105.38,459.9 C105.38,459.9 97.04,456.56 97.04,456.56 C97.04,456.56 88.63,453.39 88.63,453.39 C88.63,453.39 80.22,450.22 80.22,450.22 C80.22,450.22 71.6,447.7 71.6,447.7 C71.6,447.7 62.92,445.37 62.92,445.37 C62.92,445.37 54.24,443.05 54.24,443.05 C54.24,443.05 45.38,441.55 45.38,441.55 C45.38,441.55 36.52,440.1 36.52,440.1 C36.52,440.1 27.63,438.78 27.63,438.78 C27.63,438.78 18.66,438.2 18.66,438.2 C18.66,438.2 9.7,437.61 9.7,437.61 C9.7,437.61 0.72,437.36 0.72,437.36 C0.72,437.36 -8.26,437.65 -8.26,437.65 C-8.26,437.65 -17.24,437.95 -17.24,437.95 C-17.24,437.95 -26.18,438.77 -26.18,438.77 C-26.18,438.77 -35.09,439.94 -35.09,439.94 C-35.09,439.94 -44,441.1 -44,441.1 C-44,441.1 -52.78,442.98 -52.78,442.98 C-52.78,442.98 -61.53,445.02 -61.53,445.02 C-61.53,445.02 -70.28,447.07 -70.28,447.07 C-70.28,447.07 -78.84,449.81 -78.84,449.81 C-78.84,449.81 -87.37,452.64 -87.37,452.64 C-87.37,452.64 -95.72,455.95 -95.72,455.95 C-95.72,455.95 -104.05,459.32 -104.05,459.32 C-104.05,459.32 -112.29,462.9 -112.29,462.9 C-112.29,462.9 -120.53,466.48 -120.53,466.48 C-120.53,466.48 -128.78,470.06 -128.78,470.06 C-128.78,470.06 -137.02,473.63 -137.02,473.63 C-137.02,473.63 -145.43,476.81 -145.43,476.81 C-145.43,476.81 -153.84,479.98 -153.84,479.98 C-153.84,479.98 -162.24,483.15 -162.24,483.15 C-162.24,483.15 -170.91,485.52 -170.91,485.52 C-170.91,485.52 -179.59,487.83 -179.59,487.83 C-179.59,487.83 -188.28,490.13 -188.28,490.13 C-188.28,490.13 -197.15,491.56 -197.15,491.56 C-197.15,491.56 -206.02,492.99 -206.02,492.99 C-206.02,492.99 -214.91,494.27 -214.91,494.27 C-214.91,494.27 -223.88,494.8 -223.88,494.8 C-223.88,494.8 -232.85,495.33 -232.85,495.33 C-232.85,495.33 -241.83,495.5 -241.83,495.5 C-241.83,495.5 -250.81,495.13 -250.81,495.13 C-250.81,495.13 -259.79,494.75 -259.79,494.75 C-259.79,494.75 -268.71,493.79 -268.71,493.79 C-268.71,493.79 -277.61,492.53 -277.61,492.53 C-277.61,492.53 -286.51,491.27 -286.51,491.27 C-286.51,491.27 -295.24,489.17 -295.24,489.17 C-295.24,489.17 -303.98,487.06 -303.98,487.06 C-303.98,487.06 -312.63,484.67 -312.63,484.67 C-312.63,484.67 -321.12,481.74 -321.12,481.74 C-321.12,481.74 -329.62,478.8 -329.62,478.8 C-329.62,478.8 -337.9,475.33 -337.9,475.33 C-337.9,475.33 -346.08,471.62 -346.08,471.62 C-346.08,471.62 -354.24,467.85 -354.24,467.85 C-354.24,467.85 -362.05,463.41 -362.05,463.41 C-362.05,463.41 -369.86,458.96 -369.86,458.96 C-369.86,458.96 -377.47,454.21 -377.47,454.21 C-377.47,454.21 -384.85,449.08 -384.85,449.08 C-384.85,449.08 -392.23,443.95 -392.23,443.95 C-392.23,443.95 -399.2,438.29 -399.2,438.29 C-399.2,438.29 -406.09,432.52 -406.09,432.52 C-406.09,432.52 -412.86,426.62 -412.86,426.62 C-412.86,426.62 -419.22,420.27 -419.22,420.27 C-419.22,420.27 -425.57,413.91 -425.57,413.91 C-425.57,413.91 -431.57,407.23 -431.57,407.23 C-431.57,407.23 -437.33,400.34 -437.33,400.34 C-437.33,400.34 -443.1,393.44 -443.1,393.44 C-443.1,393.44 -448.23,386.07 -448.23,386.07 C-448.23,386.07 -453.36,378.69 -453.36,378.69 C-453.36,378.69 -458.23,371.15 -458.23,371.15 C-458.23,371.15 -462.67,363.33 -462.67,363.33 C-462.67,363.33 -467.12,355.53 -467.12,355.53 C-467.12,355.53 -471,347.43 -471,347.43 C-471,347.43 -474.72,339.25 -474.72,339.25 C-474.72,339.25 -478.32,331.02 -478.32,331.02 C-478.32,331.02 -481.25,322.52 -481.25,322.52 C-481.25,322.52 -484.19,314.03 -484.19,314.03 C-484.19,314.03 -486.71,305.42 -486.71,305.42 C-486.71,305.42 -488.82,296.68 -488.82,296.68 C-488.82,296.68 -490.94,287.95 -490.94,287.95 C-490.94,287.95 -492.32,279.07 -492.32,279.07 C-492.32,279.07 -493.58,270.18 -493.58,270.18 C-493.58,270.18 -494.69,261.27 -494.69,261.27 C-494.69,261.27 -495.07,252.29 -495.07,252.29 C-495.07,252.29 -495.44,243.31 -495.44,243.31 C-495.44,243.31 -495.42,234.33 -495.42,234.33 C-495.42,234.33 -494.89,225.36 -494.89,225.36 C-494.89,225.36 -494.36,216.39 -494.36,216.39 C-494.36,216.39 -493.23,207.49 -493.23,207.49 C-493.23,207.49 -491.8,198.61 -491.8,198.61 C-491.8,198.61 -490.37,189.74 -490.37,189.74 C-490.37,189.74 -488.22,181.02 -488.22,181.02 C-488.22,181.02 -485.9,172.34 -485.9,172.34 C-485.9,172.34 -483.58,163.66 -483.58,163.66 C-483.58,163.66 -480.51,155.22 -480.51,155.22 C-480.51,155.22 -477.34,146.81 -477.34,146.81 C-477.34,146.81 -474.17,138.41 -474.17,138.41 C-474.17,138.41 -470.65,130.14 -470.65,130.14 C-470.65,130.14 -467.07,121.9 -467.07,121.9 C-467.07,121.9 -463.49,113.65 -463.49,113.65 C-463.49,113.65 -459.91,105.41 -459.91,105.41 C-459.91,105.41 -456.57,97.07 -456.57,97.07 C-456.57,97.07 -453.4,88.66 -453.4,88.66 C-453.4,88.66 -450.23,80.25 -450.23,80.25 C-450.23,80.25 -447.7,71.64 -447.7,71.64 C-447.7,71.64 -445.38,62.96 -445.38,62.96 C-445.38,62.96 -443.06,54.28 -443.06,54.28 C-443.06,54.28 -441.56,45.42 -441.56,45.42 C-441.56,45.42 -440.1,36.55 -440.1,36.55 C-440.1,36.55 -438.78,27.67 -438.78,27.67 C-438.78,27.67 -438.2,18.7 -438.2,18.7 C-438.2,18.7 -437.62,9.73 -437.62,9.73 C-437.62,9.73 -437.36,0.76 -437.36,0.76 C-437.36,0.76 -437.66,-8.22 -437.66,-8.22 C-437.66,-8.22 -437.95,-17.2 -437.95,-17.2 C-437.95,-17.2 -438.77,-26.14 -438.77,-26.14 C-438.77,-26.14 -439.93,-35.05 -439.93,-35.05 C-439.93,-35.05 -441.1,-43.96 -441.1,-43.96 C-441.1,-43.96 -442.98,-52.75 -442.98,-52.75 C-442.98,-52.75 -445.01,-61.5 -445.01,-61.5 C-445.01,-61.5 -447.06,-70.25 -447.06,-70.25 C-447.06,-70.25 -449.8,-78.81 -449.8,-78.81 C-449.8,-78.81 -452.63,-87.33 -452.63,-87.33 C-452.63,-87.33 -455.94,-95.69 -455.94,-95.69 C-455.94,-95.69 -459.31,-104.02 -459.31,-104.02 C-459.31,-104.02 -462.89,-112.26 -462.89,-112.26 C-462.89,-112.26 -466.47,-120.5 -466.47,-120.5 C-466.47,-120.5 -470.05,-128.74 -470.05,-128.74 C-470.05,-128.74 -473.68,-137.12 -473.68,-137.12 C-473.68,-137.12 -476.85,-145.53 -476.85,-145.53 C-476.85,-145.53 -480.03,-153.94 -480.03,-153.94 C-480.03,-153.94 -483.2,-162.34 -483.2,-162.34 C-483.2,-162.34 -485.55,-171.02 -485.55,-171.02 C-485.55,-171.02 -487.86,-179.7 -487.86,-179.7 C-487.86,-179.7 -490.15,-188.39 -490.15,-188.39 C-490.15,-188.39 -491.58,-197.26 -491.58,-197.26 C-491.58,-197.26 -493.01,-206.13 -493.01,-206.13 C-493.01,-206.13 -494.28,-215.02 -494.28,-215.02 C-494.28,-215.02 -494.81,-223.99 -494.81,-223.99 C-494.81,-223.99 -495.33,-232.96 -495.33,-232.96 C-495.33,-232.96 -495.5,-241.94 -495.5,-241.94 C-495.5,-241.94 -495.12,-250.92 -495.12,-250.92 C-495.12,-250.92 -494.75,-259.9 -494.75,-259.9 C-494.75,-259.9 -493.78,-268.82 -493.78,-268.82 C-493.78,-268.82 -492.52,-277.72 -492.52,-277.72 C-492.52,-277.72 -491.26,-286.61 -491.26,-286.61 C-491.26,-286.61 -489.15,-295.35 -489.15,-295.35 C-489.15,-295.35 -487.03,-304.08 -487.03,-304.08 C-487.03,-304.08 -484.64,-312.73 -484.64,-312.73 C-484.64,-312.73 -481.7,-321.23 -481.7,-321.23 C-481.7,-321.23 -478.77,-329.72 -478.77,-329.72 C-478.77,-329.72 -475.29,-338 -475.29,-338 C-475.29,-338 -471.57,-346.18 -471.57,-346.18 C-471.57,-346.18 -467.8,-354.33 -467.8,-354.33 C-467.8,-354.33 -463.36,-362.14 -463.36,-362.14 C-463.36,-362.14 -458.91,-369.95 -458.91,-369.95 C-458.91,-369.95 -454.15,-377.56 -454.15,-377.56 C-454.15,-377.56 -449.02,-384.94 -449.02,-384.94 C-449.02,-384.94 -443.88,-392.32 -443.88,-392.32 C-443.88,-392.32 -438.22,-399.28 -438.22,-399.28 C-438.22,-399.28 -432.45,-406.18 -432.45,-406.18 C-432.45,-406.18 -426.55,-412.94 -426.55,-412.94 C-426.55,-412.94 -420.19,-419.3 -420.19,-419.3 C-420.19,-419.3 -413.84,-425.65 -413.84,-425.65 C-413.84,-425.65 -407.15,-431.64 -407.15,-431.64 C-407.15,-431.64 -400.26,-437.41 -400.26,-437.41 C-400.26,-437.41 -393.36,-443.16 -393.36,-443.16 C-393.36,-443.16 -385.98,-448.29 -385.98,-448.29 C-385.98,-448.29 -378.6,-453.43 -378.6,-453.43 C-378.6,-453.43 -371.05,-458.28 -371.05,-458.28 C-371.05,-458.28 -363.24,-462.73 -363.24,-462.73 C-363.24,-462.73 -355.43,-467.18 -355.43,-467.18 C-355.43,-467.18 -347.33,-471.05 -347.33,-471.05 C-347.33,-471.05 -339.15,-474.76 -339.15,-474.76 C-339.15,-474.76 -330.92,-478.35 -330.92,-478.35 C-330.92,-478.35 -322.42,-481.29 -322.42,-481.29 C-322.42,-481.29 -313.93,-484.23 -313.93,-484.23 C-313.93,-484.23 -305.31,-486.73 -305.31,-486.73 C-305.31,-486.73 -296.58,-488.85 -296.58,-488.85 C-296.58,-488.85 -287.85,-490.97 -287.85,-490.97 C-287.85,-490.97 -278.97,-492.34 -278.97,-492.34 C-278.97,-492.34 -270.07,-493.6 -270.07,-493.6 C-270.07,-493.6 -261.16,-494.7 -261.16,-494.7 C-261.16,-494.7 -252.18,-495.07 -252.18,-495.07 C-252.18,-495.07 -243.2,-495.44 -243.2,-495.44 C-243.2,-495.44 -234.23,-495.41 -234.23,-495.41 C-234.23,-495.41 -225.26,-494.88 -225.26,-494.88 C-225.26,-494.88 -216.29,-494.35 -216.29,-494.35 C-216.29,-494.35 -207.38,-493.22 -207.38,-493.22 C-207.38,-493.22 -198.51,-491.79 -198.51,-491.79 C-198.51,-491.79 -189.64,-490.36 -189.64,-490.36 C-189.64,-490.36 -180.92,-488.19 -180.92,-488.19 C-180.92,-488.19 -172.24,-485.87 -172.24,-485.87 C-172.24,-485.87 -163.56,-483.56 -163.56,-483.56 C-163.56,-483.56 -155.12,-480.47 -155.12,-480.47 C-155.12,-480.47 -146.72,-477.3 -146.72,-477.3 C-146.72,-477.3 -138.31,-474.13 -138.31,-474.13 C-138.31,-474.13 -130.04,-470.61 -130.04,-470.61 C-130.04,-470.61 -121.8,-467.03 -121.8,-467.03 C-121.8,-467.03 -113.55,-463.45 -113.55,-463.45 C-113.55,-463.45 -105.31,-459.87 -105.31,-459.87 C-105.31,-459.87 -96.97,-456.53 -96.97,-456.53 C-96.97,-456.53 -88.56,-453.37 -88.56,-453.37 C-88.56,-453.37 -80.15,-450.2 -80.15,-450.2 C-80.15,-450.2 -71.53,-447.68 -71.53,-447.68 C-71.53,-447.68 -62.85,-445.36 -62.85,-445.36 C-62.85,-445.36 -54.17,-443.04 -54.17,-443.04 C-54.17,-443.04 -45.31,-441.54 -45.31,-441.54 C-45.31,-441.54 -36.44,-440.09 -36.44,-440.09 C-36.44,-440.09 -27.56,-438.78 -27.56,-438.78 C-27.56,-438.78 -18.59,-438.19 -18.59,-438.19 C-18.59,-438.19 -9.62,-437.61 -9.62,-437.61 C-9.62,-437.61 -0.65,-437.37 -0.65,-437.37c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button.xml b/packages/SystemUI/res/drawable/ic_media_pause_button.xml new file mode 100644 index 000000000000..6ae89f91c5ee --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_pause_button.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="56" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="15.485" + android:valueTo="12.321" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="278" + android:propertyName="translateX" + android:startOffset="56" + android:valueFrom="12.321" + android:valueTo="7.576" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="-12.031" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="19.524" + android:translateY="12.084"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " /> + </group> + <group + android:name="_R_G_L_0_G_T_1" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="15.485" + android:translateY="12.084"> + <group + android:name="_R_G_L_0_G" + android:translateX="12.031"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " /> + </group> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml new file mode 100644 index 000000000000..571f69d51ac4 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:width="88dp" + android:height="56dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972" + android:translateX="43.528999999999996" + android:translateY="27.898"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="133" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="367" + android:propertyName="pathData" + android:startOffset="133" + android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="167" + android:propertyName="scaleX" + android:startOffset="0" + android:valueFrom="1.05905" + android:valueTo="1.17758" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="167" + android:propertyName="scaleY" + android:startOffset="0" + android:valueFrom="1.0972" + android:valueTo="1.22" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="333" + android:propertyName="scaleX" + android:startOffset="167" + android:valueFrom="1.17758" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="333" + android:propertyName="scaleY" + android:startOffset="167" + android:valueFrom="1.22" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_play_button.xml b/packages/SystemUI/res/drawable/ic_media_play_button.xml new file mode 100644 index 000000000000..f64690268cfe --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_play_button.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="7.576" + android:valueTo="15.485" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.583,0 0.089,0.874 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="-12.031" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="19.524" + android:translateY="12.084"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " /> + </group> + <group + android:name="_R_G_L_0_G_T_1" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="7.576" + android:translateY="12.084"> + <group + android:name="_R_G_L_0_G" + android:translateX="12.031"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " /> + </group> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_play_button_container.xml b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml new file mode 100644 index 000000000000..aa4e09fa4033 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:height="56dp" + android:width="88dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:translateX="43.528999999999996" + android:translateY="27.898" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillColor="#3d90ff" + android:fillAlpha="1" + android:fillType="nonZero" + android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "/> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="pathData" + android:duration="167" + android:startOffset="0" + android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="pathData" + android:duration="333" + android:startOffset="167" + android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="scaleX" + android:duration="167" + android:startOffset="0" + android:valueFrom="1.05905" + android:valueTo="1.17758" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleY" + android:duration="167" + android:startOffset="0" + android:valueFrom="1.0972" + android:valueTo="1.22" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleX" + android:duration="333" + android:startOffset="167" + android:valueFrom="1.17758" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleY" + android:duration="333" + android:startOffset="167" + android:valueFrom="1.22" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="translateX" + android:duration="517" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index 0b624e1687a6..58f2d3ccc6a8 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -44,7 +44,7 @@ app:layout_constraintBottom_toTopOf="@id/volume_dialog_main_slider_container" app:layout_constraintEnd_toEndOf="@id/volume_dialog_main_slider_container" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"/> + app:layout_constraintTop_toTopOf="parent" /> <include android:id="@+id/volume_dialog_main_slider_container" diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml index 967cb3fd68de..6eb7b730e105 100644 --- a/packages/SystemUI/res/layout/volume_dialog_slider.xml +++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml @@ -14,8 +14,9 @@ limitations under the License. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="@dimen/volume_dialog_slider_width" - android:layout_height="@dimen/volume_dialog_slider_height"> + android:layout_width="0dp" + android:layout_height="0dp" + android:maxHeight="@dimen/volume_dialog_slider_height"> <com.google.android.material.slider.Slider android:id="@+id/volume_dialog_slider" diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml index e65d0b938b65..6748cfa05c35 100644 --- a/packages/SystemUI/res/layout/volume_ringer_button.xml +++ b/packages/SystemUI/res/layout/volume_ringer_button.xml @@ -20,10 +20,9 @@ <ImageButton android:id="@+id/volume_drawer_button" - android:layout_width="@dimen/volume_dialog_ringer_drawer_button_size" - android:layout_height="@dimen/volume_dialog_ringer_drawer_button_size" + android:layout_width="match_parent" + android:layout_height="match_parent" android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius" - android:layout_marginBottom="@dimen/volume_dialog_components_spacing" android:contentDescription="@string/volume_ringer_mode" android:gravity="center" android:tint="@androidprv:color/materialColorOnSurface" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 8bf4e373a6e0..2ffa3d19e161 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1278,6 +1278,7 @@ <dimen name="qs_center_guideline_padding">10dp</dimen> <dimen name="qs_media_action_spacing">4dp</dimen> <dimen name="qs_media_action_margin">12dp</dimen> + <dimen name="qs_media_action_play_pause_width">72dp</dimen> <dimen name="qs_seamless_height">24dp</dimen> <dimen name="qs_seamless_icon_size">12dp</dimen> <dimen name="qs_media_disabled_seekbar_height">1dp</dimen> @@ -2116,6 +2117,11 @@ <dimen name="volume_dialog_button_size">40dp</dimen> <dimen name="volume_dialog_slider_width">52dp</dimen> <dimen name="volume_dialog_slider_height">254dp</dimen> + <!-- + A primary goal of this margin is to vertically constraint slider height in the landscape + orientation when the vertical space is limited + --> + <dimen name="volume_dialog_slider_vertical_margin">124dp</dimen> <fraction name="volume_dialog_half_opened_bias">0.2</fraction> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index cd37c22c8bc3..80fb8b9fcebc 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2309,6 +2309,9 @@ <string name="group_system_lock_screen">Lock screen</string> <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] --> <string name="group_system_quick_memo">Take a note</string> + <!-- TODO(b/383734125): make it translatable once string is finalized by UXW.--> + <!-- User visible title for the keyboard shortcut that toggles Voice Access. [CHAR LIMIT=70] --> + <string name="group_system_toggle_voice_access" translatable="false">Toggle Voice Access</string> <!-- User visible title for the multitasking keyboard shortcuts list. [CHAR LIMIT=70] --> <string name="keyboard_shortcut_group_system_multitasking">Multitasking</string> @@ -3798,7 +3801,7 @@ <!-- Title at the top of the keyboard shortcut helper UI when in customize mode. The helper is a component that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] --> - <string name="shortcut_helper_customize_mode_title">Customize keyboard shortcuts</string> + <string name="shortcut_helper_customize_mode_title">Customize shortcuts</string> <!-- Title at the top of the keyboard shortcut helper remove shortcut dialog. The helper is a component that shows the user which keyboard shortcuts they can use. Also allows the user to add/remove custom shortcuts.[CHAR LIMIT=NONE] --> @@ -3919,6 +3922,16 @@ The helper is a component that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] --> <string name="shortcut_helper_plus_symbol">+</string> + <!-- Accessibility label for the plus icon on a shortcut in shortcut helper that allows the user + to add a new custom shortcut. + The helper is a component that shows the user which keyboard shortcuts they can use. + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_add_shortcut_button_label">Add shortcut</string> + <!-- Accessibility label for the bin(trash) icon on a shortcut in shortcut helper that allows the + user to delete an existing custom shortcut. + The helper is a component that shows the user which keyboard shortcuts they can use. + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_delete_shortcut_button_label">Delete shortcut</string> <!-- Keyboard touchpad tutorial scheduler--> <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 691fb50a15b8..08891aa65417 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -576,12 +576,12 @@ <style name="SystemUI.Material3.Slider" parent="@style/Widget.Material3.Slider"> <item name="labelStyle">@style/Widget.Material3.Slider.Label</item> - <item name="thumbColor">@androidprv:color/materialColorPrimary</item> - <item name="tickColorActive">@androidprv:color/materialColorSurfaceContainerHighest</item> - <item name="tickColorInactive">@androidprv:color/materialColorPrimary</item> - <item name="trackColorActive">@androidprv:color/materialColorPrimary</item> - <item name="trackColorInactive">@androidprv:color/materialColorSurfaceContainerHighest</item> - <item name="trackIconActiveColor">@androidprv:color/materialColorSurfaceContainerHighest</item> + <item name="thumbColor">@color/thumb_color</item> + <item name="tickColorActive">@color/on_active_track_color</item> + <item name="tickColorInactive">@color/on_inactive_track_color</item> + <item name="trackColorActive">@color/active_track_color</item> + <item name="trackColorInactive">@color/inactive_track_color</item> + <item name="trackIconActiveColor">@color/on_active_track_color</item> </style> <style name="Theme.SystemUI.DayNightDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog"/> diff --git a/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml b/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml index 9018e5b7ed92..a8f616c2427d 100644 --- a/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml +++ b/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml @@ -6,10 +6,13 @@ <Constraint android:id="@id/volume_dialog_main_slider_container" android:layout_width="@dimen/volume_dialog_slider_width" - android:layout_height="@dimen/volume_dialog_slider_height" + android:layout_height="0dp" + android:layout_marginTop="@dimen/volume_dialog_slider_vertical_margin" android:layout_marginEnd="@dimen/volume_dialog_components_spacing" + android:layout_marginBottom="@dimen/volume_dialog_slider_vertical_margin" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_max="@dimen/volume_dialog_slider_height" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.5" /> </ConstraintSet>
\ No newline at end of file diff --git a/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml b/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml index 297c38873164..b4d8ae791f36 100644 --- a/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml +++ b/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml @@ -6,10 +6,13 @@ <Constraint android:id="@id/volume_dialog_main_slider_container" android:layout_width="@dimen/volume_dialog_slider_width" - android:layout_height="@dimen/volume_dialog_slider_height" + android:layout_height="0dp" + android:layout_marginTop="@dimen/volume_dialog_slider_vertical_margin" android:layout_marginEnd="@dimen/volume_dialog_components_spacing" + android:layout_marginBottom="@dimen/volume_dialog_slider_vertical_margin" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_max="@dimen/volume_dialog_slider_height" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="@fraction/volume_dialog_half_opened_bias" /> </ConstraintSet>
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java index 7d220b505aa0..6e23a0783c9d 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java @@ -21,11 +21,9 @@ import android.util.Log; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.PictureInPictureSurfaceTransaction; -import android.window.TaskSnapshot; import android.window.WindowAnimationState; import com.android.internal.os.IResultReceiver; -import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.wm.shell.recents.IRecentsAnimationController; public class RecentsAnimationControllerCompat { @@ -40,18 +38,6 @@ public class RecentsAnimationControllerCompat { mAnimationController = animationController; } - public ThumbnailData screenshotTask(int taskId) { - try { - final TaskSnapshot snapshot = mAnimationController.screenshotTask(taskId); - if (snapshot != null) { - return ThumbnailData.fromSnapshot(snapshot); - } - } catch (RemoteException e) { - Log.e(TAG, "Failed to screenshot task", e); - } - return new ThumbnailData(); - } - public void setInputConsumerEnabled(boolean enabled) { try { mAnimationController.setInputConsumerEnabled(enabled); diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java index 51892aac606a..ff6bcdb150f8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java @@ -19,6 +19,7 @@ package com.android.systemui.shared.system; import android.graphics.Rect; import android.os.Bundle; import android.view.RemoteAnimationTarget; +import android.window.TransitionInfo; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -30,7 +31,7 @@ public interface RecentsAnimationListener { */ void onAnimationStart(RecentsAnimationControllerCompat controller, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, - Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras); + Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras, TransitionInfo info); /** * Called when the animation into Recents was canceled. This call is made on the binder thread. diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java index acfa08643b63..c7ae02b61bff 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java @@ -142,33 +142,28 @@ public class KeyguardDisplayManager { private boolean isKeyguardShowable(Display display) { if (display == null) { - if (DEBUG) Log.i(TAG, "Cannot show Keyguard on null display"); + Log.i(TAG, "Cannot show Keyguard on null display"); return false; } if (ShadeWindowGoesAround.isEnabled()) { int shadeDisplayId = mShadePositionRepositoryProvider.get().getDisplayId().getValue(); if (display.getDisplayId() == shadeDisplayId) { - if (DEBUG) { - Log.i(TAG, - "Do not show KeyguardPresentation on the shade window display"); - } + Log.i(TAG, "Do not show KeyguardPresentation on the shade window display"); return false; } } else { if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) { - if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display"); + Log.i(TAG, "Do not show KeyguardPresentation on the default display"); return false; } } display.getDisplayInfo(mTmpDisplayInfo); if ((mTmpDisplayInfo.flags & Display.FLAG_PRIVATE) != 0) { - if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on a private display"); + Log.i(TAG, "Do not show KeyguardPresentation on a private display"); return false; } if ((mTmpDisplayInfo.flags & Display.FLAG_ALWAYS_UNLOCKED) != 0) { - if (DEBUG) { - Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display"); - } + Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display"); return false; } @@ -176,14 +171,11 @@ public class KeyguardDisplayManager { mDeviceStateHelper.isConcurrentDisplayActive(display) || mDeviceStateHelper.isRearDisplayOuterDefaultActive(display); if (mKeyguardStateController.isOccluded() && deviceStateOccludesKeyguard) { - if (DEBUG) { - // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the - // Keyguard state becomes "occluded". In this case, we should not show the - // KeyguardPresentation, since the activity is presenting content onto the - // non-default display. - Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear" - + " display is active"); - } + // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the Keyguard + // state becomes "occluded". In this case, we should not show the KeyguardPresentation, + // since the activity is presenting content onto the non-default display. + Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear" + + " display is active"); return false; } @@ -197,7 +189,7 @@ public class KeyguardDisplayManager { */ private boolean showPresentation(Display display) { if (!isKeyguardShowable(display)) return false; - if (DEBUG) Log.i(TAG, "Keyguard enabled on display: " + display); + Log.i(TAG, "Keyguard enabled on display: " + display); final int displayId = display.getDisplayId(); Presentation presentation = mPresentations.get(displayId); if (presentation == null) { @@ -239,7 +231,7 @@ public class KeyguardDisplayManager { public void show() { if (!mShowing) { - if (DEBUG) Log.v(TAG, "show"); + Log.v(TAG, "show"); if (mMediaRouter != null) { mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); @@ -253,7 +245,7 @@ public class KeyguardDisplayManager { public void hide() { if (mShowing) { - if (DEBUG) Log.v(TAG, "hide"); + Log.v(TAG, "hide"); if (mMediaRouter != null) { mMediaRouter.removeCallback(mMediaRouterCallback); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index d3c02e6f6449..b159a70066ce 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -29,7 +29,6 @@ import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor; import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -93,10 +92,8 @@ public class KeyguardPinViewController mPasswordEntry.setUserActivityListener(this::onUserInput); mView.onDevicePostureChanged(mPostureController.getDevicePosture()); mPostureController.addCallback(mPostureCallback); - if (mFeatureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)) { - mPasswordEntry.setUsePinShapes(true); - updateAutoConfirmationState(); - } + mPasswordEntry.setUsePinShapes(true); + updateAutoConfirmationState(); } protected void onUserInput() { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index a703b027b691..7d291c311ca3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -2591,6 +2591,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** + * @return true if optical udfps HW is supported on this device. Can return true even if the + * user has not enrolled udfps. This may be false if called before + * onAllAuthenticatorsRegistered. + */ + public boolean isOpticalUdfpsSupported() { + return mAuthController.isOpticalUdfpsSupported(); + } + + /** * @return true if there's at least one sfps enrollment for the current user. */ public boolean isSfpsEnrolled() { diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java index f530522fb707..5f79c8cada45 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java @@ -100,7 +100,8 @@ public abstract class SystemUIInitializer { .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper()) .setRecentTasks(mWMComponent.getRecentTasks()) .setBackAnimation(mWMComponent.getBackAnimation()) - .setDesktopMode(mWMComponent.getDesktopMode()); + .setDesktopMode(mWMComponent.getDesktopMode()) + .setAppZoomOut(mWMComponent.getAppZoomOut()); // Only initialize when not starting from tests since this currently initializes some // components that shouldn't be run in the test environment @@ -121,7 +122,8 @@ public abstract class SystemUIInitializer { .setStartingSurface(Optional.ofNullable(null)) .setRecentTasks(Optional.ofNullable(null)) .setBackAnimation(Optional.ofNullable(null)) - .setDesktopMode(Optional.ofNullable(null)); + .setDesktopMode(Optional.ofNullable(null)) + .setAppZoomOut(Optional.ofNullable(null)); } mSysUIComponent = builder.build(); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index d0cb507789a1..eee5f9e34317 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -1011,6 +1011,16 @@ public class AuthController implements } /** + * @return true if optical udfps HW is supported on this device. Can return true even if + * the user has not enrolled udfps. This may be false if called before + * onAllAuthenticatorsRegistered. + */ + public boolean isOpticalUdfpsSupported() { + return getUdfpsProps() != null && !getUdfpsProps().isEmpty() && getUdfpsProps() + .get(0).isOpticalUdfps(); + } + + /** * @return true if ultrasonic udfps HW is supported on this device. Can return true even if * the user has not enrolled udfps. This may be false if called before * onAllAuthenticatorsRegistered. diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt index 4dc2a13480f5..0303048436c9 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt @@ -104,6 +104,31 @@ constructor( } } + override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) { + withContext(backgroundDispatcher) { + if (!audioSharingInteractor.audioSharingAvailable()) { + return@withContext deviceItemActionInteractorImpl.onActionIconClick( + deviceItem, + onIntent, + ) + } + + when (deviceItem.type) { + DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + uiEventLogger.log(BluetoothTileDialogUiEvent.CHECK_MARK_ACTION_BUTTON_CLICKED) + audioSharingInteractor.stopAudioSharing() + } + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + uiEventLogger.log(BluetoothTileDialogUiEvent.PLUS_ACTION_BUTTON_CLICKED) + audioSharingInteractor.startAudioSharing() + } + else -> { + deviceItemActionInteractorImpl.onActionIconClick(deviceItem, onIntent) + } + } + } + } + private fun inSharingAndDeviceNoSource( inAudioSharing: Boolean, deviceItem: DeviceItem, diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt index c4f26cd46bf8..116e76c82008 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt @@ -29,6 +29,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest @@ -54,6 +55,8 @@ interface AudioSharingInteractor { suspend fun startAudioSharing() + suspend fun stopAudioSharing() + suspend fun audioSharingAvailable(): Boolean suspend fun qsDialogImprovementAvailable(): Boolean @@ -61,7 +64,7 @@ interface AudioSharingInteractor { @SysUISingleton @OptIn(ExperimentalCoroutinesApi::class) -class AudioSharingInteractorImpl +open class AudioSharingInteractorImpl @Inject constructor( private val context: Context, @@ -99,6 +102,9 @@ constructor( if (audioSharingAvailable()) { audioSharingRepository.leAudioBroadcastProfile?.let { profile -> isAudioSharingOn + // Skip the default value, we only care about adding source for newly + // started audio sharing session + .drop(1) .mapNotNull { audioSharingOn -> if (audioSharingOn) { // onBroadcastMetadataChanged could emit multiple times during one @@ -145,6 +151,13 @@ constructor( audioSharingRepository.startAudioSharing() } + override suspend fun stopAudioSharing() { + if (!audioSharingAvailable()) { + return + } + audioSharingRepository.stopAudioSharing() + } + // TODO(b/367965193): Move this after flags rollout override suspend fun audioSharingAvailable(): Boolean { return audioSharingRepository.audioSharingAvailable() @@ -181,6 +194,8 @@ class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingIntera override suspend fun startAudioSharing() {} + override suspend fun stopAudioSharing() {} + override suspend fun audioSharingAvailable(): Boolean = false override suspend fun qsDialogImprovementAvailable(): Boolean = false diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt index b9b8d36d41e6..44f9769f5930 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt @@ -45,6 +45,8 @@ interface AudioSharingRepository { suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) suspend fun startAudioSharing() + + suspend fun stopAudioSharing() } @SysUISingleton @@ -100,6 +102,15 @@ class AudioSharingRepositoryImpl( leAudioBroadcastProfile?.startPrivateBroadcast() } } + + override suspend fun stopAudioSharing() { + withContext(backgroundDispatcher) { + if (!settingsLibAudioSharingRepository.audioSharingAvailable()) { + return@withContext + } + leAudioBroadcastProfile?.stopLatestBroadcast() + } + } } @SysUISingleton @@ -117,4 +128,6 @@ class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {} override suspend fun startAudioSharing() {} + + override suspend fun stopAudioSharing() {} } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt index b294dd1b0b71..56caddfbd637 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt @@ -56,6 +56,13 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext +data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) { + enum class Target { + ENTIRE_ROW, + ACTION_ICON, + } +} + /** Dialog for showing active, connected and saved bluetooth devices. */ class BluetoothTileDialogDelegate @AssistedInject @@ -80,7 +87,7 @@ internal constructor( internal val bluetoothAutoOnToggle get() = mutableBluetoothAutoOnToggle.asStateFlow() - private val mutableDeviceItemClick: MutableSharedFlow<DeviceItem> = + private val mutableDeviceItemClick: MutableSharedFlow<DeviceItemClick> = MutableSharedFlow(extraBufferCapacity = 1) internal val deviceItemClick get() = mutableDeviceItemClick.asSharedFlow() @@ -90,7 +97,7 @@ internal constructor( internal val contentHeight get() = mutableContentHeight.asSharedFlow() - private val deviceItemAdapter: Adapter = Adapter(bluetoothTileDialogCallback) + private val deviceItemAdapter: Adapter = Adapter() private var lastUiUpdateMs: Long = -1 @@ -334,8 +341,7 @@ internal constructor( } } - internal inner class Adapter(private val onClickCallback: BluetoothTileDialogCallback) : - RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() { + internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() { private val diffUtilCallback = object : DiffUtil.ItemCallback<DeviceItem>() { @@ -376,7 +382,7 @@ internal constructor( override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) { val item = getItem(position) - holder.bind(item, onClickCallback) + holder.bind(item) } internal fun getItem(position: Int) = asyncListDiffer.currentList[position] @@ -390,19 +396,18 @@ internal constructor( private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name) private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary) private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon) - private val iconGear = view.requireViewById<ImageView>(R.id.gear_icon_image) - private val gearView = view.requireViewById<View>(R.id.gear_icon) + private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image) + private val actionIconView = view.requireViewById<View>(R.id.gear_icon) private val divider = view.requireViewById<View>(R.id.divider) - internal fun bind( - item: DeviceItem, - deviceItemOnClickCallback: BluetoothTileDialogCallback, - ) { + internal fun bind(item: DeviceItem) { container.apply { isEnabled = item.isEnabled background = item.background?.let { context.getDrawable(it) } setOnClickListener { - mutableDeviceItemClick.tryEmit(item) + mutableDeviceItemClick.tryEmit( + DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW) + ) uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED) } @@ -421,7 +426,8 @@ internal constructor( } } - iconGear.apply { drawable?.let { it.mutate()?.setTint(tintColor) } } + actionIcon.setImageResource(item.actionIconRes) + actionIcon.drawable?.setTint(tintColor) divider.setBackgroundColor(tintColor) @@ -454,8 +460,10 @@ internal constructor( nameView.text = item.deviceName summaryView.text = item.connectionSummary - gearView.setOnClickListener { - deviceItemOnClickCallback.onDeviceItemGearClicked(item, it) + actionIconView.setOnClickListener { + mutableDeviceItemClick.tryEmit( + DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON) + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt index aad233fe40ca..7c66ec059e64 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt @@ -49,7 +49,7 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED(1719), @Deprecated( "Use case no longer needed", - ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED") + ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED"), ) @UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked") LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720), @@ -59,7 +59,11 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent @UiEvent(doc = "Clicked on switch active button on audio sharing dialog") AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED(1890), @UiEvent(doc = "Clicked on share audio button on audio sharing dialog") - AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891); + AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891), + @UiEvent(doc = "Clicked on plus action button") + PLUS_ACTION_BUTTON_CLICKED(2061), + @UiEvent(doc = "Clicked on checkmark action button") + CHECK_MARK_ACTION_BUTTON_CLICKED(2062); override fun getId() = metricId } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt index 497d8cf2e159..9460e7c2c8d5 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt @@ -35,7 +35,6 @@ import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING -import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE import com.android.systemui.dagger.SysUISingleton @@ -227,8 +226,22 @@ constructor( // deviceItemClick is emitted when user clicked on a device item. dialogDelegate.deviceItemClick .onEach { - deviceItemActionInteractor.onClick(it, dialog) - logger.logDeviceClick(it.cachedBluetoothDevice.address, it.type) + when (it.target) { + DeviceItemClick.Target.ENTIRE_ROW -> { + deviceItemActionInteractor.onClick(it.deviceItem, dialog) + logger.logDeviceClick( + it.deviceItem.cachedBluetoothDevice.address, + it.deviceItem.type, + ) + } + + DeviceItemClick.Target.ACTION_ICON -> { + deviceItemActionInteractor.onActionIconClick(it.deviceItem) { intent + -> + startSettingsActivity(intent, it.clickedView) + } + } + } } .launchIn(this) @@ -287,20 +300,6 @@ constructor( ) } - override fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) { - uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED) - val intent = - Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply { - putExtra( - EXTRA_SHOW_FRAGMENT_ARGUMENTS, - Bundle().apply { - putString("device_address", deviceItem.cachedBluetoothDevice.address) - }, - ) - } - startSettingsActivity(intent, view) - } - override fun onSeeAllClicked(view: View) { uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED) startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view) @@ -382,8 +381,6 @@ constructor( } interface BluetoothTileDialogCallback { - fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) - fun onSeeAllClicked(view: View) fun onPairNewDeviceClicked(view: View) diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt index 2ba4c73a0293..f7af16d99fbf 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt @@ -53,5 +53,6 @@ data class DeviceItem( val background: Int? = null, var isEnabled: Boolean = true, var actionAccessibilityLabel: String = "", - var isActive: Boolean = false + var isActive: Boolean = false, + val actionIconRes: Int = -1, ) diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt index 2b55e1c51f5f..cb4ec37a1a66 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt @@ -16,6 +16,8 @@ package com.android.systemui.bluetooth.qsdialog +import android.content.Intent +import android.os.Bundle import com.android.internal.logging.UiEventLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -25,7 +27,9 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext interface DeviceItemActionInteractor { - suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {} + suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) + + suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) } @SysUISingleton @@ -67,4 +71,44 @@ constructor( } } } + + override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) { + withContext(backgroundDispatcher) { + deviceItem.cachedBluetoothDevice.apply { + when (deviceItem.type) { + DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, + DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, + DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, + DeviceItemType.SAVED_BLUETOOTH_DEVICE -> { + uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED) + val intent = + Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply { + putExtra( + EXTRA_SHOW_FRAGMENT_ARGUMENTS, + Bundle().apply { + putString( + "device_address", + deviceItem.cachedBluetoothDevice.address, + ) + }, + ) + } + onIntent(intent) + } + DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + throw IllegalArgumentException("Invalid device type: ${deviceItem.type}") + // Throw exception. Should already be handled in + // AudioSharingDeviceItemActionInteractor. + } + } + } + } + } + + private companion object { + const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" + const val ACTION_BLUETOOTH_DEVICE_DETAILS = + "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS" + } } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt index 92f05803f7cf..095e6e741584 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt @@ -30,6 +30,8 @@ private val backgroundOff = R.drawable.bluetooth_tile_dialog_bg_off private val backgroundOffBusy = R.drawable.bluetooth_tile_dialog_bg_off_busy private val connected = R.string.quick_settings_bluetooth_device_connected private val audioSharing = R.string.quick_settings_bluetooth_device_audio_sharing +private val audioSharingAddIcon = R.drawable.ic_add +private val audioSharingOnGoingIcon = R.drawable.ic_check private val saved = R.string.quick_settings_bluetooth_device_saved private val actionAccessibilityLabelActivate = R.string.accessibility_quick_settings_bluetooth_device_tap_to_activate @@ -63,6 +65,7 @@ abstract class DeviceItemFactory { background: Int, actionAccessibilityLabel: String, isActive: Boolean, + actionIconRes: Int = R.drawable.ic_settings_24dp, ): DeviceItem { return DeviceItem( type = type, @@ -75,6 +78,7 @@ abstract class DeviceItemFactory { isEnabled = !cachedDevice.isBusy, actionAccessibilityLabel = actionAccessibilityLabel, isActive = isActive, + actionIconRes = actionIconRes, ) } } @@ -125,6 +129,7 @@ internal class AudioSharingMediaDeviceItemFactory( if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn, "", isActive = !cachedDevice.isBusy, + actionIconRes = audioSharingOnGoingIcon, ) } } @@ -156,6 +161,7 @@ internal class AvailableAudioSharingMediaDeviceItemFactory( if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, "", isActive = false, + actionIconRes = audioSharingAddIcon, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt deleted file mode 100644 index 554dd6930f7f..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt +++ /dev/null @@ -1,66 +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.bouncer.ui.helper - -import androidx.annotation.VisibleForTesting - -/** Enumerates all known adaptive layout configurations. */ -enum class BouncerSceneLayout { - /** The default UI with the bouncer laid out normally. */ - STANDARD_BOUNCER, - /** The bouncer is displayed vertically stacked with the user switcher. */ - BELOW_USER_SWITCHER, - /** The bouncer is displayed side-by-side with the user switcher or an empty space. */ - BESIDE_USER_SWITCHER, - /** The bouncer is split in two with both sides shown side-by-side. */ - SPLIT_BOUNCER, -} - -/** Enumerates the supported window size classes. */ -enum class SizeClass { - COMPACT, - MEDIUM, - EXPANDED, -} - -/** - * Internal version of `calculateLayout` in the System UI Compose library, extracted here to allow - * for testing that's not dependent on Compose. - */ -@VisibleForTesting -fun calculateLayoutInternal( - width: SizeClass, - height: SizeClass, - isOneHandedModeSupported: Boolean, -): BouncerSceneLayout { - return when (height) { - SizeClass.COMPACT -> BouncerSceneLayout.SPLIT_BOUNCER - SizeClass.MEDIUM -> - when (width) { - SizeClass.COMPACT -> BouncerSceneLayout.STANDARD_BOUNCER - SizeClass.MEDIUM -> BouncerSceneLayout.STANDARD_BOUNCER - SizeClass.EXPANDED -> BouncerSceneLayout.BESIDE_USER_SWITCHER - } - SizeClass.EXPANDED -> - when (width) { - SizeClass.COMPACT -> BouncerSceneLayout.STANDARD_BOUNCER - SizeClass.MEDIUM -> BouncerSceneLayout.BELOW_USER_SWITCHER - SizeClass.EXPANDED -> BouncerSceneLayout.BESIDE_USER_SWITCHER - } - }.takeIf { it != BouncerSceneLayout.BESIDE_USER_SWITCHER || isOneHandedModeSupported } - ?: BouncerSceneLayout.STANDARD_BOUNCER -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt index 78156dbc8964..ca49de3b1510 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt @@ -25,6 +25,7 @@ import com.android.compose.animation.scene.TransitionKey */ object CommunalTransitionKeys { /** Fades the glanceable hub without any translation */ + @Deprecated("No longer supported as all hub transitions will be fades.") val SimpleFade = TransitionKey("SimpleFade") /** Transition from the glanceable hub before entering edit mode */ val ToEditMode = TransitionKey("ToEditMode") diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 00eead6eb7fc..555fe6ef157d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.NotificationInsetsModule; import com.android.systemui.statusbar.QsFrameTranslateModule; import com.android.systemui.statusbar.phone.ConfigurationForwarder; import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.wm.shell.appzoomout.AppZoomOut; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.desktopmode.DesktopMode; @@ -115,6 +116,9 @@ public interface SysUIComponent { @BindsInstance Builder setDesktopMode(Optional<DesktopMode> d); + @BindsInstance + Builder setAppZoomOut(Optional<AppZoomOut> a); + SysUIComponent build(); } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt index 41a59a959771..ae6238724042 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.util.kotlin.FlowDumperImpl @@ -50,6 +51,8 @@ class DeviceEntryHapticsInteractor constructor( biometricSettingsRepository: BiometricSettingsRepository, deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor, + deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, + keyguardBypassInteractor: KeyguardBypassInteractor, deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor: DeviceEntrySourceInteractor, fingerprintPropertyRepository: FingerprintPropertyRepository, @@ -82,7 +85,7 @@ constructor( emit(recentPowerButtonPressThresholdMs * -1L - 1L) } - val playSuccessHaptic: Flow<Unit> = + private val playHapticsOnDeviceEntry: Flow<Boolean> = deviceEntrySourceInteractor.deviceEntryFromBiometricSource .sample( combine( @@ -92,17 +95,29 @@ constructor( ::Triple, ) ) - .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> + .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> val sideFpsAllowsHaptic = !powerButtonDown && systemClock.uptimeMillis() - lastPowerButtonWakeup > recentPowerButtonPressThresholdMs val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic if (!allowHaptic) { - logger.d("Skip success haptic. Recent power button press or button is down.") + logger.d( + "Skip success entry haptic from power button. Recent power button press or button is down." + ) } allowHaptic } + + private val playHapticsOnFaceAuthSuccessAndBypassDisabled: Flow<Boolean> = + deviceEntryFaceAuthInteractor.isAuthenticated + .filter { it } + .sample(keyguardBypassInteractor.isBypassAvailable) + .map { !it } + + val playSuccessHaptic: Flow<Unit> = + merge(playHapticsOnDeviceEntry, playHapticsOnFaceAuthSuccessAndBypassDisabled) + .filter { it } // map to Unit .map {} .dumpWhileCollecting("playSuccessHaptic") diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java index e02e3fbc339b..10f060c13a59 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java @@ -22,10 +22,10 @@ import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAK import android.annotation.MainThread; import android.content.res.Configuration; import android.hardware.display.AmbientDisplayConfiguration; -import android.os.Trace; import android.util.Log; import android.view.Display; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.internal.util.Preconditions; import com.android.systemui.dock.DockManager; import com.android.systemui.doze.dagger.DozeScope; @@ -314,7 +314,7 @@ public class DozeMachine { mState = newState; mDozeLog.traceState(newState); - Trace.traceCounter(Trace.TRACE_TAG_APP, "doze_machine_state", newState.ordinal()); + TrackTracer.instantForGroup("keyguard", "doze_machine_state", newState.ordinal()); updatePulseReason(newState, oldState, pulseReason); performTransitionOnComponents(oldState, newState); diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt index 21002c676ec6..d7a4dba3188a 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt @@ -278,11 +278,11 @@ constructor( } private suspend fun hasInitialDelayElapsed(deviceType: DeviceType): Boolean { - val oobeLaunchTime = - tutorialRepository.getScheduledTutorialLaunchTime(deviceType) ?: return false - return clock - .instant() - .isAfter(oobeLaunchTime.plusSeconds(initialDelayDuration.inWholeSeconds)) + val oobeTime = + tutorialRepository.getScheduledTutorialLaunchTime(deviceType) + ?: tutorialRepository.getNotifiedTime(deviceType) + ?: return false + return clock.instant().isAfter(oobeTime.plusSeconds(initialDelayDuration.inWholeSeconds)) } private data class StatsUpdateRequest( diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 63ac783ad42b..2ed0671a570b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -23,19 +23,12 @@ import com.android.server.notification.Flags.crossAppPoliteNotifications import com.android.server.notification.Flags.politeNotifications import com.android.server.notification.Flags.vibrateWhileUnlocked import com.android.systemui.Flags.FLAG_COMMUNAL_HUB -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON -import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP import com.android.systemui.Flags.communalHub -import com.android.systemui.Flags.statusBarCallChipNotificationIcon -import com.android.systemui.Flags.statusBarScreenSharingChips -import com.android.systemui.Flags.statusBarUseReposForCallChip import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression import com.android.systemui.statusbar.notification.shared.NotificationMinimalism @@ -57,7 +50,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token NotificationMinimalism.token dependsOn NotificationThrottleHun.token - ModesEmptyShadeFix.token dependsOn FooterViewRefactor.token ModesEmptyShadeFix.token dependsOn modesUi // SceneContainer dependencies @@ -65,10 +57,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // DualShade dependencies DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag() - - // Status bar chip dependencies - statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken - statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken } private inline val politeNotifications @@ -88,17 +76,4 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha private inline val communalHub get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub()) - - private inline val statusBarCallChipNotificationIconToken - get() = - FlagToken( - FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, - statusBarCallChipNotificationIcon(), - ) - - private inline val statusBarScreenSharingChipsToken - get() = FlagToken(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, statusBarScreenSharingChips()) - - private inline val statusBarUseReposForCallChipToken - get() = FlagToken(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, statusBarUseReposForCallChip()) } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index c039e0188064..2c33c0b4403b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -76,21 +76,10 @@ object Flags { val LOCKSCREEN_CUSTOM_CLOCKS = resourceBooleanFlag(R.bool.config_enableLockScreenCustomClocks, "lockscreen_custom_clocks") - /** - * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository - * will occur in stages. This is one stage of many to come. - */ - // TODO(b/255607168): Tracking Bug - @JvmField val DOZING_MIGRATION_1 = unreleasedFlag("dozing_migration_1") - /** Flag to control the revamp of keyguard biometrics progress animation */ // TODO(b/244313043): Tracking bug @JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag("biometrics_animation_revamp") - // flag for controlling auto pin confirmation and material u shapes in bouncer - @JvmField - val AUTO_PIN_CONFIRMATION = releasedFlag("auto_pin_confirmation", "auto_pin_confirmation") - /** Enables code to show contextual loyalty cards in wallet entrypoints */ // TODO(b/294110497): Tracking Bug @JvmField @@ -100,10 +89,6 @@ object Flags { // TODO(b/242908637): Tracking Bug @JvmField val WALLPAPER_FULLSCREEN_PREVIEW = releasedFlag("wallpaper_fullscreen_preview") - /** Inflate and bind views upon emitting a blueprint value . */ - // TODO(b/297365780): Tracking Bug - @JvmField val LAZY_INFLATE_KEYGUARD = releasedFlag("lazy_inflate_keyguard") - /** Enables UI updates for AI wallpapers in the wallpaper picker. */ // TODO(b/267722622): Tracking Bug @JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = releasedFlag("wallpaper_picker_ui_for_aiwp") diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index e1ebf7cdf472..cf5c3402792e 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -463,6 +463,9 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mTelephonyListenerManager.removeServiceStateListener(mPhoneStateListener); mGlobalSettings.unregisterContentObserverSync(mAirplaneModeObserver); mConfigurationController.removeCallback(this); + if (mShowSilentToggle) { + mRingerModeTracker.getRingerMode().removeObservers(this); + } } protected Context getContext() { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt index 19a19d551613..c702ba9f401e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt @@ -25,6 +25,7 @@ import com.android.systemui.CoreStartable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyboard.shortcut.data.repository.CustomInputGesturesRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.CommandQueue @@ -41,6 +42,7 @@ constructor( private val stateRepository: ShortcutHelperStateRepository, private val activityStarter: ActivityStarter, @Background private val backgroundScope: CoroutineScope, + private val customInputGesturesRepository: CustomInputGesturesRepository ) : CoreStartable { override fun start() { registerBroadcastReceiver( @@ -55,6 +57,10 @@ constructor( action = Intent.ACTION_CLOSE_SYSTEM_DIALOGS, onReceive = { stateRepository.hide() }, ) + registerBroadcastReceiver( + action = Intent.ACTION_USER_SWITCHED, + onReceive = { customInputGesturesRepository.refreshCustomInputGestures() }, + ) commandQueue.addCallback( object : CommandQueue.Callbacks { override fun dismissKeyboardShortcutsMenu() { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt index 36cd40052041..e5c638cbdfba 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt @@ -25,6 +25,7 @@ import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_RES import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS import android.hardware.input.InputSettings import android.util.Log +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult.ERROR_OTHER @@ -37,6 +38,7 @@ import kotlinx.coroutines.withContext import javax.inject.Inject import kotlin.coroutines.CoroutineContext +@SysUISingleton class CustomInputGesturesRepository @Inject constructor(private val userTracker: UserTracker, @@ -56,7 +58,7 @@ constructor(private val userTracker: UserTracker, val customInputGestures = _customInputGesture.onStart { refreshCustomInputGestures() } - private fun refreshCustomInputGestures() { + fun refreshCustomInputGestures() { setCustomInputGestures(inputGestures = retrieveCustomInputGestures()) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt index d7be5e622276..6a42cdc876ca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt @@ -27,15 +27,22 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS +import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System @@ -58,6 +65,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System, KEY_GESTURE_TYPE_ALL_APPS to System, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to System, // Multitasking Category KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to MultiTasking, @@ -66,6 +74,11 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to MultiTasking, KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT to MultiTasking, KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to MultiTasking, + KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW to MultiTasking, + KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW to MultiTasking, + KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW to MultiTasking, + KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW to MultiTasking, + KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY to MultiTasking, // App Category KEY_GESTURE_TYPE_LAUNCH_APPLICATION to AppCategories, @@ -90,6 +103,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.shortcut_helper_category_system_apps, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to R.string.shortcut_helper_category_system_apps, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.shortcut_helper_category_system_apps, // Multitasking Category KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to @@ -102,15 +116,23 @@ class InputGestureMaps @Inject constructor(private val context: Context) { R.string.shortcutHelper_category_split_screen, KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to R.string.shortcutHelper_category_split_screen, + KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW to + R.string.shortcutHelper_category_split_screen, + KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW to + R.string.shortcutHelper_category_split_screen, + KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW to + R.string.shortcutHelper_category_split_screen, + KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW to + R.string.shortcutHelper_category_split_screen, + KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY to R.string.shortcutHelper_category_split_screen, // App Category - KEY_GESTURE_TYPE_LAUNCH_APPLICATION to - R.string.keyboard_shortcut_group_applications, + KEY_GESTURE_TYPE_LAUNCH_APPLICATION to R.string.keyboard_shortcut_group_applications, ) /** - * App Category shortcut labels are mapped dynamically based on intent - * see [InputGestureDataAdapter.fetchShortcutLabelByAppLaunchData] + * App Category shortcut labels are mapped dynamically based on intent see + * [InputGestureDataAdapter.fetchShortcutLabelByAppLaunchData] */ val gestureToInternalKeyboardShortcutInfoLabelResIdMap = mapOf( @@ -130,12 +152,23 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.group_system_access_google_assistant, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to R.string.group_system_access_google_assistant, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.group_system_toggle_voice_access, // Multitasking Category KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to R.string.group_system_cycle_forward, KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to R.string.system_multitasking_lhs, KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to R.string.system_multitasking_rhs, KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to R.string.system_multitasking_full_screen, + KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW to + R.string.system_desktop_mode_snap_left_window, + KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW to + R.string.system_desktop_mode_snap_right_window, + KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW to + R.string.system_desktop_mode_minimize_window, + KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW to + R.string.system_desktop_mode_toggle_maximize_window, + KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY to + R.string.system_multitasking_move_to_next_display, ) val shortcutLabelToKeyGestureTypeMap: Map<String, Int> diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt index 5060abdda247..c3c9df97a682 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt @@ -32,12 +32,14 @@ import android.view.KeyEvent.KEYCODE_RECENT_APPS import android.view.KeyEvent.KEYCODE_S import android.view.KeyEvent.KEYCODE_SLASH import android.view.KeyEvent.KEYCODE_TAB +import android.view.KeyEvent.KEYCODE_V import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CTRL_ON import android.view.KeyEvent.META_META_ON import android.view.KeyEvent.META_SHIFT_ON import android.view.KeyboardShortcutGroup import android.view.KeyboardShortcutInfo +import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures import com.android.systemui.Flags.shortcutHelperKeyGlyph import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo @@ -118,8 +120,8 @@ constructor(@Main private val resources: Resources, private val inputManager: In return shortcuts } - private fun systemControlsShortcuts() = - listOf( + private fun systemControlsShortcuts(): List<KeyboardShortcutInfo> { + val shortcuts = mutableListOf( // Access list of all apps and search (i.e. Search/Launcher): // - Meta shortcutInfo(resources.getString(R.string.group_system_access_all_apps_search)) { @@ -176,6 +178,19 @@ constructor(@Main private val resources: Resources, private val inputManager: In }, ) + if (enableVoiceAccessKeyGestures()) { + shortcuts.add( + // Toggle voice access: + // - Meta + Alt + V + shortcutInfo(resources.getString(R.string.group_system_toggle_voice_access)) { + command(META_META_ON or META_ALT_ON, KEYCODE_V) + } + ) + } + + return shortcuts + } + private fun systemAppsShortcuts() = listOf( // Pull up Notes app for quick memo: diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt index 274fa59045d7..a16b4a6892b4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt @@ -53,11 +53,11 @@ constructor( override suspend fun onActivated(): Nothing { viewModel.shortcutCustomizationUiState.collect { uiState -> - when(uiState){ + when (uiState) { is AddShortcutDialog, is DeleteShortcutDialog, is ResetShortcutDialog -> { - if (dialog == null){ + if (dialog == null) { dialog = createDialog().also { it.show() } } } @@ -85,7 +85,9 @@ constructor( ShortcutCustomizationDialog( uiState = uiState, modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp), - onKeyPress = { viewModel.onKeyPressed(it) }, + onShortcutKeyCombinationSelected = { + viewModel.onShortcutKeyCombinationSelected(it) + }, onCancel = { dialog.dismiss() }, onConfirmSetShortcut = { coroutineScope.launch { viewModel.onSetShortcut() } }, onConfirmDeleteShortcut = { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt index 3819f6d41856..d9e55f89cda5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt @@ -49,8 +49,12 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -65,7 +69,7 @@ import com.android.systemui.res.R fun ShortcutCustomizationDialog( uiState: ShortcutCustomizationUiState, modifier: Modifier = Modifier, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, onCancel: () -> Unit, onConfirmSetShortcut: () -> Unit, onConfirmDeleteShortcut: () -> Unit, @@ -73,7 +77,13 @@ fun ShortcutCustomizationDialog( ) { when (uiState) { is ShortcutCustomizationUiState.AddShortcutDialog -> { - AddShortcutDialog(modifier, uiState, onKeyPress, onCancel, onConfirmSetShortcut) + AddShortcutDialog( + modifier, + uiState, + onShortcutKeyCombinationSelected, + onCancel, + onConfirmSetShortcut, + ) } is ShortcutCustomizationUiState.DeleteShortcutDialog -> { DeleteShortcutDialog(modifier, onCancel, onConfirmDeleteShortcut) @@ -91,29 +101,27 @@ fun ShortcutCustomizationDialog( private fun AddShortcutDialog( modifier: Modifier, uiState: ShortcutCustomizationUiState.AddShortcutDialog, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, onCancel: () -> Unit, - onConfirmSetShortcut: () -> Unit -){ + onConfirmSetShortcut: () -> Unit, +) { Column(modifier = modifier) { Title(uiState.shortcutLabel) Description( - text = - stringResource( - id = R.string.shortcut_customize_mode_add_shortcut_description - ) + text = stringResource(id = R.string.shortcut_customize_mode_add_shortcut_description) ) PromptShortcutModifier( modifier = - Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp) - .width(131.dp) - .height(48.dp), + Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp) + .width(131.dp) + .height(48.dp), defaultModifierKey = uiState.defaultCustomShortcutModifierKey, ) SelectedKeyCombinationContainer( shouldShowError = uiState.errorMessage.isNotEmpty(), - onKeyPress = onKeyPress, + onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected, pressedKeys = uiState.pressedKeys, + onConfirmSetShortcut = onConfirmSetShortcut, ) ErrorMessageContainer(uiState.errorMessage) DialogButtons( @@ -121,9 +129,7 @@ private fun AddShortcutDialog( isConfirmButtonEnabled = uiState.pressedKeys.isNotEmpty(), onConfirm = onConfirmSetShortcut, confirmButtonText = - stringResource( - R.string.shortcut_helper_customize_dialog_set_shortcut_button_label - ), + stringResource(R.string.shortcut_helper_customize_dialog_set_shortcut_button_label), ) } } @@ -132,20 +138,15 @@ private fun AddShortcutDialog( private fun DeleteShortcutDialog( modifier: Modifier, onCancel: () -> Unit, - onConfirmDeleteShortcut: () -> Unit -){ + onConfirmDeleteShortcut: () -> Unit, +) { ConfirmationDialog( modifier = modifier, - title = - stringResource( - id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title - ), + title = stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title), description = - stringResource( - id = R.string.shortcut_customize_mode_remove_shortcut_description - ), + stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_description), confirmButtonText = - stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label), + stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label), onCancel = onCancel, onConfirm = onConfirmDeleteShortcut, ) @@ -155,20 +156,15 @@ private fun DeleteShortcutDialog( private fun ResetShortcutDialog( modifier: Modifier, onCancel: () -> Unit, - onConfirmResetShortcut: () -> Unit -){ + onConfirmResetShortcut: () -> Unit, +) { ConfirmationDialog( modifier = modifier, - title = - stringResource( - id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title - ), + title = stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title), description = - stringResource( - id = R.string.shortcut_customize_mode_reset_shortcut_description - ), + stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_description), confirmButtonText = - stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label), + stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label), onCancel = onCancel, onConfirm = onConfirmResetShortcut, ) @@ -201,6 +197,9 @@ private fun DialogButtons( onConfirm: () -> Unit, confirmButtonText: String, ) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { focusRequester.requestFocus() } + Row( modifier = Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp) @@ -218,6 +217,10 @@ private fun DialogButtons( ) Spacer(modifier = Modifier.width(8.dp)) ShortcutHelperButton( + modifier = + Modifier.focusRequester(focusRequester).focusProperties { + canFocus = true + }, // enable focus on touch/click mode onClick = onConfirm, color = MaterialTheme.colorScheme.primary, width = 116.dp, @@ -248,8 +251,9 @@ private fun ErrorMessageContainer(errorMessage: String) { @Composable private fun SelectedKeyCombinationContainer( shouldShowError: Boolean, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, pressedKeys: List<ShortcutKey>, + onConfirmSetShortcut: () -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() @@ -269,7 +273,17 @@ private fun SelectedKeyCombinationContainer( Modifier.padding(all = 16.dp) .sizeIn(minWidth = 332.dp, minHeight = 56.dp) .border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp)) - .onKeyEvent { onKeyPress(it) } + .onPreviewKeyEvent { keyEvent -> + val keyEventProcessed = onShortcutKeyCombinationSelected(keyEvent) + if ( + !keyEventProcessed && + keyEvent.key == Key.Enter && + keyEvent.type == KeyEventType.KeyUp + ) { + onConfirmSetShortcut() + true + } else keyEventProcessed + } .focusProperties { canFocus = true } // enables keyboard focus when in touch mode .focusRequester(focusRequester), interactionSource = interactionSource, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt index aea583d67289..ba31d08c9c1b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -729,6 +729,7 @@ private fun AddShortcutButton(onClick: () -> Unit) { contentColor = MaterialTheme.colorScheme.primary, contentPaddingVertical = 0.dp, contentPaddingHorizontal = 0.dp, + contentDescription = stringResource(R.string.shortcut_helper_add_shortcut_button_label), ) } @@ -749,6 +750,7 @@ private fun DeleteShortcutButton(onClick: () -> Unit) { contentColor = MaterialTheme.colorScheme.primary, contentPaddingVertical = 0.dp, contentPaddingHorizontal = 0.dp, + contentDescription = stringResource(R.string.shortcut_helper_delete_shortcut_button_label), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt index 55c0fe297bcb..9a380f495176 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt @@ -230,6 +230,7 @@ fun ShortcutHelperButton( contentPaddingVertical: Dp = 10.dp, enabled: Boolean = true, border: BorderStroke? = null, + contentDescription: String? = null, ) { ShortcutHelperButtonSurface( onClick = onClick, @@ -254,8 +255,7 @@ fun ShortcutHelperButton( Icon( tint = contentColor, imageVector = iconSource.imageVector, - contentDescription = - null, // TODO this probably should not be null for accessibility. + contentDescription = contentDescription, modifier = Modifier.size(20.dp).wrapContentSize(Alignment.Center), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt index 373eb250d61d..915a66c43a12 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt @@ -46,6 +46,7 @@ constructor( private val context: Context, private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor, ) { + private var keyDownEventCache: KeyEvent? = null private val _shortcutCustomizationUiState = MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive) @@ -94,9 +95,16 @@ constructor( shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) } - fun onKeyPressed(keyEvent: KeyEvent): Boolean { - if ((keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown)) { - updatePressedKeys(keyEvent) + fun onShortcutKeyCombinationSelected(keyEvent: KeyEvent): Boolean { + if (isModifier(keyEvent)) { + return false + } + if (keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown) { + keyDownEventCache = keyEvent + return true + } else if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key == keyDownEventCache?.key) { + updatePressedKeys(keyDownEventCache!!) + clearKeyDownEventCache() return true } return false @@ -157,16 +165,21 @@ constructor( return (uiState as? AddShortcutDialog)?.copy(errorMessage = errorMessage) ?: uiState } + private fun isModifier(keyEvent: KeyEvent) = SUPPORTED_MODIFIERS.contains(keyEvent.key) + private fun updatePressedKeys(keyEvent: KeyEvent) { - val isModifier = SUPPORTED_MODIFIERS.contains(keyEvent.key) val keyCombination = KeyCombination( modifiers = keyEvent.nativeKeyEvent.modifiers, - keyCode = if (!isModifier) keyEvent.key.nativeKeyCode else null, + keyCode = if (!isModifier(keyEvent)) keyEvent.key.nativeKeyCode else null, ) shortcutCustomizationInteractor.updateUserSelectedKeyCombination(keyCombination) } + private fun clearKeyDownEventCache() { + keyDownEventCache = null + } + @AssistedFactory interface Factory { fun create(): ShortcutCustomizationViewModel diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index d40fe468b0a5..591383999182 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -538,27 +538,30 @@ public class KeyguardService extends Service { @Override // Binder interface public void onFinishedGoingToSleep( - @PowerManager.GoToSleepReason int pmSleepReason, boolean cameraGestureTriggered) { + @PowerManager.GoToSleepReason int pmSleepReason, boolean + powerButtonLaunchGestureTriggered) { trace("onFinishedGoingToSleep pmSleepReason=" + pmSleepReason - + " cameraGestureTriggered=" + cameraGestureTriggered); + + " powerButtonLaunchTriggered=" + powerButtonLaunchGestureTriggered); checkPermission(); mKeyguardViewMediator.onFinishedGoingToSleep( WindowManagerPolicyConstants.translateSleepReasonToOffReason(pmSleepReason), - cameraGestureTriggered); - mPowerInteractor.onFinishedGoingToSleep(cameraGestureTriggered); + powerButtonLaunchGestureTriggered); + mPowerInteractor.onFinishedGoingToSleep(powerButtonLaunchGestureTriggered); mKeyguardLifecyclesDispatcher.dispatch( KeyguardLifecyclesDispatcher.FINISHED_GOING_TO_SLEEP); } @Override // Binder interface public void onStartedWakingUp( - @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) { + @PowerManager.WakeReason int pmWakeReason, + boolean powerButtonLaunchGestureTriggered) { trace("onStartedWakingUp pmWakeReason=" + pmWakeReason - + " cameraGestureTriggered=" + cameraGestureTriggered); + + " powerButtonLaunchGestureTriggered=" + powerButtonLaunchGestureTriggered); Trace.beginSection("KeyguardService.mBinder#onStartedWakingUp"); checkPermission(); - mKeyguardViewMediator.onStartedWakingUp(pmWakeReason, cameraGestureTriggered); - mPowerInteractor.onStartedWakingUp(pmWakeReason, cameraGestureTriggered); + mKeyguardViewMediator.onStartedWakingUp(pmWakeReason, + powerButtonLaunchGestureTriggered); + mPowerInteractor.onStartedWakingUp(pmWakeReason, powerButtonLaunchGestureTriggered); mKeyguardLifecyclesDispatcher.dispatch( KeyguardLifecyclesDispatcher.STARTED_WAKING_UP, pmWakeReason); Trace.endSection(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 63ac5094c400..647362873015 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -109,6 +109,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.app.animation.Interpolators; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.internal.foldables.FoldGracePeriodProvider; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.Configuration; @@ -813,7 +814,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (targetUserId != mSelectedUserInteractor.getSelectedUserId()) { return; } - if (DEBUG) Log.d(TAG, "keyguardDone"); + Log.d(TAG, "keyguardDone"); tryKeyguardDone(); } @@ -832,7 +833,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @Override public void keyguardDonePending(int targetUserId) { Trace.beginSection("KeyguardViewMediator.mViewMediatorCallback#keyguardDonePending"); - if (DEBUG) Log.d(TAG, "keyguardDonePending"); + Log.d(TAG, "keyguardDonePending"); if (targetUserId != mSelectedUserInteractor.getSelectedUserId()) { Trace.endSection(); return; @@ -2735,10 +2736,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } private void tryKeyguardDone() { - if (DEBUG) { - Log.d(TAG, "tryKeyguardDone: pending - " + mKeyguardDonePending + ", animRan - " - + mHideAnimationRun + " animRunning - " + mHideAnimationRunning); - } + Log.d(TAG, "tryKeyguardDone: pending - " + mKeyguardDonePending + ", animRan - " + + mHideAnimationRun + " animRunning - " + mHideAnimationRunning); if (!mKeyguardDonePending && mHideAnimationRun && !mHideAnimationRunning) { handleKeyguardDone(); } else if (mSurfaceBehindRemoteAnimationRunning) { @@ -3040,7 +3039,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } private final Runnable mHideAnimationFinishedRunnable = () -> { - Log.e(TAG, "mHideAnimationFinishedRunnable#run"); + Log.d(TAG, "mHideAnimationFinishedRunnable#run"); mHideAnimationRunning = false; tryKeyguardDone(); }; @@ -3983,7 +3982,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, public void setPendingLock(boolean hasPendingLock) { mPendingLock = hasPendingLock; - Trace.traceCounter(Trace.TRACE_TAG_APP, "pendingLock", mPendingLock ? 1 : 0); + TrackTracer.instantForGroup("keyguard", "pendingLock", mPendingLock ? 1 : 0); } private boolean isViewRootReady() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java index 633628f1167e..c3182003227f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java @@ -16,8 +16,7 @@ package com.android.systemui.keyguard; -import android.os.Trace; - +import com.android.app.tracing.coroutines.TrackTracer; import com.android.systemui.Dumpable; import com.android.systemui.dump.DumpManager; import com.android.systemui.power.domain.interactor.PowerInteractor; @@ -80,7 +79,7 @@ public class ScreenLifecycle extends Lifecycle<ScreenLifecycle.Observer> impleme private void setScreenState(int screenState) { mScreenState = screenState; - Trace.traceCounter(Trace.TRACE_TAG_APP, "screenState", screenState); + TrackTracer.instantForGroup("screen", "screenState", screenState); } public interface Observer { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java index c0ffda6640b2..c261cfefb2b8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java @@ -24,11 +24,11 @@ import android.graphics.Point; import android.os.Bundle; import android.os.PowerManager; import android.os.RemoteException; -import android.os.Trace; import android.util.DisplayMetrics; import androidx.annotation.Nullable; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; @@ -197,7 +197,7 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe private void setWakefulness(@Wakefulness int wakefulness) { mWakefulness = wakefulness; - Trace.traceCounter(Trace.TRACE_TAG_APP, "wakefulness", wakefulness); + TrackTracer.instantForGroup("screen", "wakefulness", wakefulness); } private void updateLastWakeOriginLocation() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index ac04dd5a7ec1..a39982dd31e7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data.repository import android.graphics.Point +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.widget.LockPatternUtils import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback @@ -64,7 +65,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch /** Defines interface for classes that encapsulate application state for the keyguard. */ interface KeyguardRepository { @@ -248,13 +248,6 @@ interface KeyguardRepository { val keyguardDoneAnimationsFinished: Flow<Unit> /** - * Receive whether clock should be centered on lockscreen. - * - * @deprecated When scene container flag is on use clockShouldBeCentered from domain level. - */ - val clockShouldBeCentered: Flow<Boolean> - - /** * Whether the primary authentication is required for the given user due to lockdown or * encryption after reboot. */ @@ -306,8 +299,6 @@ interface KeyguardRepository { suspend fun setKeyguardDone(keyguardDoneType: KeyguardDone) - fun setClockShouldBeCentered(shouldBeCentered: Boolean) - /** * Updates signal that the keyguard done animations are finished * @@ -390,9 +381,6 @@ constructor( override val panelAlpha: MutableStateFlow<Float> = MutableStateFlow(1f) - private val _clockShouldBeCentered = MutableStateFlow(true) - override val clockShouldBeCentered: Flow<Boolean> = _clockShouldBeCentered.asStateFlow() - override val topClippingBounds = MutableStateFlow<Int?>(null) override val isKeyguardShowing: MutableStateFlow<Boolean> = @@ -681,10 +669,6 @@ constructor( _isQuickSettingsVisible.value = isVisible } - override fun setClockShouldBeCentered(shouldBeCentered: Boolean) { - _clockShouldBeCentered.value = shouldBeCentered - } - override fun setKeyguardEnabled(enabled: Boolean) { _isKeyguardEnabled.value = enabled } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index 354fc3d82342..24f2493c626d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -212,8 +212,11 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR Log.i(TAG, "Duplicate call to start the transition, rejecting: $info") return@withContext null } + val isAnimatorRunning = lastAnimator?.isRunning() ?: false + val isManualTransitionRunning = + updateTransitionId != null && lastStep.transitionState != TransitionState.FINISHED val startingValue = - if (lastStep.transitionState != TransitionState.FINISHED) { + if (isAnimatorRunning || isManualTransitionRunning) { Log.i(TAG, "Transition still active: $lastStep, canceling") when (info.modeOnCanceled) { TransitionModeOnCanceled.LAST_VALUE -> lastStep.value 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 9896365abff9..b42da5265d86 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 @@ -132,6 +132,8 @@ constructor( if (SceneContainerFlag.isEnabled) return@collect startTransitionTo( toState = KeyguardState.GONE, + modeOnCanceled = TransitionModeOnCanceled.REVERSE, + ownerReason = "canWakeDirectlyToGone = true", ) } else if (shouldTransitionToLockscreen) { val modeOnCanceled = @@ -146,7 +148,7 @@ constructor( startTransitionTo( toState = KeyguardState.LOCKSCREEN, modeOnCanceled = modeOnCanceled, - ownerReason = "listen for aod to awake" + ownerReason = "listen for aod to awake", ) } else if (shouldTransitionToOccluded) { startTransitionTo( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt index f792935e67f3..ab5fdd608d03 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt @@ -25,6 +25,10 @@ import com.android.systemui.keyguard.data.repository.KeyguardClockRepository import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING +import com.android.systemui.keyguard.shared.model.KeyguardState.GONE +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockId @@ -39,6 +43,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -117,7 +122,43 @@ constructor( } } } else { - keyguardInteractor.clockShouldBeCentered + combine( + shadeInteractor.isShadeLayoutWide, + activeNotificationsInteractor.areAnyNotificationsPresent, + keyguardInteractor.dozeTransitionModel, + keyguardTransitionInteractor.startedKeyguardTransitionStep.map { it.to == AOD }, + keyguardTransitionInteractor.startedKeyguardTransitionStep.map { + it.to == LOCKSCREEN + }, + keyguardTransitionInteractor.startedKeyguardTransitionStep.map { + it.to == DOZING + }, + keyguardInteractor.isPulsing, + keyguardTransitionInteractor.startedKeyguardTransitionStep.map { it.to == GONE }, + ) { + isShadeLayoutWide, + areAnyNotificationsPresent, + dozeTransitionModel, + startedToAod, + startedToLockScreen, + startedToDoze, + isPulsing, + startedToGone -> + when { + !isShadeLayoutWide -> true + // [areAnyNotificationsPresent] also reacts to notification stack in + // homescreen + // it may cause unnecessary `false` emission when there's notification in + // homescreen + // but none in lockscreen when going from GONE to AOD / DOZING + // use null to skip emitting wrong value + startedToGone || startedToDoze -> null + startedToLockScreen -> !areAnyNotificationsPresent + startedToAod -> !isPulsing + else -> true + } + } + .filterNotNull() } fun setClockSize(size: ClockSize) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 0193d7cba616..8f7f2a0a8cbb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -44,7 +44,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.StatusBarState -import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -84,7 +83,6 @@ class KeyguardInteractor @Inject constructor( private val repository: KeyguardRepository, - powerInteractor: PowerInteractor, bouncerRepository: KeyguardBouncerRepository, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, shadeRepository: ShadeRepository, @@ -216,11 +214,7 @@ constructor( // should actually be quite strange to leave AOD and then go straight to // DREAMING so this should be fine. delay(IS_ABLE_TO_DREAM_DELAY_MS) - isDreaming - .sample(powerInteractor.isAwake) { isDreaming, isAwake -> - isDreaming && isAwake - } - .debounce(50L) + isDreaming.debounce(50L) } else { flowOf(false) } @@ -418,8 +412,6 @@ constructor( initialValue = 0f, ) - val clockShouldBeCentered: Flow<Boolean> = repository.clockShouldBeCentered - /** Whether to animate the next doze mode transition. */ val animateDozingTransitions: Flow<Boolean> by lazy { if (SceneContainerFlag.isEnabled) { @@ -485,10 +477,6 @@ constructor( repository.setAnimateDozingTransitions(animate) } - fun setClockShouldBeCentered(shouldBeCentered: Boolean) { - repository.setClockShouldBeCentered(shouldBeCentered) - } - fun setLastRootViewTapPosition(point: Point?) { repository.lastRootViewTapPosition.value = point } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt index a133f06b3f41..3bdc32dce6f5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt @@ -116,9 +116,10 @@ constructor( * - We're wake and unlocking (fingerprint auth occurred while asleep). * - We're allowed to ignore auth and return to GONE, due to timeouts not elapsing. * - We're DREAMING and dismissible. - * - We're already GONE. Technically you're already awake when GONE, but this makes it easier to - * reason about this state (for example, if canWakeDirectlyToGone, don't tell WM to pause the - * top activity - something you should never do while GONE as well). + * - We're already GONE and not transitioning out of GONE. Technically you're already awake when + * GONE, but this makes it easier to reason about this state (for example, if + * canWakeDirectlyToGone, don't tell WM to pause the top activity - something you should never + * do while GONE as well). */ val canWakeDirectlyToGone = combine( @@ -138,7 +139,8 @@ constructor( canIgnoreAuthAndReturnToGone || (currentState == KeyguardState.DREAMING && keyguardInteractor.isKeyguardDismissible.value) || - currentState == KeyguardState.GONE + (currentState == KeyguardState.GONE && + transitionInteractor.getStartedState() == KeyguardState.GONE) } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt index 542fb9b46bef..3eb8522e0338 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt @@ -23,4 +23,10 @@ data class BlurConfig(val minBlurRadiusPx: Float, val maxBlurRadiusPx: Float) { // No-op config that will be used by dagger of other SysUI variants which don't blur the // background surface. @Inject constructor() : this(0.0f, 0.0f) + + companion object { + // Blur the shade much lesser than the background surface so that the surface is + // distinguishable from the background. + @JvmStatic fun Float.maxBlurRadiusToNotificationPanelBlurRadius(): Float = this / 3.0f + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt index e77e9dd9e9ed..eb1afb406d2b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt @@ -30,6 +30,9 @@ interface PrimaryBouncerTransition { /** Radius of blur applied to the window's root view. */ val windowBlurRadius: Flow<Float> + /** Radius of blur applied to the notifications on expanded shade */ + val notificationBlurRadius: Flow<Float> + fun transitionProgressToBlurRadius( starBlurRadius: Float, endBlurRadius: Float, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt index f17455788d6e..92bb5e6029cb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -23,6 +24,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCE import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -73,7 +75,28 @@ constructor( val lockscreenAlpha: Flow<Float> = if (WindowBlurFlag.isEnabled) alphaFlow else emptyFlow() - val notificationAlpha: Flow<Float> = alphaFlow + val notificationAlpha: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = lockscreenAlpha, + flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f), + ) + } else { + alphaFlow + } + + override val notificationBlurRadius: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = emptyFlow(), + flowWhenShadeIsExpanded = + transitionAnimation.immediatelyTransitionTo( + blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius() + ), + ) + } else { + emptyFlow<Float>() + } override val deviceEntryParentViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt index dbb6a49e7844..e3b55874de6f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt @@ -53,4 +53,7 @@ constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFl override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt index d8b617a60129..c937d5c6453d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt @@ -64,4 +64,6 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio }, onFinish = { blurConfig.maxBlurRadiusPx }, ) + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt index 597df15a2b55..5ab458334a25 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt index c373fd01ba20..44c4c8723dcb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -23,6 +24,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -32,6 +34,7 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow /** * Breaks down LOCKSCREEN->PRIMARY BOUNCER transition into discrete steps for corresponding views to @@ -70,6 +73,29 @@ constructor( val lockscreenAlpha: Flow<Float> = shortcutsAlpha + val notificationAlpha: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = lockscreenAlpha, + flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f), + ) + } else { + lockscreenAlpha + } + + override val notificationBlurRadius: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = emptyFlow(), + flowWhenShadeIsExpanded = + transitionAnimation.immediatelyTransitionTo( + blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius() + ), + ) + } else { + emptyFlow() + } + override val deviceEntryParentViewAlpha: Flow<Float> = shadeDependentFlows.transitionFlow( flowWhenShadeIsNotExpanded = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt index 44598107fa4b..4d3e27265cea 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFl override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt index fab8008cbfa7..224191b64f5f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt @@ -91,4 +91,7 @@ constructor( }, onFinish = { blurConfig.minBlurRadiusPx }, ) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt index eebdf2ef418e..0f8495f34d22 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt @@ -80,4 +80,6 @@ constructor( }, onFinish = { blurConfig.minBlurRadiusPx }, ) + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt index 3636b747d5c9..a13eef2388f7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt @@ -43,4 +43,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt index 4ed3e6cde230..d1233f220f47 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt @@ -166,6 +166,9 @@ constructor( createBouncerWindowBlurFlow(primaryBouncerInteractor::willRunDismissFromKeyguard) } + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) + val scrimAlpha: Flow<ScrimAlpha> = bouncerToGoneFlows.scrimAlpha(TO_GONE_DURATION, PRIMARY_BOUNCER) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt index 2edc93cb5617..c53a408a88e1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt @@ -91,4 +91,7 @@ constructor( }, ), ) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt index 3a54a26858d4..fe1708efea2f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt index 09544827a51a..a6b9442b1270 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt @@ -31,6 +31,7 @@ import androidx.media3.session.CommandButton import androidx.media3.session.MediaController as Media3Controller import androidx.media3.session.SessionCommand import androidx.media3.session.SessionToken +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -128,7 +129,11 @@ constructor( drawable, null, // no action to perform when clicked context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_connecting_status_container) + } else { + context.getDrawable(R.drawable.ic_media_connecting_container) + }, // Specify a rebind id to prevent the spinner from restarting on later binds. com.android.internal.R.drawable.progress_small_material, ) @@ -230,17 +235,33 @@ constructor( Player.COMMAND_PLAY_PAUSE -> { if (!controller.isPlaying) { MediaAction( - context.getDrawable(R.drawable.ic_media_play), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button) + } else { + context.getDrawable(R.drawable.ic_media_play) + }, { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) }, context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button_container) + } else { + context.getDrawable(R.drawable.ic_media_play_container) + }, ) } else { MediaAction( - context.getDrawable(R.drawable.ic_media_pause), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button) + } else { + context.getDrawable(R.drawable.ic_media_pause) + }, { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) }, context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button_container) + } else { + context.getDrawable(R.drawable.ic_media_pause_container) + }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt index 4f9791353b8a..9bf556cf07c2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt @@ -29,6 +29,7 @@ import android.media.session.PlaybackState import android.service.notification.StatusBarNotification import android.util.Log import androidx.media.utils.MediaConstants +import com.android.systemui.Flags import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS import com.android.systemui.media.controls.shared.MediaControlDrawables @@ -69,7 +70,11 @@ fun createActionsFromState( drawable, null, // no action to perform when clicked context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_connecting_status_container) + } else { + context.getDrawable(R.drawable.ic_media_connecting_container) + }, // Specify a rebind id to prevent the spinner from restarting on later binds. com.android.internal.R.drawable.progress_small_material, ) @@ -157,18 +162,34 @@ private fun getStandardAction( return when (action) { PlaybackState.ACTION_PLAY -> { MediaAction( - context.getDrawable(R.drawable.ic_media_play), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button) + } else { + context.getDrawable(R.drawable.ic_media_play) + }, { controller.transportControls.play() }, context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button_container) + } else { + context.getDrawable(R.drawable.ic_media_play_container) + }, ) } PlaybackState.ACTION_PAUSE -> { MediaAction( - context.getDrawable(R.drawable.ic_media_pause), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button) + } else { + context.getDrawable(R.drawable.ic_media_pause) + }, { controller.transportControls.pause() }, context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button_container) + } else { + context.getDrawable(R.drawable.ic_media_pause_container) + }, ) } PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index 3928a711f840..a2ddc20844e7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -1016,9 +1016,24 @@ constructor( expandedLayout.load(context, R.xml.media_recommendations_expanded) } } + readjustPlayPauseWidth() refreshState() } + private fun readjustPlayPauseWidth() { + // TODO: move to xml file when flag is removed. + if (Flags.mediaControlsUiUpdate()) { + collapsedLayout.constrainWidth( + R.id.actionPlayPause, + context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width), + ) + expandedLayout.constrainWidth( + R.id.actionPlayPause, + context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width), + ) + } + } + /** Get a view state based on the width and height set by the scene */ private fun obtainSceneContainerViewState(state: MediaHostState?): TransitionViewState? { logger.logMediaSize("scene container", widthInSceneContainerPx, heightInSceneContainerPx) diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt index b4dca5dbcb39..b6395aabde0d 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt @@ -58,7 +58,6 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>( hostUid, mediaProjectionMetricsLogger, defaultSelectedMode, - dialog, ) } @@ -79,7 +78,7 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>( if (!::viewBinder.isInitialized) { viewBinder = createViewBinder() } - viewBinder.bind() + viewBinder.bind(dialog.requireViewById(R.id.screen_share_permission_dialog)) } private fun updateIcon() { diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionViewBinder.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionViewBinder.kt index d23db7c51482..c6e4db7af2d9 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionViewBinder.kt @@ -16,7 +16,6 @@ package com.android.systemui.mediaprojection.permission -import android.app.AlertDialog import android.content.Context import android.view.LayoutInflater import android.view.View @@ -37,8 +36,8 @@ open class BaseMediaProjectionPermissionViewBinder( private val hostUid: Int, private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger, @ScreenShareMode val defaultSelectedMode: Int = screenShareOptions.first().mode, - private val dialog: AlertDialog, ) : AdapterView.OnItemSelectedListener { + protected lateinit var containerView: View private lateinit var warning: TextView private lateinit var startButton: TextView private lateinit var screenShareModeSpinner: Spinner @@ -54,9 +53,10 @@ open class BaseMediaProjectionPermissionViewBinder( } } - open fun bind() { - warning = dialog.requireViewById(R.id.text_warning) - startButton = dialog.requireViewById(android.R.id.button1) + open fun bind(view: View) { + containerView = view + warning = containerView.requireViewById(R.id.text_warning) + startButton = containerView.requireViewById(android.R.id.button1) initScreenShareOptions() createOptionsView(getOptionsViewLayoutId()) } @@ -67,15 +67,15 @@ open class BaseMediaProjectionPermissionViewBinder( initScreenShareSpinner() } - /** Sets fields on the dialog that change based on which option is selected. */ + /** Sets fields on the views that change based on which option is selected. */ private fun setOptionSpecificFields() { warning.text = warningText startButton.text = startButtonText } private fun initScreenShareSpinner() { - val adapter = OptionsAdapter(dialog.context.applicationContext, screenShareOptions) - screenShareModeSpinner = dialog.requireViewById(R.id.screen_share_mode_options) + val adapter = OptionsAdapter(containerView.context.applicationContext, screenShareOptions) + screenShareModeSpinner = containerView.requireViewById(R.id.screen_share_mode_options) screenShareModeSpinner.adapter = adapter screenShareModeSpinner.onItemSelectedListener = this @@ -103,10 +103,10 @@ open class BaseMediaProjectionPermissionViewBinder( override fun onNothingSelected(parent: AdapterView<*>?) {} private val warningText: String - get() = dialog.context.getString(selectedScreenShareOption.warningText, appName) + get() = containerView.context.getString(selectedScreenShareOption.warningText, appName) private val startButtonText: String - get() = dialog.context.getString(selectedScreenShareOption.startButtonText) + get() = containerView.context.getString(selectedScreenShareOption.startButtonText) fun setStartButtonOnClickListener(listener: View.OnClickListener?) { startButton.setOnClickListener { view -> @@ -121,7 +121,7 @@ open class BaseMediaProjectionPermissionViewBinder( private fun createOptionsView(@LayoutRes layoutId: Int?) { if (layoutId == null) return - val stub = dialog.requireViewById<View>(R.id.options_stub) as ViewStub + val stub = containerView.requireViewById<View>(R.id.options_stub) as ViewStub stub.layoutResource = layoutId stub.inflate() } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java index ec8d30b01eab..e93cec875429 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java @@ -41,12 +41,14 @@ import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.plugins.qs.TileDetailsViewModel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.qs.tiles.dialog.ScreenRecordDetailsViewModel; import com.android.systemui.res.R; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.screenrecord.data.model.ScreenRecordModel; @@ -54,6 +56,8 @@ import com.android.systemui.settings.UserContextProvider; import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; +import java.util.function.Consumer; + import javax.inject.Inject; /** @@ -122,17 +126,78 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> @Override protected void handleClick(@Nullable Expandable expandable) { + handleClick(() -> showDialog(expandable)); + } + + private void showDialog(@Nullable Expandable expandable) { + final Dialog dialog = mController.createScreenRecordDialog( + this::onStartRecordingClicked); + + executeWhenUnlockedKeyguard(() -> { + // We animate from the touched view only if we are not on the keyguard, given that if we + // are we will dismiss it which will also collapse the shade. + boolean shouldAnimateFromExpandable = + expandable != null && !mKeyguardStateController.isShowing(); + + if (shouldAnimateFromExpandable) { + DialogTransitionAnimator.Controller controller = + expandable.dialogTransitionController(new DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG)); + if (controller != null) { + mDialogTransitionAnimator.show(dialog, + controller, /* animateBackgroundBoundsChange= */ true); + } else { + dialog.show(); + } + } else { + dialog.show(); + } + }); + } + + private void onStartRecordingClicked() { + // We dismiss the shade. Since starting the recording will also dismiss the dialog (if + // there is one showing), we disable the exit animation which looks weird when it happens + // at the same time as the shade collapsing. + mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); + mPanelInteractor.collapsePanels(); + } + + private void executeWhenUnlockedKeyguard(Runnable dismissActionCallback) { + ActivityStarter.OnDismissAction dismissAction = () -> { + dismissActionCallback.run(); + + int uid = mUserContextProvider.getUserContext().getUserId(); + mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid); + + return false; + }; + + mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */, + true /* afterKeyguardDone */); + } + + private void handleClick(Runnable showPromptCallback) { if (mController.isStarting()) { cancelCountdown(); } else if (mController.isRecording()) { stopRecording(); } else { - mUiHandler.post(() -> showPrompt(expandable)); + mUiHandler.post(showPromptCallback); } refreshState(); } @Override + public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) { + handleClick(() -> + callback.accept(new ScreenRecordDetailsViewModel()) + ); + return true; + } + + @Override protected void handleUpdateState(BooleanState state, Object arg) { boolean isStarting = mController.isStarting(); boolean isRecording = mController.isRecording(); @@ -178,49 +243,6 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> return mContext.getString(R.string.quick_settings_screen_record_label); } - private void showPrompt(@Nullable Expandable expandable) { - // We animate from the touched view only if we are not on the keyguard, given that if we - // are we will dismiss it which will also collapse the shade. - boolean shouldAnimateFromExpandable = - expandable != null && !mKeyguardStateController.isShowing(); - - // Create the recording dialog that will collapse the shade only if we start the recording. - Runnable onStartRecordingClicked = () -> { - // We dismiss the shade. Since starting the recording will also dismiss the dialog, we - // disable the exit animation which looks weird when it happens at the same time as the - // shade collapsing. - mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); - mPanelInteractor.collapsePanels(); - }; - - final Dialog dialog = mController.createScreenRecordDialog(onStartRecordingClicked); - - ActivityStarter.OnDismissAction dismissAction = () -> { - if (shouldAnimateFromExpandable) { - DialogTransitionAnimator.Controller controller = - expandable.dialogTransitionController(new DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG)); - if (controller != null) { - mDialogTransitionAnimator.show(dialog, - controller, /* animateBackgroundBoundsChange= */ true); - } else { - dialog.show(); - } - } else { - dialog.show(); - } - - int uid = mUserContextProvider.getUserContext().getUserId(); - mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid); - - return false; - }; - - mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */, - true /* afterKeyguardDone */); - } - private void cancelCountdown() { Log.d(TAG, "Cancelling countdown"); mController.cancelCountdown(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java index 23210ef0e688..340cb68a83a4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java @@ -373,7 +373,6 @@ public class InternetDetailsContentController implements AccessPointController.A mConnectivityManager.setAirplaneMode(false); } - @VisibleForTesting protected int getDefaultDataSubscriptionId() { return mSubscriptionManager.getDefaultDataSubscriptionId(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt new file mode 100644 index 000000000000..c64532a2c4ba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt @@ -0,0 +1,991 @@ +/* + * 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.qs.tiles.dialog + +import android.app.AlertDialog +import android.content.Context +import android.content.DialogInterface +import android.graphics.drawable.Drawable +import android.net.Network +import android.net.NetworkCapabilities +import android.os.Handler +import android.telephony.ServiceState +import android.telephony.SignalStrength +import android.telephony.SubscriptionManager +import android.telephony.TelephonyDisplayInfo +import android.text.Html +import android.text.Layout +import android.text.TextUtils +import android.text.method.LinkMovementMethod +import android.util.Log +import android.view.View +import android.view.ViewStub +import android.view.WindowManager +import android.widget.Button +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.Switch +import android.widget.TextView +import androidx.annotation.MainThread +import androidx.annotation.WorkerThread +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.MutableLiveData +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger +import com.android.internal.telephony.flags.Flags +import com.android.settingslib.satellite.SatelliteDialogUtils.TYPE_IS_WIFI +import com.android.settingslib.satellite.SatelliteDialogUtils.mayStartSatelliteWarningDialog +import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils +import com.android.systemui.Prefs +import com.android.systemui.accessibility.floatingmenu.AnnotationLinkSpan +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.flags.QsDetailedView +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.wifitrackerlib.WifiEntry +import com.google.common.annotations.VisibleForTesting +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.concurrent.Executor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job + +/** + * View content for the Internet tile details that handles all UI interactions and state management. + * + * @param internetDialog non-null if the details should be shown as part of a dialog and null + * otherwise. + */ +// TODO: b/377388104 Make this content for details view only. +class InternetDetailsContentManager +@AssistedInject +constructor( + private val internetDetailsContentController: InternetDetailsContentController, + @Assisted(CAN_CONFIG_MOBILE_DATA) private val canConfigMobileData: Boolean, + @Assisted(CAN_CONFIG_WIFI) private val canConfigWifi: Boolean, + @Assisted private val coroutineScope: CoroutineScope, + @Assisted private var context: Context, + @Assisted private var internetDialog: SystemUIDialog?, + private val uiEventLogger: UiEventLogger, + private val dialogTransitionAnimator: DialogTransitionAnimator, + @Main private val handler: Handler, + @Background private val backgroundExecutor: Executor, + private val keyguard: KeyguardStateController, +) { + // Lifecycle + private lateinit var lifecycleRegistry: LifecycleRegistry + @VisibleForTesting internal var lifecycleOwner: LifecycleOwner? = null + @VisibleForTesting internal val internetContentData = MutableLiveData<InternetContent>() + @VisibleForTesting internal var connectedWifiEntry: WifiEntry? = null + @VisibleForTesting internal var isProgressBarVisible = false + + // UI Components + private lateinit var contentView: View + private lateinit var internetDialogTitleView: TextView + private lateinit var internetDialogSubTitleView: TextView + private lateinit var divider: View + private lateinit var progressBar: ProgressBar + private lateinit var ethernetLayout: LinearLayout + private lateinit var mobileNetworkLayout: LinearLayout + private var secondaryMobileNetworkLayout: LinearLayout? = null + private lateinit var turnWifiOnLayout: LinearLayout + private lateinit var wifiToggleTitleTextView: TextView + private lateinit var wifiScanNotifyLayout: LinearLayout + private lateinit var wifiScanNotifyTextView: TextView + private lateinit var connectedWifiListLayout: LinearLayout + private lateinit var connectedWifiIcon: ImageView + private lateinit var connectedWifiTitleTextView: TextView + private lateinit var connectedWifiSummaryTextView: TextView + private lateinit var wifiSettingsIcon: ImageView + private lateinit var wifiRecyclerView: RecyclerView + private lateinit var seeAllLayout: LinearLayout + private lateinit var signalIcon: ImageView + private lateinit var mobileTitleTextView: TextView + private lateinit var mobileSummaryTextView: TextView + private lateinit var airplaneModeSummaryTextView: TextView + private lateinit var mobileDataToggle: Switch + private lateinit var mobileToggleDivider: View + private lateinit var wifiToggle: Switch + private lateinit var shareWifiButton: Button + private lateinit var airplaneModeButton: Button + private var alertDialog: AlertDialog? = null + private lateinit var doneButton: Button + + private val canChangeWifiState = + WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context) + private var wifiNetworkHeight = 0 + private var backgroundOn: Drawable? = null + private var backgroundOff: Drawable? = null + private var clickJob: Job? = null + private var defaultDataSubId = internetDetailsContentController.defaultDataSubscriptionId + @VisibleForTesting + internal var adapter = InternetAdapter(internetDetailsContentController, coroutineScope) + @VisibleForTesting internal var wifiEntriesCount: Int = 0 + @VisibleForTesting internal var hasMoreWifiEntries: Boolean = false + + @AssistedFactory + interface Factory { + fun create( + @Assisted(CAN_CONFIG_MOBILE_DATA) canConfigMobileData: Boolean, + @Assisted(CAN_CONFIG_WIFI) canConfigWifi: Boolean, + coroutineScope: CoroutineScope, + context: Context, + internetDialog: SystemUIDialog?, + ): InternetDetailsContentManager + } + + /** + * Binds the content manager to the provided content view. + * + * This method initializes the lifecycle, views, click listeners, and UI of the details content. + * It also updates the UI with the current Wi-Fi network information. + * + * @param contentView The view to which the content manager should be bound. + */ + fun bind(contentView: View) { + if (DEBUG) { + Log.d(TAG, "Bind InternetDetailsContentManager") + } + + this.contentView = contentView + + initializeLifecycle() + initializeViews() + updateDetailsUI(getStartingInternetContent()) + initializeAndConfigure() + } + + /** + * Initializes the LifecycleRegistry if it hasn't been initialized yet. It sets the initial + * state of the LifecycleRegistry to Lifecycle.State.CREATED. + */ + fun initializeLifecycle() { + if (!::lifecycleRegistry.isInitialized) { + lifecycleOwner = + object : LifecycleOwner { + override val lifecycle: Lifecycle + get() = lifecycleRegistry + } + lifecycleRegistry = LifecycleRegistry(lifecycleOwner!!) + } + lifecycleRegistry.currentState = Lifecycle.State.CREATED + } + + private fun initializeViews() { + // Set accessibility properties + contentView.accessibilityPaneTitle = + context.getText(R.string.accessibility_desc_quick_settings) + + // Get dimension resources + wifiNetworkHeight = + context.resources.getDimensionPixelSize(R.dimen.internet_dialog_wifi_network_height) + + // Initialize LiveData observer + internetContentData.observe(lifecycleOwner!!) { internetContent -> + updateDetailsUI(internetContent) + } + + // Network layouts + internetDialogTitleView = contentView.requireViewById(R.id.internet_dialog_title) + internetDialogSubTitleView = contentView.requireViewById(R.id.internet_dialog_subtitle) + divider = contentView.requireViewById(R.id.divider) + progressBar = contentView.requireViewById(R.id.wifi_searching_progress) + + // Set wifi, mobile and ethernet layouts + setWifiLayout() + setMobileLayout() + ethernetLayout = contentView.requireViewById(R.id.ethernet_layout) + + // Done button is only visible for the dialog view + doneButton = contentView.requireViewById(R.id.done_button) + if (internetDialog == null) { + doneButton.visibility = View.GONE + } else { + // Set done button if qs details view is not enabled. + doneButton.setOnClickListener { internetDialog!!.dismiss() } + } + + // Share WiFi + shareWifiButton = contentView.requireViewById(R.id.share_wifi_button) + shareWifiButton.setOnClickListener { view -> + if ( + internetDetailsContentController.mayLaunchShareWifiSettings( + connectedWifiEntry, + view, + ) + ) { + uiEventLogger.log(InternetDetailsEvent.SHARE_WIFI_QS_BUTTON_CLICKED) + } + } + + // Airplane mode + airplaneModeButton = contentView.requireViewById(R.id.apm_button) + airplaneModeButton.setOnClickListener { + internetDetailsContentController.setAirplaneModeDisabled() + } + airplaneModeSummaryTextView = contentView.requireViewById(R.id.airplane_mode_summary) + + // Background drawables + backgroundOn = context.getDrawable(R.drawable.settingslib_switch_bar_bg_on) + backgroundOff = context.getDrawable(R.drawable.internet_dialog_selected_effect) + } + + private fun setWifiLayout() { + // Initialize Wi-Fi related views + turnWifiOnLayout = contentView.requireViewById(R.id.turn_on_wifi_layout) + wifiToggleTitleTextView = contentView.requireViewById(R.id.wifi_toggle_title) + wifiScanNotifyLayout = contentView.requireViewById(R.id.wifi_scan_notify_layout) + wifiScanNotifyTextView = contentView.requireViewById(R.id.wifi_scan_notify_text) + connectedWifiListLayout = contentView.requireViewById(R.id.wifi_connected_layout) + connectedWifiIcon = contentView.requireViewById(R.id.wifi_connected_icon) + connectedWifiTitleTextView = contentView.requireViewById(R.id.wifi_connected_title) + connectedWifiSummaryTextView = contentView.requireViewById(R.id.wifi_connected_summary) + wifiSettingsIcon = contentView.requireViewById(R.id.wifi_settings_icon) + wifiToggle = contentView.requireViewById(R.id.wifi_toggle) + wifiRecyclerView = + contentView.requireViewById<RecyclerView>(R.id.wifi_list_layout).apply { + layoutManager = LinearLayoutManager(context) + adapter = this@InternetDetailsContentManager.adapter + } + seeAllLayout = contentView.requireViewById(R.id.see_all_layout) + + // Set click listeners for Wi-Fi related views + wifiToggle.setOnClickListener { + val isChecked = wifiToggle.isChecked + handleWifiToggleClicked(isChecked) + } + connectedWifiListLayout.setOnClickListener(this::onClickConnectedWifi) + seeAllLayout.setOnClickListener(this::onClickSeeMoreButton) + } + + private fun setMobileLayout() { + // Initialize mobile data related views + mobileNetworkLayout = contentView.requireViewById(R.id.mobile_network_layout) + signalIcon = contentView.requireViewById(R.id.signal_icon) + mobileTitleTextView = contentView.requireViewById(R.id.mobile_title) + mobileSummaryTextView = contentView.requireViewById(R.id.mobile_summary) + mobileDataToggle = contentView.requireViewById(R.id.mobile_toggle) + mobileToggleDivider = contentView.requireViewById(R.id.mobile_toggle_divider) + + // Set click listeners for mobile data related views + mobileNetworkLayout.setOnClickListener { + val autoSwitchNonDdsSubId: Int = + internetDetailsContentController.getActiveAutoSwitchNonDdsSubId() + if (autoSwitchNonDdsSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + showTurnOffAutoDataSwitchDialog(autoSwitchNonDdsSubId) + } + internetDetailsContentController.connectCarrierNetwork() + } + + // Mobile data toggle + mobileDataToggle.setOnClickListener { + val isChecked = mobileDataToggle.isChecked + if (!isChecked && shouldShowMobileDialog()) { + mobileDataToggle.isChecked = true + showTurnOffMobileDialog() + } else if (internetDetailsContentController.isMobileDataEnabled != isChecked) { + internetDetailsContentController.setMobileDataEnabled( + context, + defaultDataSubId, + isChecked, + false, + ) + } + } + } + + /** + * This function ensures the component is in the RESUMED state and sets up the internet details + * content controller. + * + * If the component is already in the RESUMED state, this function does nothing. + */ + fun initializeAndConfigure() { + // If the current state is RESUMED, it's already initialized. + if (lifecycleRegistry.currentState == Lifecycle.State.RESUMED) { + return + } + + lifecycleRegistry.currentState = Lifecycle.State.RESUMED + internetDetailsContentController.onStart(internetDetailsCallback, canConfigWifi) + if (!canConfigWifi) { + hideWifiViews() + } + } + + private fun getDialogTitleText(): CharSequence { + return internetDetailsContentController.getDialogTitleText() + } + + private fun updateDetailsUI(internetContent: InternetContent) { + if (DEBUG) { + Log.d(TAG, "updateDetailsUI ") + } + if (QsDetailedView.isEnabled) { + internetDialogTitleView.visibility = View.GONE + internetDialogSubTitleView.visibility = View.GONE + } else { + internetDialogTitleView.text = internetContent.internetDialogTitleString + internetDialogSubTitleView.text = internetContent.internetDialogSubTitle + } + airplaneModeButton.visibility = + if (internetContent.isAirplaneModeEnabled) View.VISIBLE else View.GONE + + updateEthernetUI(internetContent) + updateMobileUI(internetContent) + updateWifiUI(internetContent) + } + + private fun getStartingInternetContent(): InternetContent { + return InternetContent( + internetDialogTitleString = getDialogTitleText(), + internetDialogSubTitle = getSubtitleText(), + isWifiEnabled = internetDetailsContentController.isWifiEnabled, + isDeviceLocked = internetDetailsContentController.isDeviceLocked, + ) + } + + private fun getSubtitleText(): String { + return internetDetailsContentController.getSubtitleText(isProgressBarVisible).toString() + } + + @VisibleForTesting + internal fun hideWifiViews() { + setProgressBarVisible(false) + turnWifiOnLayout.visibility = View.GONE + connectedWifiListLayout.visibility = View.GONE + wifiRecyclerView.visibility = View.GONE + seeAllLayout.visibility = View.GONE + shareWifiButton.visibility = View.GONE + } + + private fun setProgressBarVisible(visible: Boolean) { + if (isProgressBarVisible == visible) { + return + } + + // Set the indeterminate value from false to true each time to ensure that the progress bar + // resets its animation and starts at the leftmost starting point each time it is displayed. + isProgressBarVisible = visible + progressBar.visibility = if (visible) View.VISIBLE else View.GONE + progressBar.isIndeterminate = visible + divider.visibility = if (visible) View.GONE else View.VISIBLE + internetDialogSubTitleView.text = getSubtitleText() + } + + private fun showTurnOffAutoDataSwitchDialog(subId: Int) { + var carrierName: CharSequence? = getMobileNetworkTitle(defaultDataSubId) + if (TextUtils.isEmpty(carrierName)) { + carrierName = getDefaultCarrierName() + } + alertDialog = + AlertDialog.Builder(context) + .setTitle(context.getString(R.string.auto_data_switch_disable_title, carrierName)) + .setMessage(R.string.auto_data_switch_disable_message) + .setNegativeButton(R.string.auto_data_switch_dialog_negative_button) { _, _ -> } + .setPositiveButton(R.string.auto_data_switch_dialog_positive_button) { _, _ -> + internetDetailsContentController.setAutoDataSwitchMobileDataPolicy( + subId, + /* enable= */ false, + ) + secondaryMobileNetworkLayout?.visibility = View.GONE + } + .create() + alertDialog!!.window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG) + SystemUIDialog.setShowForAllUsers(alertDialog, true) + SystemUIDialog.registerDismissListener(alertDialog) + SystemUIDialog.setWindowOnTop(alertDialog, keyguard.isShowing()) + if (QsDetailedView.isEnabled) { + alertDialog!!.show() + } else { + dialogTransitionAnimator.showFromDialog(alertDialog!!, internetDialog!!, null, false) + Log.e(TAG, "Internet dialog is shown with the refactor code") + } + } + + private fun shouldShowMobileDialog(): Boolean { + val mobileDataTurnedOff = + Prefs.getBoolean(context, Prefs.Key.QS_HAS_TURNED_OFF_MOBILE_DATA, false) + return internetDetailsContentController.isMobileDataEnabled && !mobileDataTurnedOff + } + + private fun getMobileNetworkTitle(subId: Int): CharSequence { + return internetDetailsContentController.getMobileNetworkTitle(subId) + } + + private fun showTurnOffMobileDialog() { + val context = contentView.context + var carrierName: CharSequence? = getMobileNetworkTitle(defaultDataSubId) + val isInService: Boolean = + internetDetailsContentController.isVoiceStateInService(defaultDataSubId) + if (TextUtils.isEmpty(carrierName) || !isInService) { + carrierName = getDefaultCarrierName() + } + alertDialog = + AlertDialog.Builder(context) + .setTitle(R.string.mobile_data_disable_title) + .setMessage(context.getString(R.string.mobile_data_disable_message, carrierName)) + .setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> } + .setPositiveButton( + com.android.internal.R.string.alert_windows_notification_turn_off_action + ) { _: DialogInterface?, _: Int -> + internetDetailsContentController.setMobileDataEnabled( + context, + defaultDataSubId, + false, + false, + ) + mobileDataToggle.isChecked = false + Prefs.putBoolean(context, Prefs.Key.QS_HAS_TURNED_OFF_MOBILE_DATA, true) + } + .create() + alertDialog!!.window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG) + SystemUIDialog.setShowForAllUsers(alertDialog, true) + SystemUIDialog.registerDismissListener(alertDialog) + SystemUIDialog.setWindowOnTop(alertDialog, keyguard.isShowing()) + if (QsDetailedView.isEnabled) { + alertDialog!!.show() + } else { + dialogTransitionAnimator.showFromDialog(alertDialog!!, internetDialog!!, null, false) + } + } + + private fun onClickConnectedWifi(view: View?) { + if (connectedWifiEntry == null) { + return + } + internetDetailsContentController.launchWifiDetailsSetting(connectedWifiEntry!!.key, view) + } + + private fun onClickSeeMoreButton(view: View?) { + internetDetailsContentController.launchNetworkSetting(view) + } + + private fun handleWifiToggleClicked(isChecked: Boolean) { + if (Flags.oemEnabledSatelliteFlag()) { + if (clickJob != null && !clickJob!!.isCompleted) { + return + } + clickJob = + mayStartSatelliteWarningDialog(contentView.context, coroutineScope, TYPE_IS_WIFI) { + isAllowClick: Boolean -> + if (isAllowClick) { + setWifiEnabled(isChecked) + } else { + wifiToggle.isChecked = !isChecked + } + } + return + } + setWifiEnabled(isChecked) + } + + private fun setWifiEnabled(isEnabled: Boolean) { + if (internetDetailsContentController.isWifiEnabled == isEnabled) { + return + } + internetDetailsContentController.isWifiEnabled = isEnabled + } + + @MainThread + private fun updateEthernetUI(internetContent: InternetContent) { + ethernetLayout.visibility = if (internetContent.hasEthernet) View.VISIBLE else View.GONE + } + + private fun updateWifiUI(internetContent: InternetContent) { + if (!canConfigWifi) { + return + } + + updateWifiToggle(internetContent) + updateConnectedWifi(internetContent) + updateWifiListAndSeeAll(internetContent) + updateWifiScanNotify(internetContent) + } + + private fun updateMobileUI(internetContent: InternetContent) { + if (!internetContent.shouldUpdateMobileNetwork) { + return + } + + val isNetworkConnected = + internetContent.activeNetworkIsCellular || internetContent.isCarrierNetworkActive + // 1. Mobile network should be gone if airplane mode ON or the list of active + // subscriptionId is null. + // 2. Carrier network should be gone if airplane mode ON and Wi-Fi is OFF. + if (DEBUG) { + Log.d( + TAG, + /*msg = */ "updateMobileUI, isCarrierNetworkActive = " + + internetContent.isCarrierNetworkActive, + ) + } + + if ( + !internetContent.hasActiveSubIdOnDds && + (!internetContent.isWifiEnabled || !internetContent.isCarrierNetworkActive) + ) { + mobileNetworkLayout.visibility = View.GONE + secondaryMobileNetworkLayout?.visibility = View.GONE + return + } + + mobileNetworkLayout.visibility = View.VISIBLE + mobileDataToggle.setChecked(internetDetailsContentController.isMobileDataEnabled) + mobileTitleTextView.text = getMobileNetworkTitle(defaultDataSubId) + val summary = getMobileNetworkSummary(defaultDataSubId) + if (!TextUtils.isEmpty(summary)) { + mobileSummaryTextView.text = Html.fromHtml(summary, Html.FROM_HTML_MODE_LEGACY) + mobileSummaryTextView.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE) + mobileSummaryTextView.visibility = View.VISIBLE + } else { + mobileSummaryTextView.visibility = View.GONE + } + backgroundExecutor.execute { + val drawable = getSignalStrengthDrawable(defaultDataSubId) + handler.post { signalIcon.setImageDrawable(drawable) } + } + + mobileDataToggle.visibility = if (canConfigMobileData) View.VISIBLE else View.INVISIBLE + mobileToggleDivider.visibility = if (canConfigMobileData) View.VISIBLE else View.INVISIBLE + val primaryColor = + if (isNetworkConnected) R.color.connected_network_primary_color + else R.color.disconnected_network_primary_color + mobileToggleDivider.setBackgroundColor(context.getColor(primaryColor)) + + // Display the info for the non-DDS if it's actively being used + val autoSwitchNonDdsSubId: Int = internetContent.activeAutoSwitchNonDdsSubId + + val nonDdsVisibility = + if (autoSwitchNonDdsSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) View.VISIBLE + else View.GONE + + val secondaryRes = + if (isNetworkConnected) R.style.TextAppearance_InternetDialog_Secondary_Active + else R.style.TextAppearance_InternetDialog_Secondary + if (nonDdsVisibility == View.VISIBLE) { + // non DDS is the currently active sub, set primary visual for it + setNonDDSActive(autoSwitchNonDdsSubId) + } else { + mobileNetworkLayout.background = if (isNetworkConnected) backgroundOn else backgroundOff + mobileTitleTextView.setTextAppearance( + if (isNetworkConnected) R.style.TextAppearance_InternetDialog_Active + else R.style.TextAppearance_InternetDialog + ) + mobileSummaryTextView.setTextAppearance(secondaryRes) + } + + secondaryMobileNetworkLayout?.visibility = nonDdsVisibility + + // Set airplane mode to the summary for carrier network + if (internetContent.isAirplaneModeEnabled) { + airplaneModeSummaryTextView.apply { + visibility = View.VISIBLE + text = context.getText(R.string.airplane_mode) + setTextAppearance(secondaryRes) + } + } else { + airplaneModeSummaryTextView.visibility = View.GONE + } + } + + private fun setNonDDSActive(autoSwitchNonDdsSubId: Int) { + val stub: ViewStub = contentView.findViewById(R.id.secondary_mobile_network_stub) + stub.inflate() + secondaryMobileNetworkLayout = + contentView.findViewById(R.id.secondary_mobile_network_layout) + secondaryMobileNetworkLayout?.setOnClickListener { view: View? -> + this.onClickConnectedSecondarySub(view) + } + secondaryMobileNetworkLayout?.background = backgroundOn + + contentView.requireViewById<TextView>(R.id.secondary_mobile_title).apply { + text = getMobileNetworkTitle(autoSwitchNonDdsSubId) + setTextAppearance(R.style.TextAppearance_InternetDialog_Active) + } + + val summary = getMobileNetworkSummary(autoSwitchNonDdsSubId) + contentView.requireViewById<TextView>(R.id.secondary_mobile_summary).apply { + if (!TextUtils.isEmpty(summary)) { + text = Html.fromHtml(summary, Html.FROM_HTML_MODE_LEGACY) + breakStrategy = Layout.BREAK_STRATEGY_SIMPLE + setTextAppearance(R.style.TextAppearance_InternetDialog_Active) + } + } + + val secondarySignalIcon: ImageView = contentView.requireViewById(R.id.secondary_signal_icon) + backgroundExecutor.execute { + val drawable = getSignalStrengthDrawable(autoSwitchNonDdsSubId) + handler.post { secondarySignalIcon.setImageDrawable(drawable) } + } + + contentView.requireViewById<ImageView>(R.id.secondary_settings_icon).apply { + setColorFilter(context.getColor(R.color.connected_network_primary_color)) + } + + // set secondary visual for default data sub + mobileNetworkLayout.background = backgroundOff + mobileTitleTextView.setTextAppearance(R.style.TextAppearance_InternetDialog) + mobileSummaryTextView.setTextAppearance(R.style.TextAppearance_InternetDialog_Secondary) + signalIcon.setColorFilter(context.getColor(R.color.connected_network_secondary_color)) + } + + @MainThread + private fun updateWifiToggle(internetContent: InternetContent) { + if (wifiToggle.isChecked != internetContent.isWifiEnabled) { + wifiToggle.isChecked = internetContent.isWifiEnabled + } + if (internetContent.isDeviceLocked) { + wifiToggleTitleTextView.setTextAppearance( + if ((connectedWifiEntry != null)) R.style.TextAppearance_InternetDialog_Active + else R.style.TextAppearance_InternetDialog + ) + } + turnWifiOnLayout.background = + if ((internetContent.isDeviceLocked && connectedWifiEntry != null)) backgroundOn + else null + + if (!canChangeWifiState && wifiToggle.isEnabled) { + wifiToggle.isEnabled = false + wifiToggleTitleTextView.isEnabled = false + contentView.requireViewById<TextView>(R.id.wifi_toggle_summary).apply { + isEnabled = false + visibility = View.VISIBLE + } + } + } + + @MainThread + private fun updateConnectedWifi(internetContent: InternetContent) { + if ( + !internetContent.isWifiEnabled || + connectedWifiEntry == null || + internetContent.isDeviceLocked + ) { + connectedWifiListLayout.visibility = View.GONE + shareWifiButton.visibility = View.GONE + return + } + connectedWifiListLayout.visibility = View.VISIBLE + connectedWifiTitleTextView.text = connectedWifiEntry!!.title + connectedWifiSummaryTextView.text = connectedWifiEntry!!.getSummary(false) + connectedWifiIcon.setImageDrawable( + internetDetailsContentController.getInternetWifiDrawable(connectedWifiEntry!!) + ) + wifiSettingsIcon.setColorFilter(context.getColor(R.color.connected_network_primary_color)) + + val canShareWifi = + internetDetailsContentController.getConfiguratorQrCodeGeneratorIntentOrNull( + connectedWifiEntry + ) != null + shareWifiButton.visibility = if (canShareWifi) View.VISIBLE else View.GONE + + secondaryMobileNetworkLayout?.visibility = View.GONE + } + + @MainThread + private fun updateWifiListAndSeeAll(internetContent: InternetContent) { + if (!internetContent.isWifiEnabled || internetContent.isDeviceLocked) { + wifiRecyclerView.visibility = View.GONE + seeAllLayout.visibility = View.GONE + return + } + val wifiListMaxCount = getWifiListMaxCount() + if (adapter.itemCount > wifiListMaxCount) { + hasMoreWifiEntries = true + } + adapter.setMaxEntriesCount(wifiListMaxCount) + val wifiListMinHeight = wifiNetworkHeight * wifiListMaxCount + if (wifiRecyclerView.minimumHeight != wifiListMinHeight) { + wifiRecyclerView.minimumHeight = wifiListMinHeight + } + wifiRecyclerView.visibility = View.VISIBLE + seeAllLayout.visibility = if (hasMoreWifiEntries) View.VISIBLE else View.INVISIBLE + } + + @MainThread + private fun updateWifiScanNotify(internetContent: InternetContent) { + if ( + internetContent.isWifiEnabled || + !internetContent.isWifiScanEnabled || + internetContent.isDeviceLocked + ) { + wifiScanNotifyLayout.visibility = View.GONE + return + } + + if (TextUtils.isEmpty(wifiScanNotifyTextView.text)) { + val linkInfo = + AnnotationLinkSpan.LinkInfo(AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION) { + view: View? -> + internetDetailsContentController.launchWifiScanningSetting(view) + } + wifiScanNotifyTextView.text = + AnnotationLinkSpan.linkify( + context.getText(R.string.wifi_scan_notify_message), + linkInfo, + ) + wifiScanNotifyTextView.movementMethod = LinkMovementMethod.getInstance() + } + wifiScanNotifyLayout.visibility = View.VISIBLE + } + + @VisibleForTesting + @MainThread + internal fun getWifiListMaxCount(): Int { + // Use the maximum count of networks to calculate the remaining count for Wi-Fi networks. + var count = MAX_NETWORK_COUNT + if (ethernetLayout.visibility == View.VISIBLE) { + count -= 1 + } + if (mobileNetworkLayout.visibility == View.VISIBLE) { + count -= 1 + } + + // If the remaining count is greater than the maximum count of the Wi-Fi network, the + // maximum count of the Wi-Fi network is used. + if (count > InternetDetailsContentController.MAX_WIFI_ENTRY_COUNT) { + count = InternetDetailsContentController.MAX_WIFI_ENTRY_COUNT + } + if (connectedWifiListLayout.visibility == View.VISIBLE) { + count -= 1 + } + return count + } + + private fun getMobileNetworkSummary(subId: Int): String { + return internetDetailsContentController.getMobileNetworkSummary(subId) + } + + /** For DSDS auto data switch */ + private fun onClickConnectedSecondarySub(view: View?) { + internetDetailsContentController.launchMobileNetworkSettings(view) + } + + private fun getSignalStrengthDrawable(subId: Int): Drawable { + return internetDetailsContentController.getSignalStrengthDrawable(subId) + } + + /** + * Unbinds all listeners and resources associated with the view. This method should be called + * when the view is no longer needed. + */ + fun unBind() { + if (DEBUG) { + Log.d(TAG, "unBind") + } + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + mobileNetworkLayout.setOnClickListener(null) + connectedWifiListLayout.setOnClickListener(null) + secondaryMobileNetworkLayout?.setOnClickListener(null) + seeAllLayout.setOnClickListener(null) + wifiToggle.setOnCheckedChangeListener(null) + doneButton.setOnClickListener(null) + shareWifiButton.setOnClickListener(null) + airplaneModeButton.setOnClickListener(null) + internetDetailsContentController.onStop() + } + + /** + * Update the internet details content when receiving the callback. + * + * @param shouldUpdateMobileNetwork `true` for update the mobile network layout, otherwise + * `false`. + */ + @VisibleForTesting + internal fun updateContent(shouldUpdateMobileNetwork: Boolean) { + backgroundExecutor.execute { + internetContentData.postValue(getInternetContent(shouldUpdateMobileNetwork)) + } + } + + private fun getInternetContent(shouldUpdateMobileNetwork: Boolean): InternetContent { + return InternetContent( + shouldUpdateMobileNetwork = shouldUpdateMobileNetwork, + internetDialogTitleString = getDialogTitleText(), + internetDialogSubTitle = getSubtitleText(), + activeNetworkIsCellular = + if (shouldUpdateMobileNetwork) + internetDetailsContentController.activeNetworkIsCellular() + else false, + isCarrierNetworkActive = + if (shouldUpdateMobileNetwork) + internetDetailsContentController.isCarrierNetworkActive() + else false, + isAirplaneModeEnabled = internetDetailsContentController.isAirplaneModeEnabled, + hasEthernet = internetDetailsContentController.hasEthernet(), + isWifiEnabled = internetDetailsContentController.isWifiEnabled, + hasActiveSubIdOnDds = internetDetailsContentController.hasActiveSubIdOnDds(), + isDeviceLocked = internetDetailsContentController.isDeviceLocked, + isWifiScanEnabled = internetDetailsContentController.isWifiScanEnabled(), + activeAutoSwitchNonDdsSubId = + internetDetailsContentController.getActiveAutoSwitchNonDdsSubId(), + ) + } + + /** + * Handles window focus changes. If the activity loses focus and the system UI dialog is + * showing, it dismisses the current alert dialog to prevent it from persisting in the + * background. + * + * @param dialog The internet system UI dialog whose focus state has changed. + * @param hasFocus True if the window has gained focus, false otherwise. + */ + fun onWindowFocusChanged(dialog: SystemUIDialog, hasFocus: Boolean) { + if (alertDialog != null && !alertDialog!!.isShowing) { + if (!hasFocus && dialog.isShowing) { + dialog.dismiss() + } + } + } + + private fun getDefaultCarrierName(): String? { + return context.getString(R.string.mobile_data_disable_message_default_carrier) + } + + @VisibleForTesting + internal val internetDetailsCallback = + object : InternetDetailsContentController.InternetDialogCallback { + override fun onRefreshCarrierInfo() { + updateContent(shouldUpdateMobileNetwork = true) + } + + override fun onSimStateChanged() { + updateContent(shouldUpdateMobileNetwork = true) + } + + @WorkerThread + override fun onCapabilitiesChanged( + network: Network?, + networkCapabilities: NetworkCapabilities?, + ) { + updateContent(shouldUpdateMobileNetwork = true) + } + + @WorkerThread + override fun onLost(network: Network) { + updateContent(shouldUpdateMobileNetwork = true) + } + + override fun onSubscriptionsChanged(dataSubId: Int) { + defaultDataSubId = dataSubId + updateContent(shouldUpdateMobileNetwork = true) + } + + override fun onServiceStateChanged(serviceState: ServiceState?) { + updateContent(shouldUpdateMobileNetwork = true) + } + + @WorkerThread + override fun onDataConnectionStateChanged(state: Int, networkType: Int) { + updateContent(shouldUpdateMobileNetwork = true) + } + + override fun onSignalStrengthsChanged(signalStrength: SignalStrength?) { + updateContent(shouldUpdateMobileNetwork = true) + } + + override fun onUserMobileDataStateChanged(enabled: Boolean) { + updateContent(shouldUpdateMobileNetwork = true) + } + + override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo?) { + updateContent(shouldUpdateMobileNetwork = true) + } + + override fun onCarrierNetworkChange(active: Boolean) { + updateContent(shouldUpdateMobileNetwork = true) + } + + override fun dismissDialog() { + if (DEBUG) { + Log.d(TAG, "dismissDialog") + } + if (internetDialog != null) { + internetDialog!!.dismiss() + internetDialog = null + } + } + + override fun onAccessPointsChanged( + wifiEntries: MutableList<WifiEntry>?, + connectedEntry: WifiEntry?, + ifHasMoreWifiEntries: Boolean, + ) { + // Should update the carrier network layout when it is connected under airplane + // mode ON. + val shouldUpdateCarrierNetwork = + (mobileNetworkLayout.visibility == View.VISIBLE) && + internetDetailsContentController.isAirplaneModeEnabled + handler.post { + connectedWifiEntry = connectedEntry + wifiEntriesCount = wifiEntries?.size ?: 0 + hasMoreWifiEntries = ifHasMoreWifiEntries + updateContent(shouldUpdateCarrierNetwork) + adapter.setWifiEntries(wifiEntries, wifiEntriesCount) + adapter.notifyDataSetChanged() + } + } + + override fun onWifiScan(isScan: Boolean) { + setProgressBarVisible(isScan) + } + } + + enum class InternetDetailsEvent(private val id: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "The Internet details became visible on the screen.") + INTERNET_DETAILS_VISIBLE(2071), + @UiEvent(doc = "The share wifi button is clicked.") SHARE_WIFI_QS_BUTTON_CLICKED(1462); + + override fun getId(): Int { + return id + } + } + + @VisibleForTesting + data class InternetContent( + val internetDialogTitleString: CharSequence, + val internetDialogSubTitle: CharSequence, + val isAirplaneModeEnabled: Boolean = false, + val hasEthernet: Boolean = false, + val shouldUpdateMobileNetwork: Boolean = false, + val activeNetworkIsCellular: Boolean = false, + val isCarrierNetworkActive: Boolean = false, + val isWifiEnabled: Boolean = false, + val hasActiveSubIdOnDds: Boolean = false, + val isDeviceLocked: Boolean = false, + val isWifiScanEnabled: Boolean = false, + val activeAutoSwitchNonDdsSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID, + ) + + companion object { + private const val TAG = "InternetDetailsContent" + private val DEBUG: Boolean = Log.isLoggable(TAG, Log.DEBUG) + private const val MAX_NETWORK_COUNT = 4 + const val CAN_CONFIG_MOBILE_DATA = "can_config_mobile_data" + const val CAN_CONFIG_WIFI = "can_config_wifi" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailedViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt index f239a179d79a..f239a179d79a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailedViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java index ee53471253af..a418b2a71f50 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java @@ -70,6 +70,7 @@ import com.android.systemui.accessibility.floatingmenu.AnnotationLinkSpan; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.qs.flags.QsDetailedView; import com.android.systemui.res.R; import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor; @@ -207,7 +208,8 @@ public class InternetDialogDelegateLegacy implements KeyguardStateController keyguardStateController, SystemUIDialog.Factory systemUIDialogFactory, ShadeDialogContextInteractor shadeDialogContextInteractor) { - // TODO: b/377388104 QsDetailedView.assertInLegacyMode(); + // If `QsDetailedView` is enabled, it should show the details view. + QsDetailedView.assertInLegacyMode(); mAboveStatusBar = aboveStatusBar; mSystemUIDialogFactory = systemUIDialogFactory; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt index 8a54648f4541..5f82e60b63ec 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt @@ -23,6 +23,7 @@ import com.android.systemui.animation.Expandable import com.android.systemui.coroutines.newTracingContext import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.statusbar.phone.SystemUIDialog import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -42,21 +43,24 @@ constructor( @Background private val bgDispatcher: CoroutineDispatcher, ) { private lateinit var coroutineScope: CoroutineScope + companion object { private const val INTERACTION_JANK_TAG = "internet" var dialog: SystemUIDialog? = null } /** - * Creates a [InternetDialogDelegateLegacy]. The dialog will be animated from [expandable] if - * it is not null. + * Creates a [InternetDialogDelegateLegacy]. The dialog will be animated from [expandable] if it + * is not null. */ fun create( aboveStatusBar: Boolean, canConfigMobileData: Boolean, canConfigWifi: Boolean, - expandable: Expandable? + expandable: Expandable?, ) { + // If `QsDetailedView` is enabled, it should show the details view. + QsDetailedView.assertInLegacyMode() if (dialog != null) { if (DEBUG) { Log.d(TAG, "InternetDialog is showing, do not create it twice.") @@ -64,11 +68,11 @@ constructor( return } else { coroutineScope = CoroutineScope(bgDispatcher + newTracingContext("InternetDialogScope")) - // TODO: b/377388104 check the QsDetailedView flag to use the correct dialogFactory dialog = dialogFactory .create(aboveStatusBar, canConfigMobileData, canConfigWifi, coroutineScope) .createDialog() + val controller = expandable?.dialogTransitionController( DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) @@ -77,10 +81,9 @@ constructor( dialogTransitionAnimator.show( dialog!!, controller, - animateBackgroundBoundsChange = true + animateBackgroundBoundsChange = true, ) - } - ?: dialog?.show() + } ?: dialog?.show() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt new file mode 100644 index 000000000000..42cb1248ccff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.dialog + +import android.view.LayoutInflater +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.plugins.qs.TileDetailsViewModel +import com.android.systemui.res.R + +/** The view model used for the screen record details view in the Quick Settings */ +class ScreenRecordDetailsViewModel() : TileDetailsViewModel() { + @Composable + override fun GetContentView() { + // TODO(b/378514312): Finish implementing this function. + AndroidView( + modifier = Modifier.fillMaxWidth().heightIn(max = VIEW_MAX_HEIGHT), + factory = { context -> + // Inflate with the existing dialog xml layout + LayoutInflater.from(context).inflate(R.layout.screen_share_dialog, null) + }, + ) + } + + override fun clickOnSettingsButton() { + // No settings button in this tile. + } + + override fun getTitle(): String { + // TODO(b/388321032): Replace this string with a string in a translatable xml file, + return "Screen recording" + } + + override fun getSubTitle(): String { + // No sub-title in this tile. + return "" + } + + companion object { + private val VIEW_MAX_HEIGHT: Dp = 320.dp + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index c34edc81bfe7..30d1f05771d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -118,7 +118,8 @@ constructor( override fun addCallback(callback: QSTile.Callback?) { callback ?: return callbacks.add(callback) - state?.let(callback::onStateChanged) + state.copyTo(cachedState) + state.let(callback::onStateChanged) } override fun removeCallback(callback: QSTile.Callback?) { @@ -212,9 +213,9 @@ constructor( qsTileViewModel.destroy() } - override fun getState(): QSTile.State = + override fun getState(): QSTile.AdapterState = qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) } - ?: QSTile.State() + ?: QSTile.AdapterState() override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId @@ -241,7 +242,7 @@ constructor( context: Context, viewModelState: QSTileState, config: QSTileConfig, - ): QSTile.State = + ): QSTile.AdapterState = // we have to use QSTile.BooleanState to support different side icons // which are bound to instanceof QSTile.BooleanState in QSTileView. QSTile.AdapterState().apply { diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index d60f05e685bb..0488962fd076 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -90,22 +90,15 @@ constructor( initialValue = defaultTransitionState, ) - fun changeScene( - toScene: SceneKey, - transitionKey: TransitionKey? = null, - ) { - dataSource.changeScene( - toScene = toScene, - transitionKey = transitionKey, - ) + /** Number of currently active transition animations. */ + val activeTransitionAnimationCount = MutableStateFlow(0) + + fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) { + dataSource.changeScene(toScene = toScene, transitionKey = transitionKey) } - fun snapToScene( - toScene: SceneKey, - ) { - dataSource.snapToScene( - toScene = toScene, - ) + fun snapToScene(toScene: SceneKey) { + dataSource.snapToScene(toScene = toScene) } /** @@ -116,10 +109,7 @@ constructor( * [overlay] is already shown. */ fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.showOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + dataSource.showOverlay(overlay = overlay, transitionKey = transitionKey) } /** @@ -130,10 +120,7 @@ constructor( * if [overlay] is already hidden. */ fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.hideOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + dataSource.hideOverlay(overlay = overlay, transitionKey = transitionKey) } /** @@ -143,11 +130,7 @@ constructor( * This throws if [from] is not currently shown or if [to] is already shown. */ fun replaceOverlay(from: OverlayKey, to: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.replaceOverlay( - from = from, - to = to, - transitionKey = transitionKey, - ) + dataSource.replaceOverlay(from = from, to = to, transitionKey = transitionKey) } /** Sets whether the container is visible. */ diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 0e6fc36fb96a..ba9dc7625769 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update /** * Generic business logic and app state accessors for the scene framework. @@ -165,12 +166,15 @@ constructor( /** Whether the scene container is visible. */ val isVisible: StateFlow<Boolean> = - combine(repository.isVisible, repository.isRemoteUserInputOngoing) { - isVisible, - isRemoteUserInteractionOngoing -> + combine( + repository.isVisible, + repository.isRemoteUserInputOngoing, + repository.activeTransitionAnimationCount, + ) { isVisible, isRemoteUserInteractionOngoing, activeTransitionAnimationCount -> isVisibleInternal( raw = isVisible, isRemoteUserInputOngoing = isRemoteUserInteractionOngoing, + activeTransitionAnimationCount = activeTransitionAnimationCount, ) } .stateIn( @@ -436,8 +440,9 @@ constructor( private fun isVisibleInternal( raw: Boolean = repository.isVisible.value, isRemoteUserInputOngoing: Boolean = repository.isRemoteUserInputOngoing.value, + activeTransitionAnimationCount: Int = repository.activeTransitionAnimationCount.value, ): Boolean { - return raw || isRemoteUserInputOngoing + return raw || isRemoteUserInputOngoing || activeTransitionAnimationCount > 0 } /** @@ -525,4 +530,50 @@ constructor( ): Flow<Map<UserAction, UserActionResult>> { return disabledContentInteractor.filteredUserActions(unfiltered) } + + /** + * Notifies that a transition animation has started. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationStart() { + repository.activeTransitionAnimationCount.update { current -> + (current + 1).also { + check(it < 10) { + "Number of active transition animations is too high. Something must be" + + " calling onTransitionAnimationStart too many times!" + } + } + } + } + + /** + * Notifies that a transition animation has ended. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationEnd() { + decrementActiveTransitionAnimationCount() + } + + /** + * Notifies that a transition animation has been canceled. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationCancelled() { + decrementActiveTransitionAnimationCount() + } + + private fun decrementActiveTransitionAnimationCount() { + repository.activeTransitionAnimationCount.update { current -> + (current - 1).also { + check(it >= 0) { + "Number of active transition animations is negative. Something must be" + + " calling onTransitionAnimationEnd or onTransitionAnimationCancelled too" + + " many times!" + } + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 8d8c24eae9e2..3a23a71cf7bf 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -25,6 +25,7 @@ import com.android.internal.logging.UiEventLogger import com.android.keyguard.AuthInteractionProperties import com.android.systemui.CoreStartable import com.android.systemui.Flags +import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor @@ -146,6 +147,7 @@ constructor( private val vibratorHelper: VibratorHelper, private val msdlPlayer: MSDLPlayer, private val disabledContentInteractor: DisabledContentInteractor, + private val activityTransitionAnimator: ActivityTransitionAnimator, ) : CoreStartable { private val centralSurfaces: CentralSurfaces? get() = centralSurfacesOptLazy.get().getOrNull() @@ -169,6 +171,7 @@ constructor( handleKeyguardEnabledness() notifyKeyguardDismissCancelledCallbacks() refreshLockscreenEnabled() + hydrateActivityTransitionAnimationState() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -929,6 +932,35 @@ constructor( } } + /** + * Wires the scene framework to activity transition animations that originate from anywhere. A + * subset of these may actually originate from UI inside one of the scenes in the framework. + * + * Telling the scene framework about ongoing activity transition animations is critical so the + * scene framework doesn't make its scene container invisible during a transition. + * + * As it turns out, making the scene container view invisible during a transition animation + * disrupts the animation and causes interaction jank CUJ tracking to ignore reports of the CUJ + * ending or being canceled. + */ + private fun hydrateActivityTransitionAnimationState() { + activityTransitionAnimator.addListener( + object : ActivityTransitionAnimator.Listener { + override fun onTransitionAnimationStart() { + sceneInteractor.onTransitionAnimationStart() + } + + override fun onTransitionAnimationEnd() { + sceneInteractor.onTransitionAnimationEnd() + } + + override fun onTransitionAnimationCancelled() { + sceneInteractor.onTransitionAnimationCancelled() + } + } + ) + } + companion object { private const val TAG = "SceneContainerStartable" } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneJankMonitor.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneJankMonitor.kt new file mode 100644 index 000000000000..48a49c60d8a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneJankMonitor.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2025 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.scene.ui.view + +import android.view.View +import androidx.compose.runtime.getValue +import com.android.compose.animation.scene.ContentKey +import com.android.internal.jank.Cuj +import com.android.internal.jank.Cuj.CujType +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator +import com.android.systemui.scene.shared.model.Scenes +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Monitors scene transitions and reports the beginning and ending of each scene-related CUJ. + * + * This general-purpose monitor can be expanded to include other rules that respond to the beginning + * and/or ending of transitions and reports jank CUI markers to the [InteractionJankMonitor]. + */ +class SceneJankMonitor +@AssistedInject +constructor( + authenticationInteractor: AuthenticationInteractor, + private val deviceUnlockedInteractor: DeviceUnlockedInteractor, + private val interactionJankMonitor: InteractionJankMonitor, +) : ExclusiveActivatable() { + + private val hydrator = Hydrator("SceneJankMonitor.hydrator") + private val authMethod: AuthenticationMethodModel? by + hydrator.hydratedStateOf( + traceName = "authMethod", + initialValue = null, + source = authenticationInteractor.authenticationMethod, + ) + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } + + /** + * Notifies that a transition is at its start. + * + * Should be called exactly once each time a new transition starts. + */ + fun onTransitionStart(view: View, from: ContentKey, to: ContentKey, @CujType cuj: Int?) { + cuj.orCalculated(from, to) { nonNullCuj -> interactionJankMonitor.begin(view, nonNullCuj) } + } + + /** + * Notifies that the previous transition is at its end. + * + * Should be called exactly once each time a transition ends. + */ + fun onTransitionEnd(from: ContentKey, to: ContentKey, @CujType cuj: Int?) { + cuj.orCalculated(from, to) { nonNullCuj -> interactionJankMonitor.end(nonNullCuj) } + } + + /** + * Returns this CUI marker (CUJ identifier), one that's calculated based on other state, or + * `null`, if no appropriate CUJ could be calculated. + */ + private fun Int?.orCalculated( + from: ContentKey, + to: ContentKey, + ifNotNull: (nonNullCuj: Int) -> Unit, + ) { + val thisOrCalculatedCuj = this ?: calculatedCuj(from = from, to = to) + + if (thisOrCalculatedCuj != null) { + ifNotNull(thisOrCalculatedCuj) + } + } + + @CujType + private fun calculatedCuj(from: ContentKey, to: ContentKey): Int? { + val isDeviceUnlocked = deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked + return when { + to == Scenes.Bouncer -> + when (authMethod) { + is AuthenticationMethodModel.Pin, + is AuthenticationMethodModel.Sim -> Cuj.CUJ_LOCKSCREEN_PIN_APPEAR + is AuthenticationMethodModel.Pattern -> Cuj.CUJ_LOCKSCREEN_PATTERN_APPEAR + is AuthenticationMethodModel.Password -> Cuj.CUJ_LOCKSCREEN_PASSWORD_APPEAR + is AuthenticationMethodModel.None -> null + null -> null + } + from == Scenes.Bouncer && isDeviceUnlocked -> + when (authMethod) { + is AuthenticationMethodModel.Pin, + is AuthenticationMethodModel.Sim -> Cuj.CUJ_LOCKSCREEN_PIN_DISAPPEAR + is AuthenticationMethodModel.Pattern -> Cuj.CUJ_LOCKSCREEN_PATTERN_DISAPPEAR + is AuthenticationMethodModel.Password -> Cuj.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR + is AuthenticationMethodModel.None -> null + null -> null + } + else -> null + } + } + + @AssistedFactory + interface Factory { + fun create(): SceneJankMonitor + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt index c45906840385..b8da2274eec1 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt @@ -34,6 +34,7 @@ class SceneWindowRootView(context: Context, attrs: AttributeSet?) : WindowRootVi layoutInsetController: LayoutInsetsController, sceneDataSourceDelegator: SceneDataSourceDelegator, qsSceneAdapter: Provider<QSSceneAdapter>, + sceneJankMonitorFactory: SceneJankMonitor.Factory, ) { setLayoutInsetsController(layoutInsetController) SceneWindowRootViewBinder.bind( @@ -52,6 +53,7 @@ class SceneWindowRootView(context: Context, attrs: AttributeSet?) : WindowRootVi }, dataSourceDelegator = sceneDataSourceDelegator, qsSceneAdapter = qsSceneAdapter, + sceneJankMonitorFactory = sceneJankMonitorFactory, ) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index f7061d9af961..7da007c2fe53 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -74,6 +74,7 @@ object SceneWindowRootViewBinder { onVisibilityChangedInternal: (isVisible: Boolean) -> Unit, dataSourceDelegator: SceneDataSourceDelegator, qsSceneAdapter: Provider<QSSceneAdapter>, + sceneJankMonitorFactory: SceneJankMonitor.Factory, ) { val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key } val sortedSceneByKey: Map<SceneKey, Scene> = @@ -133,6 +134,7 @@ object SceneWindowRootViewBinder { dataSourceDelegator = dataSourceDelegator, qsSceneAdapter = qsSceneAdapter, containerConfig = containerConfig, + sceneJankMonitorFactory = sceneJankMonitorFactory, ) .also { it.id = R.id.scene_container_root_composable } ) @@ -169,6 +171,7 @@ object SceneWindowRootViewBinder { dataSourceDelegator: SceneDataSourceDelegator, qsSceneAdapter: Provider<QSSceneAdapter>, containerConfig: SceneContainerConfig, + sceneJankMonitorFactory: SceneJankMonitor.Factory, ): View { return ComposeView(context).apply { setContent { @@ -185,6 +188,7 @@ object SceneWindowRootViewBinder { sceneTransitions = containerConfig.transitions, dataSourceDelegator = dataSourceDelegator, qsSceneAdapter = qsSceneAdapter, + sceneJankMonitorFactory = sceneJankMonitorFactory, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt index f295c0ccb3de..7ec523bc4dc5 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt @@ -104,7 +104,6 @@ class ScreenRecordPermissionDialogDelegate( mediaProjectionMetricsLogger, defaultSelectedMode, displayManager, - dialog, controller, activityStarter, userContextProvider, diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt index 691bdd4a1b27..9fcb3dfc0ad3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt @@ -18,7 +18,6 @@ package com.android.systemui.screenrecord import android.annotation.SuppressLint import android.app.Activity -import android.app.AlertDialog import android.app.PendingIntent import android.content.Intent import android.hardware.display.DisplayManager @@ -57,7 +56,6 @@ class ScreenRecordPermissionViewBinder( mediaProjectionMetricsLogger: MediaProjectionMetricsLogger, @ScreenShareMode defaultSelectedMode: Int, displayManager: DisplayManager, - private val dialog: AlertDialog, private val controller: RecordingController, private val activityStarter: ActivityStarter, private val userContextProvider: UserContextProvider, @@ -69,15 +67,14 @@ class ScreenRecordPermissionViewBinder( hostUid = hostUid, mediaProjectionMetricsLogger, defaultSelectedMode, - dialog, ) { private lateinit var tapsSwitch: Switch private lateinit var audioSwitch: Switch private lateinit var tapsView: View private lateinit var options: Spinner - override fun bind() { - super.bind() + override fun bind(view: View) { + super.bind(view) initRecordOptionsView() setStartButtonOnClickListener { startButtonOnClicked() } } @@ -91,7 +88,8 @@ class ScreenRecordPermissionViewBinder( ) } if (selectedScreenShareOption.mode == SINGLE_APP) { - val intent = Intent(dialog.context, MediaProjectionAppSelectorActivity::class.java) + val intent = + Intent(containerView.context, MediaProjectionAppSelectorActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) // We can't start activity for result here so we use result receiver to get @@ -116,10 +114,10 @@ class ScreenRecordPermissionViewBinder( @SuppressLint("ClickableViewAccessibility") private fun initRecordOptionsView() { - audioSwitch = dialog.requireViewById(R.id.screenrecord_audio_switch) - tapsSwitch = dialog.requireViewById(R.id.screenrecord_taps_switch) + audioSwitch = containerView.requireViewById(R.id.screenrecord_audio_switch) + tapsSwitch = containerView.requireViewById(R.id.screenrecord_taps_switch) - tapsView = dialog.requireViewById(R.id.show_taps) + tapsView = containerView.requireViewById(R.id.show_taps) updateTapsViewVisibility() // Add these listeners so that the switch only responds to movement @@ -127,10 +125,10 @@ class ScreenRecordPermissionViewBinder( audioSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE } tapsSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE } - options = dialog.requireViewById(R.id.screen_recording_options) + options = containerView.requireViewById(R.id.screen_recording_options) val a: ArrayAdapter<*> = ScreenRecordingAdapter( - dialog.context, + containerView.context, android.R.layout.simple_spinner_dropdown_item, MODES, ) diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 42d83637ec1a..a48d4d4d3b5f 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -316,7 +316,7 @@ internal constructor( val callback = it.callback.get() if (callback != null) { it.executor.execute { - traceSection({ "$callback" }) { action(callback) { latch.countDown() } } + traceSection({ "UserTrackerImpl::$callback" }) { action(callback) { latch.countDown() } } } } else { latch.countDown() diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index e168025b2bf8..c4306d3f7530 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -49,7 +49,6 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.ContentResolver; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Insets; @@ -58,28 +57,23 @@ import android.graphics.Region; import android.graphics.RenderEffect; import android.graphics.Shader; import android.os.Bundle; -import android.os.Handler; import android.os.Trace; -import android.os.UserManager; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; import android.view.HapticFeedbackConstants; import android.view.InputDevice; -import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.View.AccessibilityDelegate; import android.view.ViewConfiguration; import android.view.ViewPropertyAnimator; -import android.view.ViewStub; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; -import android.view.animation.Interpolator; import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; @@ -105,19 +99,17 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInt import com.android.systemui.doze.DozeLog; import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.DumpsysTableLogger; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; -import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver; import com.android.systemui.keyguard.shared.model.ClockSize; import com.android.systemui.keyguard.shared.model.Edge; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder; +import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; @@ -162,7 +154,6 @@ import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.ViewGroupFadeHelper; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor; -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper; import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener; @@ -174,10 +165,8 @@ import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor; -import com.android.systemui.statusbar.phone.BounceInterpolator; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; @@ -254,7 +243,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump * Whether the Shade should animate to reflect Back gesture progress. * To minimize latency at runtime, we cache this, else we'd be reading it every time * updateQsExpansion() is called... and it's called very often. - * + * <p> * Whenever we change this flag, SysUI is restarted, so it's never going to be "stale". */ @@ -285,8 +274,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final ConfigurationController mConfigurationController; private final Provider<FlingAnimationUtils.Builder> mFlingAnimationUtilsBuilder; private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController; - private final LayoutInflater mLayoutInflater; - private final FeatureFlags mFeatureFlags; private final AccessibilityManager mAccessibilityManager; private final NotificationWakeUpCoordinator mWakeUpCoordinator; private final PulseExpansionHandler mPulseExpansionHandler; @@ -311,7 +298,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final DozeLog mDozeLog; /** Whether or not the NotificationPanelView can be expanded or collapsed with a drag. */ private final boolean mNotificationsDragEnabled; - private final Interpolator mBounceInterpolator; private final NotificationShadeWindowController mNotificationShadeWindowController; private final ShadeExpansionStateManager mShadeExpansionStateManager; private final ShadeRepository mShadeRepository; @@ -321,7 +307,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final NotificationGutsManager mGutsManager; private final AlternateBouncerInteractor mAlternateBouncerInteractor; private final QuickSettingsControllerImpl mQsController; - private final NaturalScrollingSettingObserver mNaturalScrollingSettingObserver; private final TouchHandler mTouchHandler = new TouchHandler(); private long mDownTime; @@ -436,7 +421,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mPanelAlphaAnimator.getProperty(), Interpolators.ALPHA_IN); private final CommandQueue mCommandQueue; - private final UserManager mUserManager; private final MediaDataManager mMediaDataManager; @PanelState private int mCurrentPanelState = STATE_CLOSED; @@ -462,7 +446,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private boolean mIsGestureNavigation; private int mOldLayoutDirection; - private final ContentResolver mContentResolver; private float mMinFraction; private final KeyguardMediaController mKeyguardMediaController; @@ -475,7 +458,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private int mSplitShadeScrimTransitionDistance; private final NotificationListContainer mNotificationListContainer; - private final NotificationStackSizeCalculator mNotificationStackSizeCalculator; private final NPVCDownEventState.Buffer mLastDownEvents; private final KeyguardClockInteractor mKeyguardClockInteractor; private float mMinExpandHeight; @@ -530,8 +512,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final KeyguardInteractor mKeyguardInteractor; private final PowerInteractor mPowerInteractor; private final CoroutineDispatcher mMainDispatcher; - private boolean mIsAnyMultiShadeExpanded; - private boolean mForceFlingAnimationForTest = false; private final SplitShadeStateController mSplitShadeStateController; private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */, mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */); @@ -550,9 +530,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Inject public NotificationPanelViewController(NotificationPanelView view, - @Main Handler handler, - @ShadeDisplayAware LayoutInflater layoutInflater, - FeatureFlags featureFlags, NotificationWakeUpCoordinator coordinator, PulseExpansionHandler pulseExpansionHandler, DynamicPrivacyController dynamicPrivacyController, @@ -584,7 +561,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump KeyguardStatusBarViewComponent.Factory keyguardStatusBarViewComponentFactory, LockscreenShadeTransitionController lockscreenShadeTransitionController, ScrimController scrimController, - UserManager userManager, MediaDataManager mediaDataManager, NotificationShadeDepthController notificationShadeDepthController, AmbientState ambientState, @@ -595,7 +571,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump QuickSettingsControllerImpl quickSettingsController, FragmentService fragmentService, IStatusBarService statusBarService, - ContentResolver contentResolver, ShadeHeaderController shadeHeaderController, ScreenOffAnimationController screenOffAnimationController, LockscreenGestureLogger lockscreenGestureLogger, @@ -606,7 +581,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump KeyguardUnlockAnimationController keyguardUnlockAnimationController, KeyguardIndicationController keyguardIndicationController, NotificationListContainer notificationListContainer, - NotificationStackSizeCalculator notificationStackSizeCalculator, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, SystemClock systemClock, KeyguardClockInteractor keyguardClockInteractor, @@ -625,7 +599,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump SplitShadeStateController splitShadeStateController, PowerInteractor powerInteractor, KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm, - NaturalScrollingSettingObserver naturalScrollingSettingObserver, MSDLPlayer msdlPlayer, BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor) { SceneContainerFlag.assertInLegacyMode(); @@ -651,7 +624,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardInteractor = keyguardInteractor; mPowerInteractor = powerInteractor; mClockPositionAlgorithm = keyguardClockPositionAlgorithm; - mNaturalScrollingSettingObserver = naturalScrollingSettingObserver; mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { @@ -691,7 +663,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump .setY2(0.84f) .build(); mLatencyTracker = latencyTracker; - mBounceInterpolator = new BounceInterpolator(); mFalsingManager = falsingManager; mDozeLog = dozeLog; mNotificationsDragEnabled = mResources.getBoolean( @@ -708,13 +679,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mMediaHierarchyManager = mediaHierarchyManager; mNotificationsQSContainerController = notificationsQSContainerController; mNotificationListContainer = notificationListContainer; - mNotificationStackSizeCalculator = notificationStackSizeCalculator; mNavigationBarController = navigationBarController; mNotificationsQSContainerController.init(); mNotificationStackScrollLayoutController = notificationStackScrollLayoutController; mKeyguardStatusBarViewComponentFactory = keyguardStatusBarViewComponentFactory; mDepthController = notificationShadeDepthController; - mContentResolver = contentResolver; mFragmentService = fragmentService; mStatusBarService = statusBarService; mSplitShadeStateController = splitShadeStateController; @@ -722,8 +691,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mSplitShadeStateController.shouldUseSplitNotificationShade(mResources); mView.setWillNotDraw(!DEBUG_DRAWABLE); mShadeHeaderController = shadeHeaderController; - mLayoutInflater = layoutInflater; - mFeatureFlags = featureFlags; mAnimateBack = predictiveBackAnimateShade(); mFalsingCollector = falsingCollector; mWakeUpCoordinator = coordinator; @@ -736,7 +703,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mPulseExpansionHandler = pulseExpansionHandler; mDozeParameters = dozeParameters; mScrimController = scrimController; - mUserManager = userManager; mMediaDataManager = mediaDataManager; mTapAgainViewController = tapAgainViewController; mSysUiState = sysUiState; @@ -889,7 +855,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Dreaming->Lockscreen collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(), - setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), + setDreamLockscreenTransitionAlpha(), mMainDispatcher); collectFlow(mView, mKeyguardTransitionInteractor.transition( @@ -949,13 +915,12 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump if (!com.android.systemui.Flags.bouncerUiRevamp()) return; if (isBouncerShowing && isExpanded()) { - // Blur the shade much lesser than the background surface so that the surface is - // distinguishable from the background. - float shadeBlurEffect = mDepthController.getMaxBlurRadiusPx() / 3; + float shadeBlurEffect = BlurConfig.maxBlurRadiusToNotificationPanelBlurRadius( + mDepthController.getMaxBlurRadiusPx()); mView.setRenderEffect(RenderEffect.createBlurEffect( shadeBlurEffect, shadeBlurEffect, - Shader.TileMode.MIRROR)); + Shader.TileMode.CLAMP)); } else { mView.setRenderEffect(null); } @@ -963,28 +928,31 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void updateResources() { - Trace.beginSection("NSSLC#updateResources"); - final boolean newSplitShadeEnabled = - mSplitShadeStateController.shouldUseSplitNotificationShade(mResources); - final boolean splitShadeChanged = mSplitShadeEnabled != newSplitShadeEnabled; - mSplitShadeEnabled = newSplitShadeEnabled; - mQsController.updateResources(); - mNotificationsQSContainerController.updateResources(); - updateKeyguardStatusViewAlignment(/* animate= */false); - mKeyguardMediaController.refreshMediaPosition( - "NotificationPanelViewController.updateResources"); - - if (splitShadeChanged) { - if (isPanelVisibleBecauseOfHeadsUp()) { - // workaround for b/324642496, because HUNs set state to OPENING - onPanelStateChanged(STATE_CLOSED); + try { + Trace.beginSection("NSSLC#updateResources"); + final boolean newSplitShadeEnabled = + mSplitShadeStateController.shouldUseSplitNotificationShade(mResources); + final boolean splitShadeChanged = mSplitShadeEnabled != newSplitShadeEnabled; + mSplitShadeEnabled = newSplitShadeEnabled; + mQsController.updateResources(); + mNotificationsQSContainerController.updateResources(); + updateKeyguardStatusViewAlignment(); + mKeyguardMediaController.refreshMediaPosition( + "NotificationPanelViewController.updateResources"); + + if (splitShadeChanged) { + if (isPanelVisibleBecauseOfHeadsUp()) { + // workaround for b/324642496, because HUNs set state to OPENING + onPanelStateChanged(STATE_CLOSED); + } + onSplitShadeEnabledChanged(); } - onSplitShadeEnabledChanged(); - } - mSplitShadeFullTransitionDistance = - mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance); - Trace.endSection(); + mSplitShadeFullTransitionDistance = + mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance); + } finally { + Trace.endSection(); + } } private void onSplitShadeEnabledChanged() { @@ -1011,29 +979,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mQsController.updateQsState(); } - private View reInflateStub(int viewId, int stubId, int layoutId, boolean enabled) { - View view = mView.findViewById(viewId); - if (view != null) { - int index = mView.indexOfChild(view); - mView.removeView(view); - if (enabled) { - view = mLayoutInflater.inflate(layoutId, mView, false); - mView.addView(view, index); - } else { - // Add the stub back so we can re-inflate it again if necessary - ViewStub stub = new ViewStub(mView.getContext(), layoutId); - stub.setId(stubId); - mView.addView(stub, index); - view = null; - } - } else if (enabled) { - // It's possible the stub was never inflated if the configuration changed - ViewStub stub = mView.findViewById(stubId); - view = stub.inflate(); - } - return view; - } - @VisibleForTesting void reInflateViews() { debugLog("reInflateViews"); @@ -1042,11 +987,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mStatusBarStateController.getInterpolatedDozeAmount()); } - @VisibleForTesting - boolean isFlinging() { - return mIsFlinging; - } - /** Sets a listener to be notified when the shade starts opening or finishes closing. */ public void setOpenCloseListener(OpenCloseListener openCloseListener) { SceneContainerFlag.assertInLegacyMode(); @@ -1096,8 +1036,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump * @param forceClockUpdate Should the clock be updated even when not on keyguard */ private void positionClockAndNotifications(boolean forceClockUpdate) { - boolean animate = !SceneContainerFlag.isEnabled() - && mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); int stackScrollerPadding; boolean onKeyguard = isKeyguardShowing(); @@ -1120,14 +1058,14 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mNotificationStackScrollLayoutController.setIntrinsicPadding(stackScrollerPadding); mStackScrollerMeasuringPass++; - requestScrollerTopPaddingUpdate(animate); + requestScrollerTopPaddingUpdate(); mStackScrollerMeasuringPass = 0; mAnimateNextPositionUpdate = false; } private void updateClockAppearance() { mKeyguardClockInteractor.setClockSize(computeDesiredClockSize()); - updateKeyguardStatusViewAlignment(/* animate= */true); + updateKeyguardStatusViewAlignment(); float darkAmount = mScreenOffAnimationController.shouldExpandNotifications() @@ -1146,10 +1084,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private ClockSize computeDesiredClockSize() { - if (shouldForceSmallClock()) { - return ClockSize.SMALL; - } - if (mSplitShadeEnabled) { return computeDesiredClockSizeForSplitShade(); } @@ -1174,17 +1108,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return ClockSize.LARGE; } - private boolean shouldForceSmallClock() { - return mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE) - && !isOnAod() - // True on small landscape screens - && mResources.getBoolean(R.bool.force_small_clock_on_lockscreen); - } - - private void updateKeyguardStatusViewAlignment(boolean animate) { + private void updateKeyguardStatusViewAlignment() { boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered(); mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered)); - mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered); } private boolean shouldKeyguardStatusViewBeCentered() { @@ -1214,14 +1140,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private boolean hasVisibleNotifications() { - if (FooterViewRefactor.isEnabled()) { - return mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue() - || mMediaDataManager.hasActiveMediaOrRecommendation(); - } else { - return mNotificationStackScrollLayoutController - .getVisibleNotificationCount() != 0 - || mMediaDataManager.hasActiveMediaOrRecommendation(); - } + return mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue() + || mMediaDataManager.hasActiveMediaOrRecommendation(); } @Override @@ -1464,7 +1384,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } } }); - if (!mScrimController.isScreenOn() && !mForceFlingAnimationForTest) { + if (!mScrimController.isScreenOn()) { animator.setDuration(1); } setAnimator(animator); @@ -1472,16 +1392,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } @VisibleForTesting - void setForceFlingAnimationForTest(boolean force) { - mForceFlingAnimationForTest = force; - } - - @VisibleForTesting void onFlingEnd(boolean cancelled) { mIsFlinging = false; mExpectingSynthesizedDown = false; // No overshoot when the animation ends - setOverExpansionInternal(0, false /* isFromGesture */); + setOverExpansionInternal(0); setAnimator(null); mKeyguardStateController.notifyPanelFlingEnd(); if (!cancelled) { @@ -1572,7 +1487,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } /** Return whether a touch is near the gesture handle at the bottom of screen */ - boolean isInGestureNavHomeHandleArea(float x, float y) { + boolean isInGestureNavHomeHandleArea(float y) { return mIsGestureNavigation && y > mView.getHeight() - mNavigationBarBottomHeight; } @@ -1605,7 +1520,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump * There are two scenarios behind this function call. First, input focus transfer has * successfully happened and this view already received synthetic DOWN event. * (mExpectingSynthesizedDown == false). Do nothing. - * + * <p> * Second, before input focus transfer finished, user may have lifted finger in previous window * and this window never received synthetic DOWN event. (mExpectingSynthesizedDown == true). In * this case, we use the velocity to trigger fling event. @@ -1766,7 +1681,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return mBarState == KEYGUARD; } - void requestScrollerTopPaddingUpdate(boolean animate) { + void requestScrollerTopPaddingUpdate() { if (!SceneContainerFlag.isEnabled()) { float padding = mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing, getKeyguardNotificationStaticPadding(), mExpandedFraction); @@ -2041,11 +1956,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } @VisibleForTesting - void setTouchSlopExceeded(boolean isTouchSlopExceeded) { - mTouchSlopExceeded = isTouchSlopExceeded; - } - - @VisibleForTesting void setOverExpansion(float overExpansion) { if (overExpansion == mOverExpansion) { return; @@ -2218,9 +2128,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void setBouncerShowing(boolean bouncerShowing) { mBouncerShowing = bouncerShowing; - if (!FooterViewRefactor.isEnabled()) { - mNotificationStackScrollLayoutController.updateShowEmptyShadeView(); - } updateVisibility(); } @@ -2397,7 +2304,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump final float dozeAmount = dozing ? 1 : 0; mStatusBarStateController.setAndInstrumentDozeAmount(mView, dozeAmount, animate); - updateKeyguardStatusViewAlignment(animate); + updateKeyguardStatusViewAlignment(); } @Override @@ -2416,7 +2323,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } mNotificationStackScrollLayoutController.setPulsing(pulsing, animatePulse); - updateKeyguardStatusViewAlignment(/* animate= */ true); + updateKeyguardStatusViewAlignment(); } public void performHapticFeedback(int constant) { @@ -2982,8 +2889,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mIsSpringBackAnimation = true; ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0); animator.addUpdateListener( - animation -> setOverExpansionInternal((float) animation.getAnimatedValue(), - false /* isFromGesture */)); + animation -> setOverExpansionInternal((float) animation.getAnimatedValue())); animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION); animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); animator.addListener(new AnimatorListenerAdapter() { @@ -3075,19 +2981,10 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump * Set the current overexpansion * * @param overExpansion the amount of overexpansion to apply - * @param isFromGesture is this amount from a gesture and needs to be rubberBanded? */ - private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) { - if (!isFromGesture) { - mLastGesturedOverExpansion = -1; - setOverExpansion(overExpansion); - } else if (mLastGesturedOverExpansion != overExpansion) { - mLastGesturedOverExpansion = overExpansion; - final float heightForFullOvershoot = mView.getHeight() / 3.0f; - float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot); - newExpansion = Interpolators.getOvershootInterpolation(newExpansion); - setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f); - } + private void setOverExpansionInternal(float overExpansion) { + mLastGesturedOverExpansion = -1; + setOverExpansion(overExpansion); } /** Sets the expanded height relative to a number from 0 to 1. */ @@ -3183,29 +3080,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } /** - * Phase 2: Bounce down. - */ - private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) { - ValueAnimator animator = createHeightAnimator(getMaxPanelHeight()); - animator.setDuration(450); - animator.setInterpolator(mBounceInterpolator); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - setAnimator(null); - onAnimationFinished.run(); - updateExpansionAndVisibility(); - } - }); - animator.start(); - setAnimator(animator); - } - - private ValueAnimator createHeightAnimator(float targetHeight) { - return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */); - } - - /** * Create an animator that can also overshoot * * @param targetHeight the target height @@ -3225,7 +3099,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mPanelFlingOvershootAmount * overshootAmount, Interpolators.FAST_OUT_SLOW_IN.getInterpolation( animator.getAnimatedFraction())); - setOverExpansionInternal(expansion, false /* isFromGesture */); + setOverExpansionInternal(expansion); } setExpandedHeightInternal((float) animation.getAnimatedValue()); }); @@ -3280,8 +3154,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mFixedDuration = NO_FIXED_DURATION; } - boolean postToView(Runnable action) { - return mView.post(action); + void postToView(Runnable action) { + mView.post(action); } /** Sends an external (e.g. Status Bar) intercept touch event to the Shade touch handler. */ @@ -3360,7 +3234,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return mShadeExpansionStateManager; } - void onQsExpansionChanged(boolean expanded) { + void onQsExpansionChanged() { updateExpandedHeightToMaxHeight(); updateSystemUiStateFlags(); NavigationBarView navigationBarView = @@ -3372,7 +3246,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @VisibleForTesting void onQsSetExpansionHeightCalled(boolean qsFullyExpanded) { - requestScrollerTopPaddingUpdate(false); + requestScrollerTopPaddingUpdate(); mKeyguardStatusBarViewController.updateViewState(); int barState = getBarState(); if (barState == SHADE_LOCKED || barState == KEYGUARD) { @@ -3413,7 +3287,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void onExpansionHeightSetToMax(boolean requestPaddingUpdate) { if (requestPaddingUpdate) { - requestScrollerTopPaddingUpdate(false /* animate */); + requestScrollerTopPaddingUpdate(); } updateExpandedHeightToMaxHeight(); } @@ -3437,7 +3311,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump ? (ExpandableNotificationRow) firstChildNotGone : null; if (firstRow != null && (view == firstRow || (firstRow.getNotificationParent() == firstRow))) { - requestScrollerTopPaddingUpdate(false /* animate */); + requestScrollerTopPaddingUpdate(); } updateExpandedHeightToMaxHeight(); } @@ -3517,7 +3391,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump boolean animatingUnlockedShadeToKeyguardBypass ) { boolean goingToFullShade = mStatusBarStateController.goingToFullShade(); - boolean keyguardFadingAway = mKeyguardStateController.isKeyguardFadingAway(); int oldState = mBarState; boolean keyguardShowing = statusBarState == KEYGUARD; @@ -3738,17 +3611,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } if (state == STATE_CLOSED) { mQsController.setExpandImmediate(false); - // Close the status bar in the next frame so we can show the end of the - // animation. - if (!mIsAnyMultiShadeExpanded) { - mView.post(mMaybeHideExpandedRunnable); - } + // Close the status bar in the next frame so we can show the end of the animation. + mView.post(mMaybeHideExpandedRunnable); } mCurrentPanelState = state; } - private Consumer<Float> setDreamLockscreenTransitionAlpha( - NotificationStackScrollLayoutController stackScroller) { + private Consumer<Float> setDreamLockscreenTransitionAlpha() { return (Float alpha) -> { // Also animate the status bar's alpha during transitions between the lockscreen and // dreams. @@ -4285,4 +4154,3 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } } } - diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index c88e7b827881..d05837261b89 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -86,7 +86,6 @@ import com.android.systemui.statusbar.PulseExpansionHandler; import com.android.systemui.statusbar.QsFrameTranslateController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor; -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; @@ -96,8 +95,8 @@ import com.android.systemui.statusbar.phone.KeyguardStatusBarView; import com.android.systemui.statusbar.phone.LightBarController; import com.android.systemui.statusbar.phone.LockscreenGestureLogger; import com.android.systemui.statusbar.phone.ScrimController; -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.ShadeTouchableRegionManager; +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.SplitShadeStateController; @@ -115,9 +114,7 @@ import java.io.PrintWriter; import javax.inject.Inject; import javax.inject.Provider; -/** Handles QuickSettings touch handling, expansion and animation state - * TODO (b/264460656) make this dumpable - */ +/** Handles QuickSettings touch handling, expansion and animation state. */ @SysUISingleton public class QuickSettingsControllerImpl implements QuickSettingsController, Dumpable { public static final String TAG = "QuickSettingsController"; @@ -295,7 +292,6 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum private ValueAnimator mSizeChangeAnimator; private ExpansionHeightListener mExpansionHeightListener; - private QsStateUpdateListener mQsStateUpdateListener; private ApplyClippingImmediatelyListener mApplyClippingImmediatelyListener; private FlingQsWithoutClickListener mFlingQsWithoutClickListener; private ExpansionHeightSetToMaxListener mExpansionHeightSetToMaxListener; @@ -402,10 +398,6 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum mExpansionHeightListener = listener; } - void setQsStateUpdateListener(QsStateUpdateListener listener) { - mQsStateUpdateListener = listener; - } - void setApplyClippingImmediatelyListener(ApplyClippingImmediatelyListener listener) { mApplyClippingImmediatelyListener = listener; } @@ -563,7 +555,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum } // TODO (b/265193930): remove dependency on NPVC // Let's reject anything at the very bottom around the home handle in gesture nav - if (mPanelViewControllerLazy.get().isInGestureNavHomeHandleArea(x, y)) { + if (mPanelViewControllerLazy.get().isInGestureNavHomeHandleArea(y)) { return false; } return y <= mNotificationStackScrollLayoutController.getBottomMostNotificationBottom() @@ -805,7 +797,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum if (changed) { mShadeRepository.setLegacyIsQsExpanded(expanded); updateQsState(); - mPanelViewControllerLazy.get().onQsExpansionChanged(expanded); + mPanelViewControllerLazy.get().onQsExpansionChanged(); mShadeLog.logQsExpansionChanged("QS Expansion Changed.", expanded, getMinExpansionHeight(), getMaxExpansionHeight(), mStackScrollerOverscrolling, mAnimatorExpand, mAnimating); @@ -1022,16 +1014,6 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum } void updateQsState() { - if (!FooterViewRefactor.isEnabled()) { - // Update full screen state; note that this will be true if the QS panel is only - // partially expanded, and that is fixed with the footer view refactor. - setQsFullScreen(/* qsFullScreen = */ getExpanded() && !mSplitShadeEnabled); - } - - if (mQsStateUpdateListener != null) { - mQsStateUpdateListener.onQsStateUpdated(getExpanded(), mStackScrollerOverscrolling); - } - if (mQs == null) return; mQs.setExpanded(getExpanded()); } @@ -1094,10 +1076,8 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum // Update the light bar mLightBarController.setQsExpanded(mFullyExpanded); - if (FooterViewRefactor.isEnabled()) { - // Update full screen state - setQsFullScreen(/* qsFullScreen = */ mFullyExpanded && !mSplitShadeEnabled); - } + // Update full screen state + setQsFullScreen(/* qsFullScreen = */ mFullyExpanded && !mSplitShadeEnabled); } float getLockscreenShadeDragProgress() { @@ -1212,7 +1192,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum /** * Applies clipping to quick settings, notifications layout and * updates bounds of the notifications background (notifications scrim). - * + * <p> * The parameters are bounds of the notifications area rectangle, this function * calculates bounds for the QS clipping based on the notifications bounds. */ @@ -2268,10 +2248,8 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum setExpansionHeight(qsHeight); } - boolean hasNotifications = FooterViewRefactor.isEnabled() - ? mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue() - : mNotificationStackScrollLayoutController.getVisibleNotificationCount() - != 0; + boolean hasNotifications = + mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue(); if (!hasNotifications && !mMediaDataManager.hasActiveMediaOrRecommendation()) { // No notifications are visible, let's animate to the height of qs instead if (isQsFragmentCreated()) { @@ -2406,10 +2384,6 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum void onQsSetExpansionHeightCalled(boolean qsFullyExpanded); } - interface QsStateUpdateListener { - void onQsStateUpdated(boolean qsExpanded, boolean isStackScrollerOverscrolling); - } - interface ApplyClippingImmediatelyListener { void onQsClippingImmediatelyApplied(boolean clipStatusView, Rect lastQsClipBounds, int top, boolean qsFragmentCreated, boolean qsVisible); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt index 63e8ba8f65cd..747642097327 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -37,12 +37,13 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository -import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor -import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl import com.android.systemui.shade.display.ShadeDisplayPolicyModule +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeDisplaysInteractor +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.statusbar.phone.ConfigurationControllerImpl import com.android.systemui.statusbar.phone.ConfigurationForwarder @@ -276,6 +277,8 @@ object ShadeDisplayAwareModule { @Module internal interface OptionalShadeDisplayAwareBindings { @BindsOptionalOf fun bindOptionalOfWindowRootView(): WindowRootView + + @BindsOptionalOf fun bindOptionalOShadeExpandedStateInteractor(): ShadeExpandedStateInteractor } /** diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt index 4d35d0eba178..e358dcec8b10 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt @@ -24,7 +24,6 @@ import com.android.systemui.common.ui.view.ChoreographerUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.scene.ui.view.WindowRootView -import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker.Companion.TIMEOUT import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.util.kotlin.getOrNull import java.util.Optional @@ -33,7 +32,6 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter @@ -135,7 +133,7 @@ constructor( private companion object { const val TAG = "ShadeDisplayLatency" - val t = TrackTracer(trackName = TAG) + val t = TrackTracer(trackName = TAG, trackGroup = "shade") val TIMEOUT = 3.seconds const val SHADE_MOVE_ACTION = LatencyTracker.ACTION_SHADE_WINDOW_DISPLAY_CHANGE } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt index 359ddd86f115..5fab889735a6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt @@ -18,13 +18,16 @@ package com.android.systemui.shade import android.annotation.IntDef import android.os.Trace +import android.os.Trace.TRACE_TAG_APP as TRACE_TAG import android.util.Log import androidx.annotation.FloatRange +import com.android.app.tracing.TraceStateLogger +import com.android.app.tracing.TrackGroupUtils.trackGroup +import com.android.app.tracing.coroutines.TrackTracer import com.android.systemui.dagger.SysUISingleton import com.android.systemui.util.Compile import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject -import android.os.Trace.TRACE_TAG_APP as TRACE_TAG /** * A class responsible for managing the notification panel's current state. @@ -38,6 +41,8 @@ class ShadeExpansionStateManager @Inject constructor() { private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>() private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>() + private val stateLogger = TraceStateLogger(trackGroup("shade", TRACK_NAME)) + @PanelState private var state: Int = STATE_CLOSED @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f private var expanded: Boolean = false @@ -75,7 +80,7 @@ class ShadeExpansionStateManager @Inject constructor() { fun onPanelExpansionChanged( @FloatRange(from = 0.0, to = 1.0) fraction: Float, expanded: Boolean, - tracking: Boolean + tracking: Boolean, ) { require(!fraction.isNaN()) { "fraction cannot be NaN" } val oldState = state @@ -113,11 +118,8 @@ class ShadeExpansionStateManager @Inject constructor() { ) if (Trace.isTagEnabled(TRACE_TAG)) { - Trace.traceCounter(TRACE_TAG, "panel_expansion", (fraction * 100).toInt()) - if (state != oldState) { - Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK_NAME, 0) - Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK_NAME, state.panelStateToString(), 0) - } + TrackTracer.instantForGroup("shade", "panel_expansion", fraction) + stateLogger.log(state.panelStateToString()) } val expansionChangeEvent = ShadeExpansionChangeEvent(fraction, expanded, tracking) diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index 2348a110eb3a..b9df9f868dc3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -35,6 +35,8 @@ import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorLega import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorSceneContainerImpl import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractorImpl +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl @@ -176,4 +178,10 @@ abstract class ShadeModule { @Binds @SysUISingleton abstract fun bindShadeModeInteractor(impl: ShadeModeInteractorImpl): ShadeModeInteractor + + @Binds + @SysUISingleton + abstract fun bindShadeExpandedStateInteractor( + impl: ShadeExpandedStateInteractorImpl + ): ShadeExpandedStateInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt new file mode 100644 index 000000000000..2705cdafb4de --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 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.shade + +import com.android.app.tracing.TraceStateLogger +import com.android.app.tracing.TrackGroupUtils.trackGroup +import com.android.app.tracing.coroutines.TrackTracer.Companion.instantForGroup +import com.android.app.tracing.coroutines.launchTraced +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.data.repository.ShadeDisplaysRepository +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@SysUISingleton +class ShadeStateTraceLogger +@Inject +constructor( + private val shadeInteractor: ShadeInteractor, + private val shadeDisplaysRepository: ShadeDisplaysRepository, + @Application private val scope: CoroutineScope, +) : CoreStartable { + override fun start() { + scope.launchTraced("ShadeStateTraceLogger") { + launch { + val stateLogger = createTraceStateLogger("isShadeLayoutWide") + shadeInteractor.isShadeLayoutWide.collect { stateLogger.log(it.toString()) } + } + launch { + val stateLogger = createTraceStateLogger("shadeMode") + shadeInteractor.shadeMode.collect { stateLogger.log(it.toString()) } + } + launch { + shadeInteractor.shadeExpansion.collect { + instantForGroup(TRACK_GROUP_NAME, "shadeExpansion", it) + } + } + launch { + shadeDisplaysRepository.displayId.collect { + instantForGroup(TRACK_GROUP_NAME, "displayId", it) + } + } + } + } + + private fun createTraceStateLogger(trackName: String): TraceStateLogger { + return TraceStateLogger(trackGroup(TRACK_GROUP_NAME, trackName)) + } + + private companion object { + const val TRACK_GROUP_NAME = "shade" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt index a36c56eafbfc..9a9fc467c53f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt @@ -27,7 +27,7 @@ import com.android.app.tracing.coroutines.TrackTracer * them across various threads' logs. */ object ShadeTraceLogger { - private val t = TrackTracer(trackName = "ShadeTraceLogger") + val t = TrackTracer(trackName = "ShadeTraceLogger", trackGroup = "shade") @JvmStatic fun logOnMovedToDisplay(displayId: Int, config: Configuration) { @@ -44,8 +44,11 @@ object ShadeTraceLogger { t.instant { "moveShadeWindowTo(displayId=$displayId)" } } - @JvmStatic - fun traceReparenting(r: () -> Unit) { + suspend fun traceReparenting(r: suspend () -> Unit) { t.traceAsync({ "reparenting" }) { r() } } + + inline fun traceWaitForExpansion(expansion: Float, r: () -> Unit) { + t.traceAsync({ "waiting for shade expansion to match $expansion" }) { r() } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt index e19112047d2a..3449e81a4630 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt @@ -41,6 +41,7 @@ import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.ui.composable.Overlay import com.android.systemui.scene.ui.composable.Scene +import com.android.systemui.scene.ui.view.SceneJankMonitor import com.android.systemui.scene.ui.view.SceneWindowRootView import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel @@ -89,6 +90,7 @@ abstract class ShadeViewProviderModule { layoutInsetController: NotificationInsetsController, sceneDataSourceDelegator: Provider<SceneDataSourceDelegator>, qsSceneAdapter: Provider<QSSceneAdapter>, + sceneJankMonitorFactory: SceneJankMonitor.Factory, ): WindowRootView { return if (SceneContainerFlag.isEnabled) { checkNoSceneDuplicates(scenesProvider.get()) @@ -104,6 +106,7 @@ abstract class ShadeViewProviderModule { layoutInsetController = layoutInsetController, sceneDataSourceDelegator = sceneDataSourceDelegator.get(), qsSceneAdapter = qsSceneAdapter, + sceneJankMonitorFactory = sceneJankMonitorFactory, ) sceneWindowRootView } else { diff --git a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt index c4de78b8a28e..570a7853c394 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt @@ -40,4 +40,9 @@ internal abstract class StartShadeModule { @IntoMap @ClassKey(ShadeStartable::class) abstract fun provideShadeStartable(startable: ShadeStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(ShadeStateTraceLogger::class) + abstract fun provideShadeStateTraceLogger(startable: ShadeStateTraceLogger): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt new file mode 100644 index 000000000000..eab00166c8ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 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.shade.domain.interactor + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** Fake [ShadeExpandedStateInteractor] for tests. */ +class FakeShadeExpandedStateInteractor : ShadeExpandedStateInteractor { + + private val mutableExpandedElement = + MutableStateFlow<ShadeExpandedStateInteractor.ShadeElement?>(null) + override val currentlyExpandedElement: StateFlow<ShadeExpandedStateInteractor.ShadeElement?> + get() = mutableExpandedElement + + fun setState(state: ShadeExpandedStateInteractor.ShadeElement?) { + mutableExpandedElement.value = state + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt index be561b178136..691a383cb338 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt @@ -32,6 +32,7 @@ import com.android.systemui.shade.ShadeTraceLogger.traceReparenting import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.window.flags.Flags +import java.util.Optional import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope @@ -47,8 +48,19 @@ constructor( @Background private val bgScope: CoroutineScope, @Main private val mainThreadContext: CoroutineContext, private val shadeDisplayChangeLatencyTracker: ShadeDisplayChangeLatencyTracker, + shadeExpandedInteractor: Optional<ShadeExpandedStateInteractor>, ) : CoreStartable { + private val shadeExpandedInteractor = + shadeExpandedInteractor.orElse(null) + ?: error( + """ + ShadeExpandedStateInteractor must be provided for ShadeDisplaysInteractor to work. + If it is not, it means this is being instantiated in a SystemUI variant that shouldn't. + """ + .trimIndent() + ) + override fun start() { ShadeWindowGoesAround.isUnexpectedlyInLegacyMode() bgScope.launchTraced(TAG) { @@ -78,9 +90,12 @@ constructor( withContext(mainThreadContext) { traceReparenting { shadeDisplayChangeLatencyTracker.onShadeDisplayChanging(destinationId) + val expandedElement = shadeExpandedInteractor.currentlyExpandedElement.value + expandedElement?.collapse(reason = "Shade window move") reparentToDisplayId(id = destinationId) + expandedElement?.expand(reason = "Shade window move") + checkContextDisplayMatchesExpected(destinationId) } - checkContextDisplayMatchesExpected(destinationId) } } catch (e: IllegalStateException) { Log.e( diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt new file mode 100644 index 000000000000..dd3abeec5a72 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2025 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.shade.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shade.ShadeTraceLogger.traceWaitForExpansion +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor.ShadeElement +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.util.kotlin.Utils.Companion.combineState +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout + +/** + * Wrapper around [ShadeInteractor] to facilitate expansion and collapse of Notifications and quick + * settings. + * + * Specifially created to simplify [ShadeDisplaysInteractor] logic. + * + * NOTE: with [SceneContainerFlag] or [DualShade] disabled, [currentlyExpandedElement] will always + * return null! + */ +interface ShadeExpandedStateInteractor { + /** Returns the expanded [ShadeElement]. If none is, returns null. */ + val currentlyExpandedElement: StateFlow<ShadeElement?> + + /** An element from the shade window that can be expanded or collapsed. */ + abstract class ShadeElement { + /** Expands the shade element, returning when the expansion is done */ + abstract suspend fun expand(reason: String) + + /** Collapses the shade element, returning when the collapse is done. */ + abstract suspend fun collapse(reason: String) + } +} + +@SysUISingleton +class ShadeExpandedStateInteractorImpl +@Inject +constructor( + private val shadeInteractor: ShadeInteractor, + @Background private val bgScope: CoroutineScope, +) : ShadeExpandedStateInteractor { + + private val notificationElement = NotificationElement() + private val qsElement = QSElement() + + override val currentlyExpandedElement: StateFlow<ShadeElement?> = + if (SceneContainerFlag.isEnabled) { + combineState( + shadeInteractor.isShadeAnyExpanded, + shadeInteractor.isQsExpanded, + bgScope, + SharingStarted.Eagerly, + ) { isShadeAnyExpanded, isQsExpanded -> + when { + isShadeAnyExpanded -> notificationElement + isQsExpanded -> qsElement + else -> null + } + } + } else { + MutableStateFlow(null) + } + + inner class NotificationElement : ShadeElement() { + override suspend fun expand(reason: String) { + shadeInteractor.expandNotificationsShade(reason) + shadeInteractor.shadeExpansion.waitUntil(1f) + } + + override suspend fun collapse(reason: String) { + shadeInteractor.collapseNotificationsShade(reason) + shadeInteractor.shadeExpansion.waitUntil(0f) + } + } + + inner class QSElement : ShadeElement() { + override suspend fun expand(reason: String) { + shadeInteractor.expandQuickSettingsShade(reason) + shadeInteractor.qsExpansion.waitUntil(1f) + } + + override suspend fun collapse(reason: String) { + shadeInteractor.collapseQuickSettingsShade(reason) + shadeInteractor.qsExpansion.waitUntil(0f) + } + } + + private suspend fun StateFlow<Float>.waitUntil(f: Float) { + // it's important to not do this in the main thread otherwise it will block any rendering. + withContext(bgScope.coroutineContext) { + withTimeout(1.seconds) { traceWaitForExpansion(expansion = f) { first { it == f } } } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt index 37989f56d559..2885ce80bda9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt @@ -11,13 +11,13 @@ import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffXfermode import android.graphics.RadialGradient import android.graphics.Shader -import android.os.Trace import android.util.AttributeSet import android.util.MathUtils.lerp import android.view.MotionEvent import android.view.View import android.view.animation.PathInterpolator import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.TrackTracer import com.android.keyguard.logging.ScrimLogger import com.android.systemui.shade.TouchLogger import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold @@ -321,9 +321,8 @@ constructor( } revealEffect.setRevealAmountOnScrim(value, this) updateScrimOpaque() - Trace.traceCounter( - Trace.TRACE_TAG_APP, - "light_reveal_amount $logString", + TrackTracer.instantForGroup( + "scrim", { "light_reveal_amount $logString" }, (field * 100).toInt() ) invalidate() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 2bcd3fcfed17..10b726b90894 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -92,6 +92,7 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import javax.inject.Inject; @@ -297,6 +298,9 @@ public class NotificationLockscreenUserManagerImpl implements // The last lock time. Uses currentTimeMillis @VisibleForTesting protected final AtomicLong mLastLockTime = new AtomicLong(-1); + // Whether or not the device is locked + @VisibleForTesting + protected final AtomicBoolean mLocked = new AtomicBoolean(true); protected int mCurrentUserId = 0; @@ -369,6 +373,7 @@ public class NotificationLockscreenUserManagerImpl implements if (!unlocked) { mLastLockTime.set(System.currentTimeMillis()); } + mLocked.set(!unlocked); })); } } @@ -737,7 +742,7 @@ public class NotificationLockscreenUserManagerImpl implements return false; } - if (!mKeyguardManager.isDeviceLocked()) { + if (!mLocked.get()) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index e83cded4e2ce..38f7c39203f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -22,7 +22,6 @@ import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration import android.os.SystemClock -import android.os.Trace import android.util.IndentingPrintWriter import android.util.Log import android.util.MathUtils @@ -33,7 +32,9 @@ import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.TrackTracer import com.android.systemui.Dumpable +import com.android.systemui.Flags.spatialModelAppPushback import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -52,7 +53,9 @@ import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.WallpaperController import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import com.android.systemui.window.flag.WindowBlurFlag +import com.android.wm.shell.appzoomout.AppZoomOut import java.io.PrintWriter +import java.util.Optional import javax.inject.Inject import kotlin.math.max import kotlin.math.sign @@ -79,6 +82,7 @@ constructor( private val splitShadeStateController: SplitShadeStateController, private val windowRootViewBlurInteractor: WindowRootViewBlurInteractor, @Application private val applicationScope: CoroutineScope, + private val appZoomOutOptional: Optional<AppZoomOut>, dumpManager: DumpManager, configurationController: ConfigurationController, ) : ShadeExpansionListener, Dumpable { @@ -263,7 +267,7 @@ constructor( updateScheduled = false val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut() val opaque = shouldBlurBeOpaque - Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur) + TrackTracer.instantForGroup("shade", "shade_blur_radius", blur) blurUtils.applyBlur(root.viewRootImpl, blur, opaque) onBlurApplied(blur, zoomOutFromShadeRadius) } @@ -271,6 +275,13 @@ constructor( private fun onBlurApplied(appliedBlurRadius: Int, zoomOutFromShadeRadius: Float) { lastAppliedBlur = appliedBlurRadius wallpaperController.setNotificationShadeZoom(zoomOutFromShadeRadius) + if (spatialModelAppPushback()) { + appZoomOutOptional.ifPresent { appZoomOut -> + appZoomOut.setProgress( + zoomOutFromShadeRadius + ) + } + } listeners.forEach { it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius) it.onBlurRadiusChanged(appliedBlurRadius) @@ -384,7 +395,7 @@ constructor( windowRootViewBlurInteractor.onBlurAppliedEvent.collect { appliedBlurRadius -> if (updateScheduled) { // Process the blur applied event only if we scheduled the update - Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", appliedBlurRadius) + TrackTracer.instantForGroup("shade", "shade_blur_radius", appliedBlurRadius) updateScheduled = false onBlurApplied(appliedBlurRadius, zoomOutCalculatedFromShadeRadius) } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 48cf7a83c324..155049f512d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.IndentingPrintWriter; import android.util.MathUtils; @@ -30,6 +31,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; @@ -1014,12 +1016,24 @@ public class NotificationShelf extends ActivatableNotificationView { public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); if (mInteractive) { + // Add two accessibility actions that both performs expanding the notification shade info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); - AccessibilityNodeInfo.AccessibilityAction unlock - = new AccessibilityNodeInfo.AccessibilityAction( + + AccessibilityAction seeAll = new AccessibilityAction( AccessibilityNodeInfo.ACTION_CLICK, - getContext().getString(R.string.accessibility_overflow_action)); - info.addAction(unlock); + getContext().getString(R.string.accessibility_overflow_action) + ); + info.addAction(seeAll); + } + } + + @Override + public boolean performAccessibilityAction(int action, Bundle args) { + // override ACTION_EXPAND with ACTION_CLICK + if (action == AccessibilityNodeInfo.ACTION_EXPAND) { + return super.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, args); + } else { + return super.performAccessibilityAction(action, args); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index a7ad46296e08..ead8f6a1123e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -37,6 +37,7 @@ import android.view.animation.Interpolator; import androidx.annotation.NonNull; import com.android.app.animation.Interpolators; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.compose.animation.scene.OverlayKey; import com.android.compose.animation.scene.SceneKey; import com.android.internal.annotations.GuardedBy; @@ -671,7 +672,7 @@ public class StatusBarStateControllerImpl implements } private void recordHistoricalState(int newState, int lastState, boolean upcoming) { - Trace.traceCounter(Trace.TRACE_TAG_APP, "statusBarState", newState); + TrackTracer.instantForGroup("statusBar", "state", newState); mHistoryIndex = (mHistoryIndex + 1) % HISTORY_SIZE; HistoricalState state = mHistoricalRecords[mHistoryIndex]; state.mNewState = newState; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index de08e3891902..86954d569199 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel import android.view.View import com.android.internal.jank.InteractionJankMonitor -import com.android.systemui.Flags import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon @@ -64,18 +63,12 @@ constructor( is OngoingCallModel.InCallWithVisibleApp -> OngoingActivityChipModel.Hidden() is OngoingCallModel.InCall -> { val icon = - if ( - Flags.statusBarCallChipNotificationIcon() && - state.notificationIconView != null - ) { + if (state.notificationIconView != null) { StatusBarConnectedDisplays.assertInLegacyMode() OngoingActivityChipModel.ChipIcon.StatusBarView( state.notificationIconView ) - } else if ( - StatusBarConnectedDisplays.isEnabled && - Flags.statusBarCallChipNotificationIcon() - ) { + } else if (StatusBarConnectedDisplays.isEnabled) { OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon( state.notificationKey ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 2f6431b05c8b..ec3a5b271e35 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import javax.inject.Inject @@ -60,7 +61,7 @@ constructor( /** Converts the notification to the [OngoingActivityChipModel] object. */ private fun NotificationChipModel.toActivityChipModel( - headsUpState: PinnedStatus + headsUpState: TopPinnedState ): OngoingActivityChipModel.Shown { StatusBarNotifChips.assertInNewMode() val icon = @@ -87,8 +88,12 @@ constructor( } } - if (headsUpState == PinnedStatus.PinnedByUser) { - // If the user tapped the chip to show the HUN, we want to just show the icon because + val isShowingHeadsUpFromChipTap = + headsUpState is TopPinnedState.Pinned && + headsUpState.status == PinnedStatus.PinnedByUser && + headsUpState.key == this.key + if (isShowingHeadsUpFromChipTap) { + // If the user tapped this chip to show the HUN, we want to just show the icon because // the HUN will show the rest of the information. return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt index 69ef09d8bf5e..b0fa9d842480 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt @@ -25,6 +25,7 @@ import android.widget.DateTimeView import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.annotation.UiThread import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView @@ -38,24 +39,24 @@ import com.android.systemui.statusbar.notification.icon.ui.viewbinder.Notificati /** Binder for ongoing activity chip views. */ object OngoingActivityChipBinder { /** Binds the given [chipModel] data to the given [chipView]. */ - fun bind(chipModel: OngoingActivityChipModel, chipView: View, iconViewStore: IconViewStore?) { - val chipContext = chipView.context - val chipDefaultIconView: ImageView = - chipView.requireViewById(R.id.ongoing_activity_chip_icon) - val chipTimeView: ChipChronometer = - chipView.requireViewById(R.id.ongoing_activity_chip_time) - val chipTextView: TextView = chipView.requireViewById(R.id.ongoing_activity_chip_text) - val chipShortTimeDeltaView: DateTimeView = - chipView.requireViewById(R.id.ongoing_activity_chip_short_time_delta) - val chipBackgroundView: ChipBackgroundContainer = - chipView.requireViewById(R.id.ongoing_activity_chip_background) + fun bind( + chipModel: OngoingActivityChipModel, + viewBinding: OngoingActivityChipViewBinding, + iconViewStore: IconViewStore?, + ) { + val chipContext = viewBinding.rootView.context + val chipDefaultIconView = viewBinding.defaultIconView + val chipTimeView = viewBinding.timeView + val chipTextView = viewBinding.textView + val chipShortTimeDeltaView = viewBinding.shortTimeDeltaView + val chipBackgroundView = viewBinding.backgroundView when (chipModel) { is OngoingActivityChipModel.Shown -> { // Data setChipIcon(chipModel, chipBackgroundView, chipDefaultIconView, iconViewStore) setChipMainContent(chipModel, chipTextView, chipTimeView, chipShortTimeDeltaView) - chipView.setOnClickListener(chipModel.onClickListener) + viewBinding.rootView.setOnClickListener(chipModel.onClickListener) updateChipPadding( chipModel, chipBackgroundView, @@ -65,7 +66,7 @@ object OngoingActivityChipBinder { ) // Accessibility - setChipAccessibility(chipModel, chipView, chipBackgroundView) + setChipAccessibility(chipModel, viewBinding.rootView, chipBackgroundView) // Colors val textColor = chipModel.colors.text(chipContext) @@ -83,6 +84,85 @@ object OngoingActivityChipBinder { } } + /** Stores [rootView] and relevant child views in an object for easy reference. */ + fun createBinding(rootView: View): OngoingActivityChipViewBinding { + return OngoingActivityChipViewBinding( + rootView = rootView, + timeView = rootView.requireViewById(R.id.ongoing_activity_chip_time), + textView = rootView.requireViewById(R.id.ongoing_activity_chip_text), + shortTimeDeltaView = + rootView.requireViewById(R.id.ongoing_activity_chip_short_time_delta), + defaultIconView = rootView.requireViewById(R.id.ongoing_activity_chip_icon), + backgroundView = rootView.requireViewById(R.id.ongoing_activity_chip_background), + ) + } + + /** + * Resets any width restrictions that were placed on the primary chip's contents. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + fun resetPrimaryChipWidthRestrictions( + primaryChipViewBinding: OngoingActivityChipViewBinding, + currentPrimaryChipViewModel: OngoingActivityChipModel, + ) { + if (currentPrimaryChipViewModel is OngoingActivityChipModel.Hidden) { + return + } + resetChipMainContentWidthRestrictions( + primaryChipViewBinding, + currentPrimaryChipViewModel as OngoingActivityChipModel.Shown, + ) + } + + /** + * Resets any width restrictions that were placed on the secondary chip and its contents. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + fun resetSecondaryChipWidthRestrictions( + secondaryChipViewBinding: OngoingActivityChipViewBinding, + currentSecondaryChipModel: OngoingActivityChipModel, + ) { + if (currentSecondaryChipModel is OngoingActivityChipModel.Hidden) { + return + } + secondaryChipViewBinding.rootView.resetWidthRestriction() + resetChipMainContentWidthRestrictions( + secondaryChipViewBinding, + currentSecondaryChipModel as OngoingActivityChipModel.Shown, + ) + } + + private fun resetChipMainContentWidthRestrictions( + viewBinding: OngoingActivityChipViewBinding, + model: OngoingActivityChipModel.Shown, + ) { + when (model) { + is OngoingActivityChipModel.Shown.Text -> viewBinding.textView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.Timer -> viewBinding.timeView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.ShortTimeDelta -> + viewBinding.shortTimeDeltaView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.IconOnly, + is OngoingActivityChipModel.Shown.Countdown -> {} + } + } + + /** + * Resets any width restrictions that were placed on the given view. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + @UiThread + fun View.resetWidthRestriction() { + // View needs to be visible in order to be re-measured + visibility = View.VISIBLE + forceLayout() + } + private fun setChipIcon( chipModel: OngoingActivityChipModel.Shown, backgroundView: ChipBackgroundContainer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt new file mode 100644 index 000000000000..1814b7430330 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 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.chips.ui.binder + +import android.view.View +import android.widget.ImageView +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.ui.view.ChipChronometer +import com.android.systemui.statusbar.chips.ui.view.ChipDateTimeView +import com.android.systemui.statusbar.chips.ui.view.ChipTextView + +/** Stores bound views for a given chip. */ +data class OngoingActivityChipViewBinding( + val rootView: View, + val timeView: ChipChronometer, + val textView: ChipTextView, + val shortTimeDeltaView: ChipDateTimeView, + val defaultIconView: ImageView, + val backgroundView: ChipBackgroundContainer, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChronometerText.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChronometerText.kt index a747abbc6a6e..1c14d3349027 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChronometerText.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChronometerText.kt @@ -28,17 +28,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.Measurable -import androidx.compose.ui.layout.MeasureResult -import androidx.compose.ui.layout.MeasureScope -import androidx.compose.ui.node.LayoutModifierNode -import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.constrain import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.statusbar.chips.ui.compose.modifiers.neverDecreaseWidth import kotlinx.coroutines.delay /** Platform-optimized interface for getting current time */ @@ -97,35 +91,3 @@ fun ChronometerText( modifier = modifier.neverDecreaseWidth(), ) } - -/** A modifier that ensures the width of the content only increases and never decreases. */ -private fun Modifier.neverDecreaseWidth(): Modifier { - return this.then(neverDecreaseWidthElement) -} - -private data object neverDecreaseWidthElement : ModifierNodeElement<NeverDecreaseWidthNode>() { - override fun create(): NeverDecreaseWidthNode { - return NeverDecreaseWidthNode() - } - - override fun update(node: NeverDecreaseWidthNode) { - error("This should never be called") - } -} - -private class NeverDecreaseWidthNode : Modifier.Node(), LayoutModifierNode { - private var minWidth = 0 - - override fun MeasureScope.measure( - measurable: Measurable, - constraints: Constraints, - ): MeasureResult { - val placeable = measurable.measure(Constraints(minWidth = minWidth).constrain(constraints)) - val width = placeable.width - val height = placeable.height - - minWidth = maxOf(minWidth, width) - - return layout(width, height) { placeable.place(0, 0) } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt new file mode 100644 index 000000000000..1be5842bceeb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -0,0 +1,208 @@ +/* + * 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.chips.ui.compose + +import android.content.res.ColorStateList +import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.compose.modifiers.neverDecreaseWidth +import com.android.systemui.statusbar.chips.ui.model.ColorsModel +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel + +@Composable +fun OngoingActivityChip(model: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) { + val context = LocalContext.current + val isClickable = model.onClickListener != null + val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView + + // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible + // height of the chip is determined by the height of the background of the Row below. + Box( + contentAlignment = Alignment.Center, + modifier = + modifier + .fillMaxHeight() + .clickable( + enabled = isClickable, + onClick = { + // TODO(b/372657935): Implement click actions. + }, + ), + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.clip( + RoundedCornerShape( + dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius) + ) + ) + .height(dimensionResource(R.dimen.ongoing_appops_chip_height)) + .widthIn( + min = + if (isClickable) { + dimensionResource(id = R.dimen.min_clickable_item_size) + } else { + 0.dp + } + ) + .background(Color(model.colors.background(context).defaultColor)) + .padding( + horizontal = + if (hasEmbeddedIcon) { + 0.dp + } else { + dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding) + } + ), + ) { + model.icon?.let { ChipIcon(viewModel = it, colors = model.colors) } + + val isIconOnly = model is OngoingActivityChipModel.Shown.IconOnly + val isTextOnly = model.icon == null + if (!isIconOnly) { + ChipContent( + viewModel = model, + modifier = + Modifier.padding( + start = + if (isTextOnly || hasEmbeddedIcon) { + 0.dp + } else { + dimensionResource( + id = R.dimen.ongoing_activity_chip_icon_text_padding + ) + }, + end = + if (hasEmbeddedIcon) { + dimensionResource( + id = + R.dimen + .ongoing_activity_chip_text_end_padding_for_embedded_padding_icon + ) + } else { + 0.dp + }, + ), + ) + } + } + } +} + +@Composable +private fun ChipIcon( + viewModel: OngoingActivityChipModel.ChipIcon, + colors: ColorsModel, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + when (viewModel) { + is OngoingActivityChipModel.ChipIcon.StatusBarView -> { + val originalIcon = viewModel.impl + val iconSizePx = + context.resources.getDimensionPixelSize( + R.dimen.ongoing_activity_chip_embedded_padding_icon_size + ) + AndroidView( + modifier = modifier, + factory = { _ -> + originalIcon.apply { + layoutParams = ViewGroup.LayoutParams(iconSizePx, iconSizePx) + imageTintList = ColorStateList.valueOf(colors.text(context)) + } + }, + ) + } + + is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> { + Icon( + icon = viewModel.impl, + tint = Color(colors.text(context)), + modifier = + modifier.size(dimensionResource(id = R.dimen.ongoing_activity_chip_icon_size)), + ) + } + + // TODO(b/372657935): Add recommended architecture implementation for + // StatusBarNotificationIcons + is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> {} + } +} + +@Composable +private fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) { + val context = LocalContext.current + when (viewModel) { + is OngoingActivityChipModel.Shown.Timer -> { + ChronometerText( + startTimeMillis = viewModel.startTimeMs, + style = MaterialTheme.typography.labelLarge, + color = Color(viewModel.colors.text(context)), + modifier = modifier, + ) + } + + is OngoingActivityChipModel.Shown.Countdown -> { + ChipText( + text = viewModel.secondsUntilStarted.toString(), + color = Color(viewModel.colors.text(context)), + style = MaterialTheme.typography.labelLarge, + modifier = modifier.neverDecreaseWidth(), + backgroundColor = Color(viewModel.colors.background(context).defaultColor), + ) + } + + is OngoingActivityChipModel.Shown.Text -> { + ChipText( + text = viewModel.text, + color = Color(viewModel.colors.text(context)), + style = MaterialTheme.typography.labelLarge, + modifier = modifier, + backgroundColor = Color(viewModel.colors.background(context).defaultColor), + ) + } + + is OngoingActivityChipModel.Shown.ShortTimeDelta -> { + // TODO(b/372657935): Implement ShortTimeDelta content in compose. + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt new file mode 100644 index 000000000000..85ea087f531b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.ui.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel + +@Composable +fun OngoingActivityChips(chips: MultipleOngoingActivityChipsModel, modifier: Modifier = Modifier) { + Row( + // TODO(b/372657935): Remove magic numbers for padding and spacing. + modifier = modifier.fillMaxHeight().padding(horizontal = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + // TODO(b/372657935): Make sure chips are only shown when there is enough horizontal + // space. + if (chips.primary is OngoingActivityChipModel.Shown) { + OngoingActivityChip(model = chips.primary) + } + if (chips.secondary is OngoingActivityChipModel.Shown) { + OngoingActivityChip(model = chips.secondary) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/modifiers/NeverDecreaseWidth.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/modifiers/NeverDecreaseWidth.kt new file mode 100644 index 000000000000..505a5fcb18b4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/modifiers/NeverDecreaseWidth.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 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.chips.ui.compose.modifiers + +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.node.LayoutModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.constrain + +/** A modifier that ensures the width of the content only increases and never decreases. */ +fun Modifier.neverDecreaseWidth(): Modifier { + return this.then(neverDecreaseWidthElement) +} + +private data object neverDecreaseWidthElement : ModifierNodeElement<NeverDecreaseWidthNode>() { + override fun create(): NeverDecreaseWidthNode { + return NeverDecreaseWidthNode() + } + + override fun update(node: NeverDecreaseWidthNode) { + error("This should never be called") + } +} + +private class NeverDecreaseWidthNode : Modifier.Node(), LayoutModifierNode { + private var minWidth = 0 + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { + val placeable = measurable.measure(Constraints(minWidth = minWidth).constrain(constraints)) + val width = placeable.width + val height = placeable.height + + minWidth = maxOf(minWidth, width) + + return layout(width, height) { placeable.place(0, 0) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index c81e8e211507..956d99e46766 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.chips.ui.model import android.view.View -import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips @@ -130,10 +129,6 @@ sealed class OngoingActivityChipModel { */ data class StatusBarView(val impl: StatusBarIconView) : ChipIcon { init { - check(Flags.statusBarCallChipNotificationIcon()) { - "OngoingActivityChipModel.ChipIcon.StatusBarView created even though " + - "Flags.statusBarCallChipNotificationIcon is not enabled" - } StatusBarConnectedDisplays.assertInLegacyMode() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt index ff3061e850d9..7b4b79d7c852 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt @@ -33,10 +33,8 @@ import androidx.annotation.UiThread * that wide. This means the chip may get larger over time (e.g. in the transition from 59:59 to * 1:00:00), but never smaller. * 2) Hiding the text if the time gets too long for the space available. Once the text has been - * hidden, it remains hidden for the duration of the activity. - * - * Note that if the text was too big in portrait mode, resulting in the text being hidden, then the - * text will also be hidden in landscape (even if there is enough space for it in landscape). + * hidden, it remains hidden for the duration of the activity (or until [resetWidthRestriction] + * is called). */ class ChipChronometer @JvmOverloads @@ -51,12 +49,23 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : private var shouldHideText: Boolean = false override fun setBase(base: Long) { - // These variables may have changed during the previous activity, so re-set them before the - // new activity starts. + resetWidthRestriction() + super.setBase(base) + } + + /** + * Resets any width restrictions that were placed on the chronometer. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + @UiThread + fun resetWidthRestriction() { minimumTextWidth = 0 shouldHideText = false + // View needs to be visible in order to be re-measured visibility = VISIBLE - super.setBase(base) + forceLayout() } /** Sets whether this view should hide its text or not. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index 254b792f8152..d327fc23fd06 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.dagger; -import static com.android.systemui.Flags.predictiveBackAnimateDialogs; - import android.content.Context; import android.os.Handler; import android.os.RemoteException; @@ -28,7 +26,6 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.CoreStartable; import com.android.systemui.animation.ActivityTransitionAnimator; -import com.android.systemui.animation.AnimationFeatureFlags; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.dagger.SysUISingleton; @@ -226,8 +223,7 @@ public interface CentralSurfacesDependenciesModule { IDreamManager dreamManager, KeyguardStateController keyguardStateController, Lazy<AlternateBouncerInteractor> alternateBouncerInteractor, - InteractionJankMonitor interactionJankMonitor, - AnimationFeatureFlags animationFeatureFlags) { + InteractionJankMonitor interactionJankMonitor) { DialogTransitionAnimator.Callback callback = new DialogTransitionAnimator.Callback() { @Override public boolean isDreaming() { @@ -249,19 +245,6 @@ public interface CentralSurfacesDependenciesModule { return alternateBouncerInteractor.get().canShowAlternateBouncerForFingerprint(); } }; - return new DialogTransitionAnimator( - mainExecutor, callback, interactionJankMonitor, animationFeatureFlags); - } - - /** */ - @Provides - @SysUISingleton - static AnimationFeatureFlags provideAnimationFeatureFlags() { - return new AnimationFeatureFlags() { - @Override - public boolean isPredictiveBackQsDialogAnim() { - return predictiveBackAnimateDialogs(); - } - }; + return new DialogTransitionAnimator(mainExecutor, callback, interactionJankMonitor); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt index eff959d0f83b..351cdc8e7f36 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.data.StatusBarDataLayerModule import com.android.systemui.statusbar.data.repository.LightBarControllerStore import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider import com.android.systemui.statusbar.layout.StatusBarContentInsetsProviderImpl +import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModel import com.android.systemui.statusbar.phone.AutoHideController import com.android.systemui.statusbar.phone.AutoHideControllerImpl import com.android.systemui.statusbar.phone.LightBarController @@ -39,6 +40,7 @@ import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.domain.interactor.OngoingCallInteractor import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.ui.StatusBarUiLayerModule import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore import com.android.systemui.statusbar.window.SingleDisplayStatusBarWindowControllerStore @@ -60,7 +62,14 @@ import dagger.multibindings.IntoMap * ([com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule], * [com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule], etc.). */ -@Module(includes = [StatusBarDataLayerModule::class, SystemBarUtilsProxyImpl.Module::class]) +@Module( + includes = + [ + StatusBarDataLayerModule::class, + StatusBarUiLayerModule::class, + SystemBarUtilsProxyImpl.Module::class, + ] +) interface StatusBarModule { @Binds @@ -169,5 +178,13 @@ interface StatusBarModule { ): StatusBarContentInsetsProvider { return factory.create(context, configurationController, sysUICutoutProvider) } + + @Provides + @SysUISingleton + fun contentInsetsViewModel( + insetsProvider: StatusBarContentInsetsProvider + ): StatusBarContentInsetsViewModel { + return StatusBarContentInsetsViewModel(insetsProvider) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt index 4e68bee295fc..e3e77e16be6d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.shared.model.MediaCommonModel import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.res.R import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -71,5 +72,10 @@ constructor( } private fun MediaData.toMediaControlChipModel(): MediaControlChipModel { - return MediaControlChipModel(appIcon = this.appIcon, appName = this.app, songName = this.song) + return MediaControlChipModel( + appIcon = this.appIcon, + appName = this.app, + songName = this.song, + playOrPause = this.semanticActions?.getActionById(R.id.actionPlayPause), + ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt index 403566749e03..2e47c1eb9eca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt @@ -17,10 +17,12 @@ package com.android.systemui.statusbar.featurepods.media.shared.model import android.graphics.drawable.Icon +import com.android.systemui.media.controls.shared.model.MediaAction /** Model used to display a media control chip in the status bar. */ data class MediaControlChipModel( val appIcon: Icon?, val appName: String?, val songName: CharSequence?, + val playOrPause: MediaAction?, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt index 2aea7d85e01a..19acb2e9839c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt @@ -24,6 +24,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel +import com.android.systemui.statusbar.featurepods.popups.shared.model.HoverBehavior import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipViewModel @@ -33,6 +34,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** * [StatusBarPopupChipViewModel] for a media control chip in the status bar. This view model is @@ -54,40 +56,51 @@ constructor( */ override val chip: StateFlow<PopupChipModel> = mediaControlChipInteractor.mediaControlModel - .map { mediaControlModel -> toPopupChipModel(mediaControlModel, applicationContext) } + .map { mediaControlModel -> toPopupChipModel(mediaControlModel) } .stateIn( backgroundScope, SharingStarted.WhileSubscribed(), PopupChipModel.Hidden(PopupChipId.MediaControl), ) -} -private fun toPopupChipModel(model: MediaControlChipModel?, context: Context): PopupChipModel { - if (model == null || model.songName.isNullOrEmpty()) { - return PopupChipModel.Hidden(PopupChipId.MediaControl) - } + private fun toPopupChipModel(model: MediaControlChipModel?): PopupChipModel { + if (model == null || model.songName.isNullOrEmpty()) { + return PopupChipModel.Hidden(PopupChipId.MediaControl) + } - val contentDescription = model.appName?.let { ContentDescription.Loaded(description = it) } - return PopupChipModel.Shown( - chipId = PopupChipId.MediaControl, - icon = - model.appIcon?.loadDrawable(context)?.let { + val contentDescription = model.appName?.let { ContentDescription.Loaded(description = it) } + + val defaultIcon = + model.appIcon?.loadDrawable(applicationContext)?.let { Icon.Loaded(drawable = it, contentDescription = contentDescription) } ?: Icon.Resource( res = com.android.internal.R.drawable.ic_audio_media, contentDescription = contentDescription, - ), - hoverIcon = - Icon.Resource( - res = com.android.internal.R.drawable.ic_media_pause, - contentDescription = null, - ), - chipText = model.songName.toString(), - isToggled = false, - // TODO(b/385202114): Show a popup containing the media carousal when the chip is toggled. - onToggle = {}, - // TODO(b/385202193): Add support for clicking on the icon on a media chip. - onIconPressed = {}, - ) + ) + return PopupChipModel.Shown( + chipId = PopupChipId.MediaControl, + icon = defaultIcon, + chipText = model.songName.toString(), + isToggled = false, + // TODO(b/385202114): Show a popup containing the media carousal when the chip is + // toggled. + onToggle = {}, + hoverBehavior = createHoverBehavior(model), + ) + } + + private fun createHoverBehavior(model: MediaControlChipModel): HoverBehavior { + val playOrPause = model.playOrPause ?: return HoverBehavior.None + val icon = playOrPause.icon ?: return HoverBehavior.None + val action = playOrPause.action ?: return HoverBehavior.None + + val contentDescription = + ContentDescription.Loaded(description = playOrPause.contentDescription.toString()) + + return HoverBehavior.Button( + icon = Icon.Loaded(drawable = icon, contentDescription = contentDescription), + onIconPressed = { backgroundScope.launch { action.run() } }, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt index e7e3d02ae4c5..683b97166f3e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt @@ -26,6 +26,18 @@ sealed class PopupChipId(val value: String) { data object MediaControl : PopupChipId("MediaControl") } +/** Defines the behavior of the chip when hovered over. */ +sealed interface HoverBehavior { + /** No specific hover behavior. The default icon will be shown. */ + data object None : HoverBehavior + + /** + * Shows a button on hover with the given [icon] and executes [onIconPressed] when the icon is + * pressed. + */ + data class Button(val icon: Icon, val onIconPressed: () -> Unit) : HoverBehavior +} + /** Model for individual status bar popup chips. */ sealed class PopupChipModel { abstract val logName: String @@ -40,15 +52,10 @@ sealed class PopupChipModel { override val chipId: PopupChipId, /** Default icon displayed on the chip */ val icon: Icon, - /** - * Icon to be displayed if the chip is hovered. i.e. the mouse pointer is inside the bounds - * of the chip. - */ - val hoverIcon: Icon, val chipText: String, val isToggled: Boolean = false, val onToggle: () -> Unit, - val onIconPressed: () -> Unit, + val hoverBehavior: HoverBehavior = HoverBehavior.None, ) : PopupChipModel() { override val logName = "Shown(id=$chipId, toggled=$isToggled)" } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt index 1a775d71983c..34bef9d3ca3a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -42,7 +41,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.android.compose.modifiers.thenIf import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.statusbar.featurepods.popups.shared.model.HoverBehavior import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel /** @@ -52,52 +53,59 @@ import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipM */ @Composable fun StatusBarPopupChip(model: PopupChipModel.Shown, modifier: Modifier = Modifier) { - val interactionSource = remember { MutableInteractionSource() } - val isHovered by interactionSource.collectIsHoveredAsState() + val hasHoverBehavior = model.hoverBehavior !is HoverBehavior.None + val hoverInteractionSource = remember { MutableInteractionSource() } + val isHovered by hoverInteractionSource.collectIsHoveredAsState() val isToggled = model.isToggled + val chipBackgroundColor = + if (isToggled) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceContainerHighest + } Surface( shape = RoundedCornerShape(16.dp), modifier = modifier - .hoverable(interactionSource = interactionSource) - .padding(vertical = 4.dp) .widthIn(max = 120.dp) + .padding(vertical = 4.dp) .animateContentSize() - .clickable(onClick = { model.onToggle() }), - color = - if (isToggled) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surfaceContainerHighest - }, + .thenIf(hasHoverBehavior) { Modifier.hoverable(hoverInteractionSource) } + .clickable { model.onToggle() }, + color = chipBackgroundColor, ) { Row( modifier = Modifier.padding(start = 4.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - val currentIcon = if (isHovered) model.hoverIcon else model.icon - val backgroundColor = - if (isToggled) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.primaryContainer - } - + val iconColor = + if (isHovered) chipBackgroundColor else contentColorFor(chipBackgroundColor) + val hoverBehavior = model.hoverBehavior + val iconBackgroundColor = contentColorFor(chipBackgroundColor) + val iconInteractionSource = remember { MutableInteractionSource() } Icon( - icon = currentIcon, + icon = + when { + isHovered && hoverBehavior is HoverBehavior.Button -> hoverBehavior.icon + else -> model.icon + }, modifier = - Modifier.background(color = backgroundColor, shape = CircleShape) - .clickable( - role = Role.Button, - onClick = model.onIconPressed, - indication = ripple(), - interactionSource = remember { MutableInteractionSource() }, - ) - .padding(2.dp) - .size(18.dp), - tint = contentColorFor(backgroundColor), + Modifier.thenIf(isHovered) { + Modifier.padding(3.dp) + .background(color = iconBackgroundColor, shape = CircleShape) + } + .thenIf(hoverBehavior is HoverBehavior.Button) { + Modifier.clickable( + role = Role.Button, + onClick = (hoverBehavior as HoverBehavior.Button).onIconPressed, + indication = ripple(), + interactionSource = iconInteractionSource, + ) + } + .padding(3.dp), + tint = iconColor, ) Text( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt new file mode 100644 index 000000000000..03c07480ecea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import android.graphics.Rect +import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener +import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onStart + +/** A recommended architecture version of [StatusBarContentInsetsProvider]. */ +class StatusBarContentInsetsViewModel( + private val statusBarContentInsetsProvider: StatusBarContentInsetsProvider +) { + /** Emits the status bar content area for the given rotation in absolute bounds. */ + val contentArea: Flow<Rect> = + conflatedCallbackFlow { + val listener = + object : StatusBarContentInsetsChangedListener { + override fun onStatusBarContentInsetsChanged() { + trySend( + statusBarContentInsetsProvider + .getStatusBarContentAreaForCurrentRotation() + ) + } + } + statusBarContentInsetsProvider.addCallback(listener) + awaitClose { statusBarContentInsetsProvider.removeCallback(listener) } + } + .onStart { + emit(statusBarContentInsetsProvider.getStatusBarContentAreaForCurrentRotation()) + } + .distinctUntilChanged() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt new file mode 100644 index 000000000000..d2dccc49ffd7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.display.data.repository.DisplayRepository +import com.android.systemui.display.data.repository.PerDisplayStore +import com.android.systemui.display.data.repository.PerDisplayStoreImpl +import com.android.systemui.display.data.repository.SingleDisplayStore +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore +import dagger.Lazy +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +/** Provides per-display instances of [StatusBarContentInsetsViewModel]. */ +interface StatusBarContentInsetsViewModelStore : PerDisplayStore<StatusBarContentInsetsViewModel> + +@SysUISingleton +class MultiDisplayStatusBarContentInsetsViewModelStore +@Inject +constructor( + @Background backgroundApplicationScope: CoroutineScope, + displayRepository: DisplayRepository, + private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, +) : + StatusBarContentInsetsViewModelStore, + PerDisplayStoreImpl<StatusBarContentInsetsViewModel>( + backgroundApplicationScope, + displayRepository, + ) { + + override fun createInstanceForDisplay(displayId: Int): StatusBarContentInsetsViewModel? { + val insetsProvider = + statusBarContentInsetsProviderStore.forDisplay(displayId) ?: return null + return StatusBarContentInsetsViewModel(insetsProvider) + } + + override val instanceClass = StatusBarContentInsetsViewModel::class.java +} + +@SysUISingleton +class SingleDisplayStatusBarContentInsetsViewModelStore +@Inject +constructor(statusBarContentInsetsViewModel: StatusBarContentInsetsViewModel) : + StatusBarContentInsetsViewModelStore, + PerDisplayStore<StatusBarContentInsetsViewModel> by SingleDisplayStore( + defaultInstance = statusBarContentInsetsViewModel + ) + +@Module +object StatusBarContentInsetsViewModelStoreModule { + @Provides + @SysUISingleton + @IntoMap + @ClassKey(StatusBarContentInsetsViewModelStore::class) + fun storeAsCoreStartable( + multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsViewModelStore> + ): CoreStartable { + return if (StatusBarConnectedDisplays.isEnabled) { + return multiDisplayLazy.get() + } else { + CoreStartable.NOP + } + } + + @Provides + @SysUISingleton + fun store( + singleDisplayLazy: Lazy<SingleDisplayStatusBarContentInsetsViewModelStore>, + multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsViewModelStore>, + ): StatusBarContentInsetsViewModelStore { + return if (StatusBarConnectedDisplays.isEnabled) { + multiDisplayLazy.get() + } else { + singleDisplayLazy.get() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt index 90212ed5b5f7..034a4fd2af72 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt @@ -36,7 +36,7 @@ class DataStoreCoordinator internal constructor(private val notifLiveDataStoreImpl: NotifLiveDataStoreImpl) : CoreCoordinator { override fun attach(pipeline: NotifPipeline) { - pipeline.addOnAfterRenderListListener { entries, _ -> onAfterRenderList(entries) } + pipeline.addOnAfterRenderListListener { entries -> onAfterRenderList(entries) } } override fun dumpPipeline(d: PipelineDumper) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index c7535ec14d5d..eb5a3703bcfb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -112,20 +112,20 @@ constructor( if (StatusBarNotifChips.isEnabled) { applicationScope.launch { statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent.collect { - showPromotedNotificationHeadsUp(it) + onPromotedNotificationChipTapEvent(it) } } } } /** - * Shows the promoted notification with the given [key] as heads-up. + * Updates the heads-up state based on which promoted notification with the given [key] was + * tapped. * * Must be run on the main thread. */ - private fun showPromotedNotificationHeadsUp(key: String) { + private fun onPromotedNotificationChipTapEvent(key: String) { StatusBarNotifChips.assertInNewMode() - mLogger.logShowPromotedNotificationHeadsUp(key) val entry = notifCollection.getEntry(key) if (entry == null) { @@ -135,22 +135,29 @@ constructor( // TODO(b/364653005): Validate that the given key indeed matches a promoted notification, // not just any notification. + val isCurrentlyHeadsUp = mHeadsUpManager.isHeadsUpEntry(entry.key) val posted = PostedEntry( entry, wasAdded = false, wasUpdated = false, - // Force-set this notification to show heads-up. - shouldHeadsUpEver = true, - shouldHeadsUpAgain = true, + // We want the chip to act as a toggle, so if the chip's notification is currently + // showing as heads up, then we should stop showing it. + shouldHeadsUpEver = !isCurrentlyHeadsUp, + shouldHeadsUpAgain = !isCurrentlyHeadsUp, isPinnedByUser = true, - isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key), + isHeadsUpEntry = isCurrentlyHeadsUp, isBinding = isEntryBinding(entry), ) + if (isCurrentlyHeadsUp) { + mLogger.logHidePromotedNotificationHeadsUp(key) + } else { + mLogger.logShowPromotedNotificationHeadsUp(key) + } mExecutor.execute { mPostedEntries[entry.key] = posted - mNotifPromoter.invalidateList("showPromotedNotificationHeadsUp: ${entry.logKey}") + mNotifPromoter.invalidateList("onPromotedNotificationChipTapEvent: ${entry.logKey}") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt index e443a0418ffd..5141aa35b041 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt @@ -148,6 +148,15 @@ class HeadsUpCoordinatorLogger(private val buffer: LogBuffer, private val verbos ) } + fun logHidePromotedNotificationHeadsUp(key: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "requesting promoted entry to hide heads up: $str1" }, + ) + } + fun logPromotedNotificationForHeadsUpNotFound(key: String) { buffer.log( TAG, 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 32de65be5b5b..1cb2366a16fe 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 @@ -23,11 +23,9 @@ import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl -import com.android.systemui.statusbar.notification.collection.render.NotifStackController -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController import javax.inject.Inject @@ -43,7 +41,8 @@ internal constructor( private val groupExpansionManagerImpl: GroupExpansionManagerImpl, private val renderListInteractor: RenderNotificationListInteractor, private val activeNotificationsInteractor: ActiveNotificationsInteractor, - private val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController, + private val sensitiveNotificationProtectionController: + SensitiveNotificationProtectionController, ) : Coordinator { override fun attach(pipeline: NotifPipeline) { @@ -51,14 +50,10 @@ internal constructor( groupExpansionManagerImpl.attach(pipeline) } - private fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) = + private fun onAfterRenderList(entries: List<ListEntry>) = traceSection("StackCoordinator.onAfterRenderList") { val notifStats = calculateNotifStats(entries) - if (FooterViewRefactor.isEnabled) { - activeNotificationsInteractor.setNotifStats(notifStats) - } else { - controller.setNotifStats(notifStats) - } + activeNotificationsInteractor.setNotifStats(notifStats) renderListInteractor.setRenderedList(entries) } @@ -87,7 +82,6 @@ internal constructor( } } return NotifStats( - numActiveNotifs = entries.size, hasNonClearableAlertingNotifs = hasNonClearableAlertingNotifs, hasClearableAlertingNotifs = hasClearableAlertingNotifs, hasNonClearableSilentNotifs = hasNonClearableSilentNotifs, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java index a34d033afcaa..c58b3febe54b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java @@ -33,7 +33,6 @@ import com.android.systemui.statusbar.notification.collection.ShadeListBuilder; import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer; import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinators; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; -import com.android.systemui.statusbar.notification.collection.render.NotifStackController; import com.android.systemui.statusbar.notification.collection.render.RenderStageManager; import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager; import com.android.systemui.statusbar.notification.collection.render.ShadeViewManagerFactory; @@ -89,8 +88,7 @@ public class NotifPipelineInitializer implements Dumpable, PipelineDumpable { public void initialize( NotificationListener notificationService, NotificationRowBinderImpl rowBinder, - NotificationListContainer listContainer, - NotifStackController stackController) { + NotificationListContainer listContainer) { mDumpManager.registerDumpable("NotifPipeline", this); mNotificationService = notificationService; @@ -102,7 +100,7 @@ public class NotifPipelineInitializer implements Dumpable, PipelineDumpable { mNotifPluggableCoordinators.attach(mPipelineWrapper); // Wire up pipeline - mShadeViewManager = mShadeViewManagerFactory.create(listContainer, stackController); + mShadeViewManager = mShadeViewManagerFactory.create(listContainer); mShadeViewManager.attach(mRenderStageManager); mRenderStageManager.attach(mListBuilder); mListBuilder.attach(mNotifCollection); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java index b5a0f7ae169d..ac450c03b850 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java @@ -20,7 +20,6 @@ import androidx.annotation.NonNull; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.render.NotifStackController; import java.util.List; @@ -31,9 +30,6 @@ public interface OnAfterRenderListListener { * * @param entries The current list of top-level entries. Note that this is a live view into the * current list and will change whenever the pipeline is rerun. - * @param controller An object for setting state on the shade. */ - void onAfterRenderList( - @NonNull List<ListEntry> entries, - @NonNull NotifStackController controller); + void onAfterRenderList(@NonNull List<ListEntry> entries); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt deleted file mode 100644 index a37937a6c495..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.collection.render - -import javax.inject.Inject - -/** An interface by which the pipeline can make updates to the notification root view. */ -interface NotifStackController { - /** Provides stats about the list of notifications attached to the shade */ - fun setNotifStats(stats: NotifStats) -} - -/** Data provided to the NotificationRootController whenever the pipeline runs */ -data class NotifStats( - // TODO(b/293167744): The count can be removed from here when we remove the FooterView flag. - val numActiveNotifs: Int, - val hasNonClearableAlertingNotifs: Boolean, - val hasClearableAlertingNotifs: Boolean, - val hasNonClearableSilentNotifs: Boolean, - val hasClearableSilentNotifs: Boolean -) { - companion object { - @JvmStatic val empty = NotifStats(0, false, false, false, false) - } -} - -/** - * An implementation of NotifStackController which provides default, no-op implementations of each - * method. This is used by ArcSystemUI so that that implementation can opt-in to overriding methods, - * rather than forcing us to add no-op implementations in their implementation every time a method - * is added. - */ -open class DefaultNotifStackController @Inject constructor() : NotifStackController { - override fun setNotifStats(stats: NotifStats) {} -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt index 410b78b9d3bf..8284022c7270 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt @@ -37,12 +37,6 @@ interface NotifViewRenderer { fun onRenderList(notifList: List<ListEntry>) /** - * Provides an interface for the pipeline to update the overall shade. This will be called at - * most once for each time [onRenderList] is called. - */ - fun getStackController(): NotifStackController - - /** * Provides an interface for the pipeline to update individual groups. This will be called at * most once for each group in the most recent call to [onRenderList]. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt index 9d3b098fa966..21e68376031c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt @@ -50,7 +50,7 @@ class RenderStageManager @Inject constructor() : PipelineDumpable { traceSection("RenderStageManager.onRenderList") { val viewRenderer = viewRenderer ?: return viewRenderer.onRenderList(notifList) - dispatchOnAfterRenderList(viewRenderer, notifList) + dispatchOnAfterRenderList(notifList) dispatchOnAfterRenderGroups(viewRenderer, notifList) dispatchOnAfterRenderEntries(viewRenderer, notifList) viewRenderer.onDispatchComplete() @@ -85,15 +85,9 @@ class RenderStageManager @Inject constructor() : PipelineDumpable { dump("onAfterRenderEntryListeners", onAfterRenderEntryListeners) } - private fun dispatchOnAfterRenderList( - viewRenderer: NotifViewRenderer, - entries: List<ListEntry>, - ) { + private fun dispatchOnAfterRenderList(entries: List<ListEntry>) { traceSection("RenderStageManager.dispatchOnAfterRenderList") { - val stackController = viewRenderer.getStackController() - onAfterRenderListListeners.forEach { listener -> - listener.onAfterRenderList(entries, stackController) - } + onAfterRenderListListeners.forEach { listener -> listener.onAfterRenderList(entries) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt index 3c838e5b707e..72316bf14c9a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt @@ -41,7 +41,6 @@ class ShadeViewManager constructor( @ShadeDisplayAware context: Context, @Assisted listContainer: NotificationListContainer, - @Assisted private val stackController: NotifStackController, mediaContainerController: MediaContainerController, featureManager: NotificationSectionsFeatureManager, sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, @@ -83,8 +82,6 @@ constructor( } } - override fun getStackController(): NotifStackController = stackController - override fun getGroupController(group: GroupEntry): NotifGroupController = viewBarn.requireGroupController(group.requireSummary) @@ -95,8 +92,5 @@ constructor( @AssistedFactory interface ShadeViewManagerFactory { - fun create( - listContainer: NotificationListContainer, - stackController: NotifStackController, - ): ShadeViewManager + fun create(listContainer: NotificationListContainer): ShadeViewManager } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt new file mode 100644 index 000000000000..d7fd7025a94f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.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.systemui.statusbar.notification.data.model + +/** Information about the current list of notifications. */ +data class NotifStats( + val hasNonClearableAlertingNotifs: Boolean, + val hasClearableAlertingNotifs: Boolean, + val hasNonClearableSilentNotifs: Boolean, + val hasClearableSilentNotifs: Boolean, +) { + companion object { + @JvmStatic + val empty = + NotifStats( + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = false, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = false, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt index 2b9e49372a63..70f06ebe8468 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.data.repository import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore.Key import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt index 6b93ee1c435e..0c040c855368 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt @@ -18,7 +18,7 @@ package com.android.systemui.statusbar.notification.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt index 75c7d2d5be98..6140c92369b3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt @@ -25,6 +25,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey import javax.inject.Inject @@ -98,21 +99,39 @@ constructor( } } - /** What [PinnedStatus] does the top row have? */ - private val topPinnedStatus: Flow<PinnedStatus> = + /** What [PinnedStatus] and key does the top row have? */ + private val topPinnedState: Flow<TopPinnedState> = headsUpRepository.activeHeadsUpRows.flatMapLatest { rows -> if (rows.isNotEmpty()) { - combine(rows.map { it.pinnedStatus }) { pinnedStatus -> - pinnedStatus.firstOrNull { it.isPinned } ?: PinnedStatus.NotPinned + // For each row, emits a (key, pinnedStatus) pair each time any row's + // `pinnedStatus` changes + val toCombine: List<Flow<Pair<String, PinnedStatus>>> = + rows.map { row -> row.pinnedStatus.map { status -> row.key to status } } + combine(toCombine) { pairs -> + val topPinnedRow: Pair<String, PinnedStatus>? = + pairs.firstOrNull { it.second.isPinned } + if (topPinnedRow != null) { + TopPinnedState.Pinned( + key = topPinnedRow.first, + status = topPinnedRow.second, + ) + } else { + TopPinnedState.NothingPinned + } } } else { - // if the set is empty, there are no flows to combine - flowOf(PinnedStatus.NotPinned) + flowOf(TopPinnedState.NothingPinned) } } /** Are there any pinned heads up rows to display? */ - val hasPinnedRows: Flow<Boolean> = topPinnedStatus.map { it.isPinned } + val hasPinnedRows: Flow<Boolean> = + topPinnedState.map { + when (it) { + is TopPinnedState.Pinned -> it.status.isPinned + is TopPinnedState.NothingPinned -> false + } + } val isHeadsUpOrAnimatingAway: Flow<Boolean> by lazy { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { @@ -142,13 +161,25 @@ constructor( } } - /** Emits the pinned notification state as it relates to the status bar. */ - val statusBarHeadsUpState: Flow<PinnedStatus> = - combine(topPinnedStatus, canShowHeadsUp) { topPinnedStatus, canShowHeadsUp -> + /** + * Emits the pinned notification state as it relates to the status bar. Includes both the pinned + * status and key of the notification that's pinned (if there is a pinned notification). + */ + val statusBarHeadsUpState: Flow<TopPinnedState> = + combine(topPinnedState, canShowHeadsUp) { topPinnedState, canShowHeadsUp -> if (canShowHeadsUp) { - topPinnedStatus + topPinnedState } else { - PinnedStatus.NotPinned + TopPinnedState.NothingPinned + } + } + + /** Emits the pinned notification status as it relates to the status bar. */ + val statusBarHeadsUpStatus: Flow<PinnedStatus> = + statusBarHeadsUpState.map { + when (it) { + is TopPinnedState.Pinned -> it.status + is TopPinnedState.NothingPinned -> PinnedStatus.NotPinned } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt index 042389f7fde7..fd5973e0ab3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt @@ -25,7 +25,6 @@ import android.graphics.drawable.Icon import android.service.notification.StatusBarNotification import android.util.ArrayMap import com.android.app.tracing.traceSection -import com.android.systemui.Flags import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry @@ -132,12 +131,6 @@ private class ActiveNotificationsStoreBuilder( } private fun NotificationEntry.toModel(): ActiveNotificationModel { - val statusBarChipIcon = - if (Flags.statusBarCallChipNotificationIcon()) { - icons.statusBarChipIcon - } else { - null - } val promotedContent = if (PromotedNotificationContentModel.featureFlagEnabled()) { promotedNotificationContentModel @@ -158,7 +151,7 @@ private class ActiveNotificationsStoreBuilder( aodIcon = icons.aodIcon?.sourceIcon, shelfIcon = icons.shelfIcon?.sourceIcon, statusBarIcon = icons.statusBarIcon?.sourceIcon, - statusBarChipIconView = statusBarChipIcon, + statusBarChipIconView = icons.statusBarChipIcon, uid = sbn.uid, packageName = sbn.packageName, contentIntent = sbn.notification.contentIntent, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt new file mode 100644 index 000000000000..51c448adf998 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.domain.model + +import com.android.systemui.statusbar.notification.headsup.PinnedStatus + +/** A class representing the state of the top pinned row. */ +sealed interface TopPinnedState { + /** Nothing is pinned. */ + data object NothingPinned : TopPinnedState + + /** + * The top pinned row is a notification with the given key and status. + * + * @property status must have [PinnedStatus.isPinned] as true. + */ + data class Pinned(val key: String, val status: PinnedStatus) : TopPinnedState { + init { + check(status.isPinned) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt index fbec6406e9d4..7e2361f24da9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt @@ -26,7 +26,6 @@ import com.android.systemui.shared.notifications.domain.interactor.NotificationS import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterMessageViewModel import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.util.kotlin.FlowDumperImpl @@ -35,7 +34,6 @@ import dagger.assisted.AssistedInject import java.util.Locale import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf @@ -57,9 +55,7 @@ constructor( dumpManager: DumpManager, ) : FlowDumperImpl(dumpManager) { val areNotificationsHiddenInShade: Flow<Boolean> by lazy { - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - flowOf(false) - } else if (ModesEmptyShadeFix.isEnabled) { + if (ModesEmptyShadeFix.isEnabled) { zenModeInteractor.areNotificationsHiddenInShade .dumpWhileCollecting("areNotificationsHiddenInShade") .flowOn(bgDispatcher) @@ -70,15 +66,10 @@ constructor( } } - val hasFilteredOutSeenNotifications: StateFlow<Boolean> by lazy { - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - MutableStateFlow(false) - } else { - seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpValue( - "hasFilteredOutSeenNotifications" - ) - } - } + val hasFilteredOutSeenNotifications: StateFlow<Boolean> = + seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpValue( + "hasFilteredOutSeenNotifications" + ) val text: Flow<String> by lazy { if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt deleted file mode 100644 index 7e6044eb6869..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt +++ /dev/null @@ -1,53 +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.statusbar.notification.footer.shared - -import com.android.systemui.Flags -import com.android.systemui.flags.FlagToken -import com.android.systemui.flags.RefactorFlagUtils - -/** Helper for reading or using the FooterView refactor flag state. */ -@Suppress("NOTHING_TO_INLINE") -object FooterViewRefactor { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the refactor enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.notificationsFooterViewRefactor() - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an eng - * build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception if - * the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java index d25889820629..a670f69df601 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java @@ -41,7 +41,6 @@ import androidx.annotation.NonNull; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.ColorUpdateLogger; -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter; import com.android.systemui.statusbar.notification.row.FooterViewButton; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; @@ -63,16 +62,9 @@ public class FooterView extends StackScrollerDecorView { private FooterViewButton mSettingsButton; private FooterViewButton mHistoryButton; private boolean mShouldBeHidden; - private boolean mShowHistory; - // String cache, for performance reasons. - // Reading them from a Resources object can be quite slow sometimes. - private String mManageNotificationText; - private String mManageNotificationHistoryText; // Footer label private TextView mSeenNotifsFooterTextView; - private String mSeenNotifsFilteredText; - private Drawable mSeenNotifsFilteredIcon; private @StringRes int mClearAllButtonTextId; private @StringRes int mClearAllButtonDescriptionId; @@ -159,8 +151,8 @@ public class FooterView extends StackScrollerDecorView { IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal); super.dump(pw, args); DumpUtilsKt.withIncreasedIndent(pw, () -> { + // TODO: b/375010573 - update dumps for redesign pw.println("visibility: " + DumpUtilsKt.visibilityString(getVisibility())); - pw.println("manageButton showHistory: " + mShowHistory); pw.println("manageButton visibility: " + DumpUtilsKt.visibilityString(mClearAllButton.getVisibility())); pw.println("dismissButton visibility: " @@ -170,7 +162,6 @@ public class FooterView extends StackScrollerDecorView { /** Set the text label for the "Clear all" button. */ public void setClearAllButtonText(@StringRes int textId) { - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return; if (mClearAllButtonTextId == textId) { return; // nothing changed } @@ -187,9 +178,6 @@ public class FooterView extends StackScrollerDecorView { /** Set the accessibility content description for the "Clear all" button. */ public void setClearAllButtonDescription(@StringRes int contentDescriptionId) { - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - return; - } if (mClearAllButtonDescriptionId == contentDescriptionId) { return; // nothing changed } @@ -207,7 +195,6 @@ public class FooterView extends StackScrollerDecorView { /** Set the text label for the "Manage"/"History" button. */ public void setManageOrHistoryButtonText(@StringRes int textId) { NotifRedesignFooter.assertInLegacyMode(); - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return; if (mManageOrHistoryButtonTextId == textId) { return; // nothing changed } @@ -226,9 +213,6 @@ public class FooterView extends StackScrollerDecorView { /** Set the accessibility content description for the "Clear all" button. */ public void setManageOrHistoryButtonDescription(@StringRes int contentDescriptionId) { NotifRedesignFooter.assertInLegacyMode(); - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - return; - } if (mManageOrHistoryButtonDescriptionId == contentDescriptionId) { return; // nothing changed } @@ -247,7 +231,6 @@ public class FooterView extends StackScrollerDecorView { /** Set the string for a message to be shown instead of the buttons. */ public void setMessageString(@StringRes int messageId) { - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return; if (mMessageStringId == messageId) { return; // nothing changed } @@ -265,7 +248,6 @@ public class FooterView extends StackScrollerDecorView { /** Set the icon to be shown before the message (see {@link #setMessageString(int)}). */ public void setMessageIcon(@DrawableRes int iconId) { - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return; if (mMessageIconId == iconId) { return; // nothing changed } @@ -303,32 +285,17 @@ public class FooterView extends StackScrollerDecorView { mManageOrHistoryButton = findViewById(R.id.manage_text); } mSeenNotifsFooterTextView = findViewById(R.id.unlock_prompt_footer); - if (!FooterViewRefactor.isEnabled()) { - updateResources(); - } updateContent(); updateColors(); } /** Show a message instead of the footer buttons. */ public void setFooterLabelVisible(boolean isVisible) { - // In the refactored code, hiding the buttons is handled in the FooterViewModel - if (FooterViewRefactor.isEnabled()) { - if (isVisible) { - mSeenNotifsFooterTextView.setVisibility(View.VISIBLE); - } else { - mSeenNotifsFooterTextView.setVisibility(View.GONE); - } + // Note: hiding the buttons is handled in the FooterViewModel + if (isVisible) { + mSeenNotifsFooterTextView.setVisibility(View.VISIBLE); } else { - if (isVisible) { - mManageOrHistoryButton.setVisibility(View.GONE); - mClearAllButton.setVisibility(View.GONE); - mSeenNotifsFooterTextView.setVisibility(View.VISIBLE); - } else { - mManageOrHistoryButton.setVisibility(View.VISIBLE); - mClearAllButton.setVisibility(View.VISIBLE); - mSeenNotifsFooterTextView.setVisibility(View.GONE); - } + mSeenNotifsFooterTextView.setVisibility(View.GONE); } } @@ -359,10 +326,8 @@ public class FooterView extends StackScrollerDecorView { /** Set onClickListener for the clear all (end) button. */ public void setClearAllButtonClickListener(OnClickListener listener) { - if (FooterViewRefactor.isEnabled()) { - if (mClearAllButtonClickListener == listener) return; - mClearAllButtonClickListener = listener; - } + if (mClearAllButtonClickListener == listener) return; + mClearAllButtonClickListener = listener; mClearAllButton.setOnClickListener(listener); } @@ -379,62 +344,17 @@ public class FooterView extends StackScrollerDecorView { || touchY > mContent.getY() + mContent.getHeight(); } - /** Show "History" instead of "Manage" on the start button. */ - public void showHistory(boolean showHistory) { - FooterViewRefactor.assertInLegacyMode(); - if (mShowHistory == showHistory) { - return; - } - mShowHistory = showHistory; - updateContent(); - } - private void updateContent() { - if (FooterViewRefactor.isEnabled()) { - updateClearAllButtonText(); - updateClearAllButtonDescription(); - - if (!NotifRedesignFooter.isEnabled()) { - updateManageOrHistoryButtonText(); - updateManageOrHistoryButtonDescription(); - } - - updateMessageString(); - updateMessageIcon(); - } else { - // NOTE: Prior to the refactor, `updateResources` set the class properties to the right - // string values. It was always being called together with `updateContent`, which - // deals with actually associating those string values with the correct views - // (buttons or text). - // In the new code, the resource IDs are being set in the view binder (through - // setMessageString and similar setters). The setters themselves now deal with - // updating both the resource IDs and the views where appropriate (as in, calling - // `updateMessageString` when the resource ID changes). This eliminates the need for - // `updateResources`, which will eventually be removed. There are, however, still - // situations in which we want to update the views even if the resource IDs didn't - // change, such as configuration changes. - if (mShowHistory) { - mManageOrHistoryButton.setText(mManageNotificationHistoryText); - mManageOrHistoryButton.setContentDescription(mManageNotificationHistoryText); - } else { - mManageOrHistoryButton.setText(mManageNotificationText); - mManageOrHistoryButton.setContentDescription(mManageNotificationText); - } - - mClearAllButton.setText(R.string.clear_all_notifications_text); - mClearAllButton.setContentDescription( - mContext.getString(R.string.accessibility_clear_all)); + updateClearAllButtonText(); + updateClearAllButtonDescription(); - mSeenNotifsFooterTextView.setText(mSeenNotifsFilteredText); - mSeenNotifsFooterTextView - .setCompoundDrawablesRelative(mSeenNotifsFilteredIcon, null, null, null); + if (!NotifRedesignFooter.isEnabled()) { + updateManageOrHistoryButtonText(); + updateManageOrHistoryButtonDescription(); } - } - /** Whether the start button shows "History" (true) or "Manage" (false). */ - public boolean isHistoryShown() { - FooterViewRefactor.assertInLegacyMode(); - return mShowHistory; + updateMessageString(); + updateMessageIcon(); } @Override @@ -445,9 +365,6 @@ public class FooterView extends StackScrollerDecorView { } super.onConfigurationChanged(newConfig); updateColors(); - if (!FooterViewRefactor.isEnabled()) { - updateResources(); - } updateContent(); } @@ -502,18 +419,6 @@ public class FooterView extends StackScrollerDecorView { } } - private void updateResources() { - FooterViewRefactor.assertInLegacyMode(); - mManageNotificationText = getContext().getString(R.string.manage_notifications_text); - mManageNotificationHistoryText = getContext() - .getString(R.string.manage_notifications_history_text); - int unlockIconSize = getResources() - .getDimensionPixelSize(R.dimen.notifications_unseen_footer_icon_size); - mSeenNotifsFilteredText = getContext().getString(R.string.unlock_to_see_notif_text); - mSeenNotifsFilteredIcon = getContext().getDrawable(R.drawable.ic_friction_lock_closed); - mSeenNotifsFilteredIcon.setBounds(0, 0, unlockIconSize, unlockIconSize); - } - @Override @NonNull public ExpandableViewState createExpandableViewState() { 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 e724935e3ef4..5696e9f0c5a2 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 @@ -27,7 +27,6 @@ import com.android.systemui.statusbar.notification.NotificationActivityStarter.S import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.util.kotlin.sample import com.android.systemui.util.ui.AnimatableEvent @@ -144,6 +143,7 @@ class FooterViewModel( ) } +// TODO: b/293167744 - remove this, use new viewmodel style @Module object FooterViewModelModule { @Provides @@ -153,18 +153,13 @@ object FooterViewModelModule { notificationSettingsInteractor: Provider<NotificationSettingsInteractor>, seenNotificationsInteractor: Provider<SeenNotificationsInteractor>, shadeInteractor: Provider<ShadeInteractor>, - ): Optional<FooterViewModel> { - return if (FooterViewRefactor.isEnabled) { - Optional.of( - FooterViewModel( - activeNotificationsInteractor.get(), - notificationSettingsInteractor.get(), - seenNotificationsInteractor.get(), - shadeInteractor.get(), - ) + ): Optional<FooterViewModel> = + Optional.of( + FooterViewModel( + activeNotificationsInteractor.get(), + notificationSettingsInteractor.get(), + seenNotificationsInteractor.get(), + shadeInteractor.get(), ) - } else { - Optional.empty() - } - } + ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index b56a838a80a5..31375cc4a03a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -162,9 +162,7 @@ constructor( val sbIcon = iconBuilder.createIconView(entry) sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE val sbChipIcon: StatusBarIconView? - if ( - Flags.statusBarCallChipNotificationIcon() && !StatusBarConnectedDisplays.isEnabled - ) { + if (!StatusBarConnectedDisplays.isEnabled) { sbChipIcon = iconBuilder.createIconView(entry) sbChipIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE } else { @@ -186,7 +184,7 @@ constructor( try { setIcon(entry, normalIconDescriptor, sbIcon) - if (Flags.statusBarCallChipNotificationIcon() && sbChipIcon != null) { + if (sbChipIcon != null) { setIcon(entry, normalIconDescriptor, sbChipIcon) } setIcon(entry, sensitiveIconDescriptor, shelfIcon) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt index 2c5d9c2e449b..3c2051f0b153 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt @@ -20,7 +20,6 @@ import android.service.notification.StatusBarNotification import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.NotificationActivityStarter -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.stack.NotificationListContainer /** @@ -33,7 +32,6 @@ interface NotificationsController { fun initialize( presenter: NotificationPresenter, listContainer: NotificationListContainer, - stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index ea6a60bd7a1c..0a9899e88d24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -34,7 +34,6 @@ import com.android.systemui.statusbar.notification.collection.inflation.Notifica import com.android.systemui.statusbar.notification.collection.init.NotifPipelineInitializer import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.logging.NotificationLogger import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer @@ -76,7 +75,6 @@ constructor( override fun initialize( presenter: NotificationPresenter, listContainer: NotificationListContainer, - stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, ) { notificationListener.registerAsSystemService() @@ -101,7 +99,7 @@ constructor( notifPipelineInitializer .get() - .initialize(notificationListener, notificationRowBinder, listContainer, stackController) + .initialize(notificationListener, notificationRowBinder, listContainer) targetSdkResolver.initialize(notifPipeline.get()) notificationsMediaManager.setUpWithPresenter(presenter) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt index 148b3f021643..92d96f9e899b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt @@ -21,7 +21,6 @@ import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.Snoo import com.android.systemui.statusbar.NotificationListener import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.NotificationActivityStarter -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.stack.NotificationListContainer import javax.inject.Inject @@ -35,7 +34,6 @@ constructor(private val notificationListener: NotificationListener) : Notificati override fun initialize( presenter: NotificationPresenter, listContainer: NotificationListContainer, - stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, ) { // Always connect the listener even if notification-handling is disabled. Being a listener diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java index c6832bc20e6d..cc4be57168cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java @@ -20,7 +20,6 @@ import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; -import android.os.Trace; import android.service.notification.NotificationListenerService; import android.util.ArrayMap; import android.util.ArraySet; @@ -29,6 +28,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.IStatusBarService; @@ -152,8 +152,8 @@ public class NotificationLogger implements StateListener, CoreStartable, mExpansionStateLogger.onVisibilityChanged( mTmpCurrentlyVisibleNotifications, mTmpCurrentlyVisibleNotifications); - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", N); - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]", + TrackTracer.instantForGroup("Notifications", "Active", N); + TrackTracer.instantForGroup("Notifications", "Visible", mCurrentlyVisibleNotifications.size()); recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt index 10e67a40ebc9..640d364895ae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt @@ -25,7 +25,7 @@ import android.app.NotificationManager.IMPORTANCE_NONE import android.app.NotificationManager.IMPORTANCE_UNSPECIFIED import android.content.Context import android.graphics.drawable.Drawable -import android.text.TextUtils +import android.text.TextUtils.isEmpty import android.transition.AutoTransition import android.transition.Transition import android.transition.TransitionManager @@ -37,13 +37,10 @@ import android.widget.LinearLayout import android.widget.Switch import android.widget.TextView import com.android.settingslib.Utils - import com.android.systemui.res.R import com.android.systemui.util.Assert -/** - * Half-shelf for notification channel controls - */ +/** Half-shelf for notification channel controls */ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { lateinit var controller: ChannelEditorDialogController var appIcon: Drawable? = null @@ -84,23 +81,21 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a val transition = AutoTransition() transition.duration = 200 - transition.addListener(object : Transition.TransitionListener { - override fun onTransitionEnd(p0: Transition?) { - notifySubtreeAccessibilityStateChangedIfNeeded() - } + transition.addListener( + object : Transition.TransitionListener { + override fun onTransitionEnd(p0: Transition?) { + notifySubtreeAccessibilityStateChangedIfNeeded() + } - override fun onTransitionResume(p0: Transition?) { - } + override fun onTransitionResume(p0: Transition?) {} - override fun onTransitionPause(p0: Transition?) { - } + override fun onTransitionPause(p0: Transition?) {} - override fun onTransitionCancel(p0: Transition?) { - } + override fun onTransitionCancel(p0: Transition?) {} - override fun onTransitionStart(p0: Transition?) { + override fun onTransitionStart(p0: Transition?) {} } - }) + ) TransitionManager.beginDelayedTransition(this, transition) // Remove any rows @@ -130,8 +125,9 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a private fun updateAppControlRow(enabled: Boolean) { appControlRow.iconView.setImageDrawable(appIcon) - appControlRow.channelName.text = context.resources - .getString(R.string.notification_channel_dialog_title, appName) + val title = context.resources.getString(R.string.notification_channel_dialog_title, appName) + appControlRow.channelName.text = title + appControlRow.switch.contentDescription = title appControlRow.switch.isChecked = enabled appControlRow.switch.setOnCheckedChangeListener { _, b -> controller.proposeSetAppNotificationsEnabled(b) @@ -164,8 +160,8 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { var gentle = false init { - highlightColor = Utils.getColorAttrDefaultColor( - context, android.R.attr.colorControlHighlight) + highlightColor = + Utils.getColorAttrDefaultColor(context, android.R.attr.colorControlHighlight) } var channel: NotificationChannel? = null @@ -182,17 +178,16 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { switch = requireViewById(R.id.toggle) switch.setOnCheckedChangeListener { _, b -> channel?.let { - controller.proposeEditForChannel(it, - if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW) - else IMPORTANCE_NONE) + controller.proposeEditForChannel( + it, + if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW) else IMPORTANCE_NONE, + ) } } setOnClickListener { switch.toggle() } } - /** - * Play an animation that highlights this row - */ + /** Play an animation that highlights this row */ fun playHighlight() { // Use 0 for the start value because our background is given to us by our parent val fadeInLoop = ValueAnimator.ofObject(ArgbEvaluator(), 0, highlightColor) @@ -211,17 +206,21 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { channelName.text = nc.name ?: "" - nc.group?.let { groupId -> - channelDescription.text = controller.groupNameForId(groupId) - } + nc.group?.let { groupId -> channelDescription.text = controller.groupNameForId(groupId) } - if (nc.group == null || TextUtils.isEmpty(channelDescription.text)) { + if (nc.group == null || isEmpty(channelDescription.text)) { channelDescription.visibility = View.GONE } else { channelDescription.visibility = View.VISIBLE } switch.isChecked = nc.importance != IMPORTANCE_NONE + switch.contentDescription = + if (isEmpty(channelDescription.text)) { + channelName.text + } else { + "${channelName.text} ${channelDescription.text}" + } } private fun updateImportance() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 7e3d0043b91a..95604c113a15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -1267,6 +1267,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } if (mExpandedWhenPinned) { return Math.max(getMaxExpandHeight(), getHeadsUpHeight()); + } else if (android.app.Flags.compactHeadsUpNotification() + && getShowingLayout().isHUNCompact()) { + return getHeadsUpHeight(); } else if (atLeastMinHeight) { return Math.max(getCollapsedHeight(), getHeadsUpHeight()); } else { @@ -3680,6 +3683,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return super.disallowSingleClick(event); } + // TODO: b/388470175 - Although this does get triggered when a notification + // is expanded by the system (e.g. the first notication in the shade), it + // will not be when a notification is collapsed by the system (such as when + // the shade is closed). private void onExpansionChanged(boolean userAction, boolean wasExpanded) { boolean nowExpanded = isExpanded(); if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt new file mode 100644 index 000000000000..e27ff7d6746b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import android.content.Context +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.drawable.Drawable + +/** + * A background style for smarter-smart-actions. + * + * TODO(b/383567383) implement final UX + */ +class MagicActionBackgroundDrawable(context: Context) : Drawable() { + + private var _alpha: Int = 255 + private var _colorFilter: ColorFilter? = null + private val paint = + Paint().apply { + color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer) + } + + override fun draw(canvas: Canvas) { + canvas.drawRect(bounds, paint) + } + + override fun setAlpha(alpha: Int) { + _alpha = alpha + invalidateSelf() + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + _colorFilter = colorFilter + invalidateSelf() + } + + override fun getOpacity(): Int = PixelFormat.TRANSLUCENT +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 7c44eae6c0b8..70e27a981b49 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.notification.row; -import static android.app.Flags.notificationsRedesignTemplates; - import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; @@ -481,16 +479,15 @@ public class NotificationContentInflater implements NotificationRowContentBinder logger.logAsyncTaskProgress(entryForLogging, "creating low-priority group summary remote view"); result.mNewMinimizedGroupHeaderView = - builder.makeLowPriorityContentView(/* useRegularSubtext = */ true, - /* highlightExpander = */ notificationsRedesignTemplates()); + builder.makeLowPriorityContentView(true /* useRegularSubtext */); } } setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider); result.packageContext = packageContext; result.headsUpStatusBarText = builder.getHeadsUpStatusBarText( - /* showingPublic = */ false); + false /* showingPublic */); result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText( - /* showingPublic = */ true); + true /* showingPublic */); return result; }); @@ -1139,8 +1136,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private static RemoteViews createContentView(Notification.Builder builder, boolean isMinimized, boolean useLarge) { if (isMinimized) { - return builder.makeLowPriorityContentView(/* useRegularSubtext = */ false, - /* highlightExpander = */ false); + return builder.makeLowPriorityContentView(false /* useRegularSubtext */); } return builder.createContentView(useLarge); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 786d7d9ea0f3..0d2998174121 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -207,6 +207,8 @@ public class NotificationContentView extends FrameLayout implements Notification private boolean mContentAnimating; private UiEventLogger mUiEventLogger; + private boolean mIsHUNCompact; + public NotificationContentView(Context context, AttributeSet attrs) { super(context, attrs); mHybridGroupManager = new HybridGroupManager(getContext()); @@ -543,6 +545,7 @@ public class NotificationContentView extends FrameLayout implements Notification if (child == null) { mHeadsUpChild = null; mHeadsUpWrapper = null; + mIsHUNCompact = false; if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) { mTransformationStartVisibleType = VISIBLE_TYPE_NONE; } @@ -556,8 +559,9 @@ public class NotificationContentView extends FrameLayout implements Notification mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); - if (Flags.compactHeadsUpNotification() - && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper) { + mIsHUNCompact = Flags.compactHeadsUpNotification() + && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper; + if (mIsHUNCompact) { logCompactHUNShownEvent(); } @@ -902,6 +906,10 @@ public class NotificationContentView extends FrameLayout implements Notification } } + public boolean isHUNCompact() { + return mIsHUNCompact; + } + private boolean isGroupExpanded() { return mContainingNotification.isGroupExpanded(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index ae9b69c8f6bf..c619b17f1ad8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification.row import android.annotation.SuppressLint -import android.app.Flags.notificationsRedesignTemplates import android.app.Notification import android.app.Notification.MessagingStyle import android.content.Context @@ -888,10 +887,7 @@ constructor( entryForLogging, "creating low-priority group summary remote view", ) - builder.makeLowPriorityContentView( - /* useRegularSubtext = */ true, - /* highlightExpander = */ notificationsRedesignTemplates(), - ) + builder.makeLowPriorityContentView(true /* useRegularSubtext */) } else null NewRemoteViews( contracted = contracted, @@ -1661,10 +1657,7 @@ constructor( useLarge: Boolean, ): RemoteViews { return if (isMinimized) { - builder.makeLowPriorityContentView( - /* useRegularSubtext = */ false, - /* highlightExpander = */ false, - ) + builder.makeLowPriorityContentView(false /* useRegularSubtext */) } else builder.createContentView(useLarge) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index e477c7430262..8e48065d9d1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -568,8 +568,7 @@ public class NotificationChildrenContainer extends ViewGroup builder = Notification.Builder.recoverBuilder(getContext(), notification.getNotification()); } - header = builder.makeLowPriorityContentView(true /* useRegularSubtext */, - notificationsRedesignTemplates() /* highlightExpander */); + header = builder.makeLowPriorityContentView(true /* useRegularSubtext */); if (mMinimizedGroupHeader == null) { mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(), this); 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 071d23283c43..76591ac4e453 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 @@ -108,7 +108,6 @@ import com.android.systemui.statusbar.notification.collection.render.GroupExpans import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix; import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView; -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.headsup.HeadsUpTouchHelper; import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil; @@ -703,9 +702,6 @@ public class NotificationStackScrollLayout if (!ModesEmptyShadeFix.isEnabled()) { inflateEmptyShadeView(); } - if (!FooterViewRefactor.isEnabled()) { - inflateFooterView(); - } } /** @@ -741,22 +737,12 @@ public class NotificationStackScrollLayout } void reinflateViews() { - if (!FooterViewRefactor.isEnabled()) { - inflateFooterView(); - updateFooter(); - } if (!ModesEmptyShadeFix.isEnabled()) { inflateEmptyShadeView(); } mSectionsManager.reinflateViews(); } - public void setIsRemoteInputActive(boolean isActive) { - FooterViewRefactor.assertInLegacyMode(); - mIsRemoteInputActive = isActive; - updateFooter(); - } - void sendRemoteInputRowBottomBound(Float bottom) { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; if (bottom != null) { @@ -766,43 +752,6 @@ public class NotificationStackScrollLayout mScrollViewFields.sendRemoteInputRowBottomBound(bottom); } - /** Setter for filtered notifs, to be removed with the FooterViewRefactor flag. */ - public void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) { - FooterViewRefactor.assertInLegacyMode(); - mHasFilteredOutSeenNotifications = hasFilteredOutSeenNotifications; - } - - @VisibleForTesting - public void updateFooter() { - FooterViewRefactor.assertInLegacyMode(); - if (mFooterView == null || mController == null) { - return; - } - final boolean showHistory = mController.isHistoryEnabled(); - final boolean showDismissView = shouldShowDismissView(); - - updateFooterView(shouldShowFooterView(showDismissView)/* visible */, - showDismissView /* showDismissView */, - showHistory/* showHistory */); - } - - 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() - && mUpcomingStatusBarState != StatusBarState.KEYGUARD - // quick settings don't affect notifications when not in full screen - && (getQsExpansionFraction() != 1 || !mQsFullScreen) - && !mScreenOffAnimationController.shouldHideNotificationsFooter() - && !mIsRemoteInputActive; - } - void updateBgColor() { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); @@ -1861,9 +1810,6 @@ public class NotificationStackScrollLayout */ private float getAppearEndPosition() { SceneContainerFlag.assertInLegacyMode(); - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - return getAppearEndPositionLegacy(); - } int appearPosition = mAmbientState.getStackTopMargin(); if (mEmptyShadeView.getVisibility() == GONE) { @@ -1883,32 +1829,6 @@ public class NotificationStackScrollLayout return appearPosition + (onKeyguard() ? getTopPadding() : getIntrinsicPadding()); } - /** - * The version of {@code getAppearEndPosition} that uses the notif count. The view shouldn't - * need to know about that, so we want to phase this out with the footer view refactor. - */ - private float getAppearEndPositionLegacy() { - FooterViewRefactor.assertInLegacyMode(); - - int appearPosition = mAmbientState.getStackTopMargin(); - int visibleNotifCount = mController.getVisibleNotificationCount(); - if (mEmptyShadeView.getVisibility() == GONE && visibleNotifCount > 0) { - if (isHeadsUpTransition() - || (mInHeadsUpPinnedMode && !mAmbientState.isDozing())) { - if (mShelf.getVisibility() != GONE && visibleNotifCount > 1) { - appearPosition += mShelf.getIntrinsicHeight() + mPaddingBetweenElements; - } - appearPosition += getTopHeadsUpPinnedHeight() - + getPositionInLinearLayout(mAmbientState.getTrackedHeadsUpRow()); - } else if (mShelf.getVisibility() != GONE) { - appearPosition += mShelf.getIntrinsicHeight(); - } - } else { - appearPosition = mEmptyShadeView.getHeight(); - } - return appearPosition + (onKeyguard() ? getTopPadding() : getIntrinsicPadding()); - } - private boolean isHeadsUpTransition() { return mAmbientState.getTrackedHeadsUpRow() != null; } @@ -1928,8 +1848,7 @@ public class NotificationStackScrollLayout // This can't use expansion fraction as that goes only from 0 to 1. Also when // appear fraction for HUN is 0, expansion fraction will be already around 0.2-0.3 // and that makes translation jump immediately. - float appearEndPosition = FooterViewRefactor.isEnabled() ? getAppearEndPosition() - : getAppearEndPositionLegacy(); + float appearEndPosition = getAppearEndPosition(); float appearStartPosition = getAppearStartPosition(); float hunAppearFraction = (height - appearStartPosition) / (appearEndPosition - appearStartPosition); @@ -4848,15 +4767,6 @@ public class NotificationStackScrollLayout } } - /** - * Returns whether or not a History button is shown in the footer. If there is no footer, then - * this will return false. - **/ - public boolean isHistoryShown() { - FooterViewRefactor.assertInLegacyMode(); - return mFooterView != null && mFooterView.isHistoryShown(); - } - /** Bind the {@link FooterView} to the NSSL. */ public void setFooterView(@NonNull FooterView footerView) { int index = -1; @@ -4866,18 +4776,6 @@ public class NotificationStackScrollLayout } mFooterView = footerView; addView(mFooterView, index); - if (!FooterViewRefactor.isEnabled()) { - if (mManageButtonClickListener != null) { - mFooterView.setManageButtonClickListener(mManageButtonClickListener); - } - mFooterView.setClearAllButtonClickListener(v -> { - if (mFooterClearAllListener != null) { - mFooterClearAllListener.onClearAll(); - } - clearNotifications(ROWS_ALL, true /* closeShade */); - footerView.setClearAllButtonVisible(false /* visible */, true /* animate */); - }); - } } public void setEmptyShadeView(EmptyShadeView emptyShadeView) { @@ -4890,13 +4788,6 @@ public class NotificationStackScrollLayout addView(mEmptyShadeView, index); } - /** Legacy version, should be removed with the footer refactor flag. */ - public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade) { - FooterViewRefactor.assertInLegacyMode(); - updateEmptyShadeView(visible, areNotificationsHiddenInShade, - mHasFilteredOutSeenNotifications); - } - /** Trigger an update for the empty shade resources and visibility. */ public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade, boolean hasFilteredOutSeenNotifications) { @@ -4949,18 +4840,6 @@ public class NotificationStackScrollLayout return mEmptyShadeView.isVisible(); } - 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); - mFooterView.showHistory(showHistory); - mFooterView.setClearAllButtonVisible(showDismissView, animate); - mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications); - } - @VisibleForTesting public void setClearAllInProgress(boolean clearAllInProgress) { mClearAllInProgress = clearAllInProgress; @@ -5244,10 +5123,8 @@ public class NotificationStackScrollLayout public void setQsFullScreen(boolean qsFullScreen) { SceneContainerFlag.assertInLegacyMode(); - if (FooterViewRefactor.isEnabled()) { - if (qsFullScreen == mQsFullScreen) { - return; // no change - } + if (qsFullScreen == mQsFullScreen) { + return; // no change } mQsFullScreen = qsFullScreen; updateAlgorithmLayoutMinHeight(); @@ -5266,8 +5143,6 @@ public class NotificationStackScrollLayout public void setQsExpansionFraction(float qsExpansionFraction) { SceneContainerFlag.assertInLegacyMode(); - boolean footerAffected = getQsExpansionFraction() != qsExpansionFraction - && (getQsExpansionFraction() == 1 || qsExpansionFraction == 1); mQsExpansionFraction = qsExpansionFraction; updateUseRoundedRectClipping(); @@ -5276,9 +5151,6 @@ public class NotificationStackScrollLayout if (getOwnScrollY() > 0) { setOwnScrollY((int) MathUtils.lerp(getOwnScrollY(), 0, getQsExpansionFraction())); } - if (!FooterViewRefactor.isEnabled() && footerAffected) { - updateFooter(); - } } @VisibleForTesting @@ -5456,14 +5328,6 @@ public class NotificationStackScrollLayout requestChildrenUpdate(); } - void setUpcomingStatusBarState(int upcomingStatusBarState) { - FooterViewRefactor.assertInLegacyMode(); - mUpcomingStatusBarState = upcomingStatusBarState; - if (mUpcomingStatusBarState != mStatusBarState) { - updateFooter(); - } - } - void onStatePostChange(boolean fromShadeLocked) { boolean onKeyguard = onKeyguard(); @@ -5472,9 +5336,6 @@ public class NotificationStackScrollLayout } setExpandingEnabled(!onKeyguard); - if (!FooterViewRefactor.isEnabled()) { - updateFooter(); - } requestChildrenUpdate(); onUpdateRowStates(); updateVisibility(); @@ -5490,8 +5351,7 @@ public class NotificationStackScrollLayout if (mEmptyShadeView == null || mEmptyShadeView.getVisibility() == GONE) { return getMinExpansionHeight(); } else { - return FooterViewRefactor.isEnabled() ? getAppearEndPosition() - : getAppearEndPositionLegacy(); + return getAppearEndPosition(); } } @@ -5583,12 +5443,6 @@ public class NotificationStackScrollLayout for (int i = 0; i < childCount; i++) { ExpandableView child = getChildAtIndex(i); child.dump(pw, args); - if (!FooterViewRefactor.isEnabled()) { - if (child instanceof FooterView) { - DumpUtilsKt.withIncreasedIndent(pw, - () -> dumpFooterViewVisibility(pw)); - } - } pw.println(); } int transientViewCount = getTransientViewCount(); @@ -5615,45 +5469,6 @@ public class NotificationStackScrollLayout pw.append(" bottomRadius=").println(mBgCornerRadii[4]); } - private void dumpFooterViewVisibility(IndentingPrintWriter pw) { - FooterViewRefactor.assertInLegacyMode(); - final boolean showDismissView = shouldShowDismissView(); - - pw.println("showFooterView: " + shouldShowFooterView(showDismissView)); - DumpUtilsKt.withIncreasedIndent( - pw, - () -> { - pw.println("showDismissView: " + showDismissView); - DumpUtilsKt.withIncreasedIndent( - pw, - () -> { - pw.println( - "hasActiveClearableNotifications: " - + mController.hasActiveClearableNotifications( - ROWS_ALL)); - }); - pw.println(); - pw.println("showHistory: " + mController.isHistoryEnabled()); - pw.println(); - pw.println( - "visibleNotificationCount: " - + mController.getVisibleNotificationCount()); - pw.println("mIsCurrentUserSetup: " + mIsCurrentUserSetup); - pw.println("onKeyguard: " + onKeyguard()); - pw.println("mUpcomingStatusBarState: " + mUpcomingStatusBarState); - if (!SceneContainerFlag.isEnabled()) { - pw.println("QsExpansionFraction: " + getQsExpansionFraction()); - } - pw.println("mQsFullScreen: " + mQsFullScreen); - pw.println( - "mScreenOffAnimationController" - + ".shouldHideNotificationsFooter: " - + mScreenOffAnimationController - .shouldHideNotificationsFooter()); - pw.println("mIsRemoteInputActive: " + mIsRemoteInputActive); - }); - } - public boolean isFullyHidden() { return mAmbientState.isFullyHidden(); } @@ -5764,14 +5579,6 @@ public class NotificationStackScrollLayout clearNotifications(ROWS_GENTLE, closeShade, hideSilentSection); } - /** Legacy version of clearNotifications below. Uses the old data source for notif stats. */ - void clearNotifications(@SelectedRows int selection, boolean closeShade) { - FooterViewRefactor.assertInLegacyMode(); - final boolean hideSilentSection = !mController.hasNotifications( - ROWS_GENTLE, false /* clearable */); - clearNotifications(selection, closeShade, hideSilentSection); - } - /** * Collects a list of visible rows, and animates them away in a staggered fashion as if they * were dismissed. Notifications are dismissed in the backend via onClearAllAnimationsEnd. @@ -5826,25 +5633,6 @@ public class NotificationStackScrollLayout return canChildBeCleared(row) && matchesSelection(row, selection); } - /** - * Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked. - */ - public void setManageButtonClickListener(@Nullable OnClickListener listener) { - FooterViewRefactor.assertInLegacyMode(); - mManageButtonClickListener = listener; - if (mFooterView != null) { - mFooterView.setManageButtonClickListener(mManageButtonClickListener); - } - } - - @VisibleForTesting - protected void inflateFooterView() { - FooterViewRefactor.assertInLegacyMode(); - FooterView footerView = (FooterView) LayoutInflater.from(mContext).inflate( - R.layout.status_bar_notification_footer, this, false); - setFooterView(footerView); - } - private void inflateEmptyShadeView() { ModesEmptyShadeFix.assertInLegacyMode(); @@ -6091,11 +5879,6 @@ public class NotificationStackScrollLayout mHighPriorityBeforeSpeedBump = highPriorityBeforeSpeedBump; } - void setFooterClearAllListener(FooterClearAllListener listener) { - FooterViewRefactor.assertInLegacyMode(); - mFooterClearAllListener = listener; - } - void setClearAllFinishedWhilePanelExpandedRunnable(Runnable runnable) { mClearAllFinishedWhilePanelExpandedRunnable = runnable; } @@ -6394,17 +6177,6 @@ public class NotificationStackScrollLayout } /** - * 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(); - } - } - - /** * Sets a {@link StackStateLogger} which is notified as the {@link StackStateAnimator} updates * the views. */ 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 a33a9ed2df75..c717e3b229be 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 @@ -29,15 +29,14 @@ import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; -import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE; -import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_HIGH_PRIORITY; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.SelectedRows; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD; -import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.animation.ObjectAnimator; import android.content.res.Configuration; import android.graphics.Point; +import android.graphics.RenderEffect; +import android.graphics.Shader; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; @@ -64,14 +63,10 @@ import com.android.internal.view.OneShotPreDrawListener; import com.android.systemui.Dumpable; import com.android.systemui.ExpandHelper; import com.android.systemui.Gefingerpoken; -import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; -import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; -import com.android.systemui.keyguard.shared.model.KeyguardState; -import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.media.controls.ui.controller.KeyguardMediaController; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; @@ -92,18 +87,13 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener; -import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.DynamicPrivacyController; -import com.android.systemui.statusbar.notification.headsup.HeadsUpNotificationViewControllerEmptyImpl; -import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper; -import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper.HeadsUpNotificationViewController; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; -import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.collection.EntryWithDismissStats; import com.android.systemui.statusbar.notification.collection.NotifCollection; @@ -116,13 +106,12 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; -import com.android.systemui.statusbar.notification.collection.render.NotifStackController; -import com.android.systemui.statusbar.notification.collection.render.NotifStats; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; -import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; -import com.android.systemui.statusbar.notification.dagger.SilentHeader; -import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor; -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; +import com.android.systemui.statusbar.notification.headsup.HeadsUpNotificationViewControllerEmptyImpl; +import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper; +import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper.HeadsUpNotificationViewController; +import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener; import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; @@ -137,13 +126,8 @@ import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; -import com.android.systemui.statusbar.policy.DeviceProvisionedController; -import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; -import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener; import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.SplitShadeStateController; -import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; import com.android.systemui.util.Compile; import com.android.systemui.util.settings.SecureSettings; @@ -179,10 +163,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { private HeadsUpTouchHelper mHeadsUpTouchHelper; private final NotificationRoundnessManager mNotificationRoundnessManager; private final TunerService mTunerService; - private final DeviceProvisionedController mDeviceProvisionedController; private final DynamicPrivacyController mDynamicPrivacyController; private final ConfigurationController mConfigurationController; - private final ZenModeController mZenModeController; private final MetricsLogger mMetricsLogger; private final ColorUpdateLogger mColorUpdateLogger; @@ -193,7 +175,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final NotifPipeline mNotifPipeline; private final NotifCollection mNotifCollection; private final UiEventLogger mUiEventLogger; - private final NotificationRemoteInputManager mRemoteInputManager; private final VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; private final ShadeController mShadeController; private final Provider<WindowRootView> mWindowRootView; @@ -201,9 +182,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final SysuiStatusBarStateController mStatusBarStateController; private final KeyguardBypassController mKeyguardBypassController; private final PowerInteractor mPowerInteractor; - private final PrimaryBouncerInteractor mPrimaryBouncerInteractor; private final NotificationLockscreenUserManager mLockscreenUserManager; - private final SectionHeaderController mSilentHeaderController; private final LockscreenShadeTransitionController mLockscreenShadeTransitionController; private final InteractionJankMonitor mJankMonitor; private final NotificationStackSizeCalculator mNotificationStackSizeCalculator; @@ -211,8 +190,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final NotificationStackScrollLogger mLogger; private final GroupExpansionManager mGroupExpansionManager; - private final SeenNotificationsInteractor mSeenNotificationsInteractor; - private final KeyguardTransitionRepository mKeyguardTransitionRepo; private NotificationStackScrollLayout mView; private TouchHandler mTouchHandler; private NotificationSwipeHelper mSwipeHelper; @@ -220,7 +197,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { private Boolean mHistoryEnabled; private int mBarState; private HeadsUpAppearanceController mHeadsUpAppearanceController; - private boolean mIsInTransitionToAod = false; private final NotificationTargetsHelper mNotificationTargetsHelper; private final SecureSettings mSecureSettings; @@ -235,11 +211,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final NotificationListContainerImpl mNotificationListContainer = new NotificationListContainerImpl(); - private final NotifStackController mNotifStackController = - new NotifStackControllerImpl(); - - @Nullable - private NotificationActivityStarter mNotificationActivityStarter; @VisibleForTesting final View.OnAttachStateChangeListener mOnAttachStateChangeListener = @@ -248,9 +219,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { public void onViewAttachedToWindow(View v) { mColorUpdateLogger.logTriggerEvent("NSSLC.onViewAttachedToWindow()"); mConfigurationController.addCallback(mConfigurationListener); - if (!FooterViewRefactor.isEnabled()) { - mZenModeController.addCallback(mZenModeControllerCallback); - } final int newBarState = mStatusBarStateController.getState(); if (newBarState != mBarState) { mStateListener.onStateChanged(newBarState); @@ -264,9 +232,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { public void onViewDetachedFromWindow(View v) { mColorUpdateLogger.logTriggerEvent("NSSLC.onViewDetachedFromWindow()"); mConfigurationController.removeCallback(mConfigurationListener); - if (!FooterViewRefactor.isEnabled()) { - mZenModeController.removeCallback(mZenModeControllerCallback); - } mStatusBarStateController.removeCallback(mStateListener); } }; @@ -287,28 +252,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { @Nullable private ObjectAnimator mHideAlphaAnimator = null; - private final DeviceProvisionedListener mDeviceProvisionedListener = - new DeviceProvisionedListener() { - @Override - public void onDeviceProvisionedChanged() { - updateCurrentUserIsSetup(); - } - - @Override - public void onUserSwitched() { - updateCurrentUserIsSetup(); - } - - @Override - public void onUserSetupChanged() { - updateCurrentUserIsSetup(); - } - - private void updateCurrentUserIsSetup() { - mView.setCurrentUserSetup(mDeviceProvisionedController.isCurrentUserSetup()); - } - }; - private final Runnable mSensitiveStateChangedListener = new Runnable() { @Override public void run() { @@ -318,20 +261,10 @@ public class NotificationStackScrollLayoutController implements Dumpable { } }; - private final DynamicPrivacyController.Listener mDynamicPrivacyControllerListener = () -> { - if (!FooterViewRefactor.isEnabled()) { - // Let's update the footer once the notifications have been updated (in the next frame) - mView.post(this::updateFooter); - } - }; - @VisibleForTesting final ConfigurationListener mConfigurationListener = new ConfigurationListener() { @Override public void onDensityOrFontScaleChanged() { - if (!FooterViewRefactor.isEnabled()) { - updateShowEmptyShadeView(); - } mView.reinflateViews(); } @@ -351,10 +284,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.updateBgColor(); mView.updateDecorViews(); mView.reinflateViews(); - if (!FooterViewRefactor.isEnabled()) { - updateShowEmptyShadeView(); - updateFooter(); - } } @Override @@ -363,7 +292,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { } }; - private NotifStats mNotifStats = NotifStats.getEmpty(); private float mMaxAlphaForKeyguard = 1.0f; private String mMaxAlphaForKeyguardSource = "constructor"; private float mMaxAlphaForUnhide = 1.0f; @@ -401,19 +329,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { } @Override - public void onUpcomingStateChanged(int newState) { - if (!FooterViewRefactor.isEnabled()) { - mView.setUpcomingStatusBarState(newState); - } - } - - @Override public void onStatePostChange() { updateSensitivenessWithAnimation(mStatusBarStateController.goingToFullShade()); mView.onStatePostChange(mStatusBarStateController.fromShadeLocked()); - if (!FooterViewRefactor.isEnabled()) { - updateImportantForAccessibility(); - } } }; @@ -422,9 +340,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { public void onUserChanged(int userId) { updateSensitivenessWithAnimation(false); mHistoryEnabled = null; - if (!FooterViewRefactor.isEnabled()) { - updateFooter(); - } } }; @@ -656,7 +571,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { == null) { mHeadsUpManager.removeNotification( row.getEntry().getSbn().getKey(), - /* removeImmediately= */ true , + /* removeImmediately= */ true, /* reason= */ "onChildSnappedBack" ); } @@ -714,14 +629,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { } }; - private final ZenModeController.Callback mZenModeControllerCallback = - new ZenModeController.Callback() { - @Override - public void onZenChanged(int zen) { - updateShowEmptyShadeView(); - } - }; - @Inject public NotificationStackScrollLayoutController( NotificationStackScrollLayout view, @@ -734,16 +641,12 @@ public class NotificationStackScrollLayoutController implements Dumpable { Provider<IStatusBarService> statusBarService, NotificationRoundnessManager notificationRoundnessManager, TunerService tunerService, - DeviceProvisionedController deviceProvisionedController, DynamicPrivacyController dynamicPrivacyController, @ShadeDisplayAware ConfigurationController configurationController, SysuiStatusBarStateController statusBarStateController, KeyguardMediaController keyguardMediaController, KeyguardBypassController keyguardBypassController, PowerInteractor powerInteractor, - PrimaryBouncerInteractor primaryBouncerInteractor, - KeyguardTransitionRepository keyguardTransitionRepo, - ZenModeController zenModeController, NotificationLockscreenUserManager lockscreenUserManager, MetricsLogger metricsLogger, ColorUpdateLogger colorUpdateLogger, @@ -752,14 +655,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { FalsingManager falsingManager, NotificationSwipeHelper.Builder notificationSwipeHelperBuilder, GroupExpansionManager groupManager, - @SilentHeader SectionHeaderController silentHeaderController, NotifPipeline notifPipeline, NotifCollection notifCollection, LockscreenShadeTransitionController lockscreenShadeTransitionController, UiEventLogger uiEventLogger, - NotificationRemoteInputManager remoteInputManager, VisibilityLocationProviderDelegator visibilityLocationProviderDelegator, - SeenNotificationsInteractor seenNotificationsInteractor, NotificationListViewBinder viewBinder, ShadeController shadeController, Provider<WindowRootView> windowRootView, @@ -775,7 +675,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { SensitiveNotificationProtectionController sensitiveNotificationProtectionController, WallpaperInteractor wallpaperInteractor) { mView = view; - mKeyguardTransitionRepo = keyguardTransitionRepo; mViewBinder = viewBinder; mStackStateLogger = stackLogger; mLogger = logger; @@ -795,15 +694,12 @@ public class NotificationStackScrollLayoutController implements Dumpable { } mNotificationRoundnessManager = notificationRoundnessManager; mTunerService = tunerService; - mDeviceProvisionedController = deviceProvisionedController; mDynamicPrivacyController = dynamicPrivacyController; mConfigurationController = configurationController; mStatusBarStateController = statusBarStateController; mKeyguardMediaController = keyguardMediaController; mKeyguardBypassController = keyguardBypassController; mPowerInteractor = powerInteractor; - mPrimaryBouncerInteractor = primaryBouncerInteractor; - mZenModeController = zenModeController; mLockscreenUserManager = lockscreenUserManager; mMetricsLogger = metricsLogger; mColorUpdateLogger = colorUpdateLogger; @@ -815,13 +711,10 @@ public class NotificationStackScrollLayoutController implements Dumpable { mJankMonitor = jankMonitor; mNotificationStackSizeCalculator = notificationStackSizeCalculator; mGroupExpansionManager = groupManager; - mSilentHeaderController = silentHeaderController; mNotifPipeline = notifPipeline; mNotifCollection = notifCollection; mUiEventLogger = uiEventLogger; - mRemoteInputManager = remoteInputManager; mVisibilityLocationProviderDelegator = visibilityLocationProviderDelegator; - mSeenNotificationsInteractor = seenNotificationsInteractor; mShadeController = shadeController; mWindowRootView = windowRootView; mNotificationTargetsHelper = notificationTargetsHelper; @@ -850,18 +743,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.setClearAllAnimationListener(this::onAnimationEnd); mView.setClearAllListener((selection) -> mUiEventLogger.log( NotificationPanelEvent.fromSelection(selection))); - 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.setClearAllFinishedWhilePanelExpandedRunnable(()-> { + mView.setClearAllFinishedWhilePanelExpandedRunnable(() -> { final Runnable doCollapseRunnable = () -> mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE); mView.postDelayed(doCollapseRunnable, /* delayMillis = */ DELAY_BEFORE_SHADE_CLOSE); @@ -889,19 +771,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.setKeyguardBypassEnabled(mKeyguardBypassController.getBypassEnabled()); mKeyguardBypassController .registerOnBypassStateChangedListener(mView::setKeyguardBypassEnabled); - if (!FooterViewRefactor.isEnabled()) { - mView.setManageButtonClickListener(v -> { - if (mNotificationActivityStarter != null) { - mNotificationActivityStarter.startHistoryIntent(v, mView.isHistoryShown()); - } - }); - } if (!SceneContainerFlag.isEnabled()) { mHeadsUpManager.addListener(mOnHeadsUpChangedListener); } mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed); - mDynamicPrivacyController.addListener(mDynamicPrivacyControllerListener); mLockscreenShadeTransitionController.setStackScroller(this); @@ -914,9 +788,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { switch (key) { case Settings.Secure.NOTIFICATION_HISTORY_ENABLED: mHistoryEnabled = null; // invalidate - if (!FooterViewRefactor.isEnabled()) { - updateFooter(); - } break; case HIGH_PRIORITY: mView.setHighPriorityBeforeSpeedBump("1".equals(newValue)); @@ -938,12 +809,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { return kotlin.Unit.INSTANCE; }); - if (!FooterViewRefactor.isEnabled()) { - // attach callback, and then call it to update mView immediately - mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); - mDeviceProvisionedListener.onDeviceProvisionedChanged(); - } - if (screenshareNotificationHiding()) { mSensitiveNotificationProtectionController .registerSensitiveStateListener(mSensitiveStateChangedListener); @@ -953,20 +818,12 @@ public class NotificationStackScrollLayoutController implements Dumpable { mOnAttachStateChangeListener.onViewAttachedToWindow(mView); } mView.addOnAttachStateChangeListener(mOnAttachStateChangeListener); - if (!FooterViewRefactor.isEnabled()) { - mSilentHeaderController.setOnClearSectionClickListener(v -> clearSilentNotifications()); - } mGroupExpansionManager.registerGroupExpansionChangeListener( (changedRow, expanded) -> mView.onGroupExpandChanged(changedRow, expanded)); mViewBinder.bindWhileAttached(mView, this); - if (!FooterViewRefactor.isEnabled()) { - collectFlow(mView, mKeyguardTransitionRepo.getTransitions(), - this::onKeyguardTransitionChanged); - } - mView.setWallpaperInteractor(mWallpaperInteractor); } @@ -1168,11 +1025,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { return mView != null && mView.isAddOrRemoveAnimationPending(); } - public int getVisibleNotificationCount() { - FooterViewRefactor.assertInLegacyMode(); - return mNotifStats.getNumActiveNotifs(); - } - public boolean isHistoryEnabled() { Boolean historyEnabled = mHistoryEnabled; if (historyEnabled == null) { @@ -1284,9 +1136,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { public void setQsFullScreen(boolean fullScreen) { mView.setQsFullScreen(fullScreen); - if (!FooterViewRefactor.isEnabled()) { - updateShowEmptyShadeView(); - } } public void setScrollingEnabled(boolean enabled) { @@ -1390,6 +1239,22 @@ public class NotificationStackScrollLayoutController implements Dumpable { updateAlpha(); } + /** + * Applies a blur effect to the view. + * + * @param blurRadius Radius of blur + */ + public void setBlurRadius(float blurRadius) { + if (blurRadius > 0.0f) { + mView.setRenderEffect(RenderEffect.createBlurEffect( + blurRadius, + blurRadius, + Shader.TileMode.CLAMP)); + } else { + mView.setRenderEffect(null); + } + } + private void updateAlpha() { if (mView != null) { mView.setAlpha(Math.min(Math.min(mMaxAlphaFromView, mMaxAlphaForKeyguard), @@ -1464,64 +1329,12 @@ public class NotificationStackScrollLayoutController implements Dumpable { } /** - * Set the visibility of the view, and propagate it to specific children. + * Set the visibility of the view. * * @param visible either the view is visible or not. */ public void updateVisibility(boolean visible) { mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); - - // Refactor note: the empty shade's visibility doesn't seem to actually depend on the - // parent visibility (so this update seemingly doesn't do anything). Therefore, this is not - // modeled in the refactored code. - if (!FooterViewRefactor.isEnabled() && mView.getVisibility() == View.VISIBLE) { - // Synchronize EmptyShadeView visibility with the parent container. - updateShowEmptyShadeView(); - updateImportantForAccessibility(); - } - } - - /** - * Update whether we should show the empty shade view ("no notifications" in the shade). - * <p> - * When in split mode, notifications are always visible regardless of the state of the - * QuickSettings panel. That being the case, empty view is always shown if the other conditions - * are true. - */ - public void updateShowEmptyShadeView() { - FooterViewRefactor.assertInLegacyMode(); - - Trace.beginSection("NSSLC.updateShowEmptyShadeView"); - - final boolean shouldShow = getVisibleNotificationCount() == 0 - && !mView.isQsFullScreen() - // Hide empty shade view when in transition to AOD. - // That avoids "No Notifications" to blink when transitioning to AOD. - // For more details, see: b/228790482 - && !mIsInTransitionToAod - // Don't show any notification content if the bouncer is showing. See b/267060171. - && !mPrimaryBouncerInteractor.isBouncerShowing(); - - mView.updateEmptyShadeView(shouldShow, mZenModeController.areNotificationsHiddenInShade()); - - Trace.endSection(); - } - - /** - * Update the importantForAccessibility of NotificationStackScrollLayout. - * <p> - * We want the NSSL to be unimportant for accessibility when there's no - * notifications in it while the device is on lock screen, to avoid unlablel NSSL view. - * Otherwise, we want it to be important for accessibility to enable accessibility - * auto-scrolling in NSSL. - */ - public void updateImportantForAccessibility() { - FooterViewRefactor.assertInLegacyMode(); - if (getVisibleNotificationCount() == 0 && mView.onKeyguard()) { - mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - } else { - mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } } public boolean isShowingEmptyShadeView() { @@ -1577,34 +1390,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.setPulsing(pulsing, animatePulse); } - /** - * Return whether there are any clearable notifications - */ - public boolean hasActiveClearableNotifications(@SelectedRows int selection) { - FooterViewRefactor.assertInLegacyMode(); - return hasNotifications(selection, true /* clearable */); - } - - public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) { - FooterViewRefactor.assertInLegacyMode(); - boolean hasAlertingMatchingClearable = isClearable - ? mNotifStats.getHasClearableAlertingNotifs() - : mNotifStats.getHasNonClearableAlertingNotifs(); - boolean hasSilentMatchingClearable = isClearable - ? mNotifStats.getHasClearableSilentNotifs() - : mNotifStats.getHasNonClearableSilentNotifs(); - switch (selection) { - case ROWS_GENTLE: - return hasSilentMatchingClearable; - case ROWS_HIGH_PRIORITY: - return hasAlertingMatchingClearable; - case ROWS_ALL: - return hasSilentMatchingClearable || hasAlertingMatchingClearable; - default: - throw new IllegalStateException("Bad selection: " + selection); - } - } - /** Sets whether the NSSL is displayed over the unoccluded Lockscreen. */ public void setOnLockscreen(boolean isOnLockscreen) { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; @@ -1637,9 +1422,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { } mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive); entry.notifyHeightChanged(true /* needsAnimation */); - if (!FooterViewRefactor.isEnabled()) { - updateFooter(); - } } public void lockScrollTo(NotificationEntry entry) { @@ -1662,13 +1444,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { }; } - public void updateFooter() { - FooterViewRefactor.assertInLegacyMode(); - Trace.beginSection("NSSLC.updateFooter"); - mView.updateFooter(); - Trace.endSection(); - } - public void onUpdateRowStates() { mView.onUpdateRowStates(); } @@ -1695,18 +1470,10 @@ public class NotificationStackScrollLayoutController implements Dumpable { return mView.getTransientViewCount(); } - public View getTransientView(int i) { - return mView.getTransientView(i); - } - public NotificationStackScrollLayout getView() { return mView; } - public float calculateGapHeight(ExpandableView previousView, ExpandableView child, int count) { - return mView.calculateGapHeight(previousView, child, count); - } - NotificationRoundnessManager getNotificationRoundnessManager() { return mNotificationRoundnessManager; } @@ -1715,10 +1482,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { return mNotificationListContainer; } - public NotifStackController getNotifStackController() { - return mNotifStackController; - } - public void resetCheckSnoozeLeavebehind() { mView.resetCheckSnoozeLeavebehind(); } @@ -1772,13 +1535,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { return NotificationSwipeHelper.isTouchInView(event, view); } - public void clearSilentNotifications() { - FooterViewRefactor.assertInLegacyMode(); - // Leave the shade open if there will be other notifs left over to clear - final boolean closeShade = !hasActiveClearableNotifications(ROWS_HIGH_PRIORITY); - mView.clearNotifications(ROWS_GENTLE, closeShade); - } - private void onAnimationEnd(List<ExpandableNotificationRow> viewsToRemove, @SelectedRows int selectedRows) { if (selectedRows == ROWS_ALL) { @@ -1880,10 +1636,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.animateNextTopPaddingChange(); } - public void setNotificationActivityStarter(NotificationActivityStarter activityStarter) { - mNotificationActivityStarter = activityStarter; - } - public NotificationTargetsHelper getNotificationTargetsHelper() { return mNotificationTargetsHelper; } @@ -1898,18 +1650,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { } @VisibleForTesting - void onKeyguardTransitionChanged(TransitionStep transitionStep) { - FooterViewRefactor.assertInLegacyMode(); - boolean isTransitionToAod = transitionStep.getTo().equals(KeyguardState.AOD) - && (transitionStep.getFrom().equals(KeyguardState.GONE) - || transitionStep.getFrom().equals(KeyguardState.OCCLUDED)); - if (mIsInTransitionToAod != isTransitionToAod) { - mIsInTransitionToAod = isTransitionToAod; - updateShowEmptyShadeView(); - } - } - - @VisibleForTesting TouchHandler getTouchHandler() { return mTouchHandler; } @@ -2288,22 +2028,4 @@ public class NotificationStackScrollLayoutController implements Dumpable { && !mSwipeHelper.isSwiping(); } } - - private class NotifStackControllerImpl implements NotifStackController { - @Override - public void setNotifStats(@NonNull NotifStats notifStats) { - FooterViewRefactor.assertInLegacyMode(); - mNotifStats = notifStats; - - if (!FooterViewRefactor.isEnabled()) { - mView.setHasFilteredOutSeenNotifications( - mSeenNotificationsInteractor - .getHasFilteredOutSeenNotifications().getValue()); - - 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 1653029dc994..06b989aaab57 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 @@ -35,7 +35,6 @@ import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView; -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; @@ -463,26 +462,23 @@ public class StackScrollAlgorithm { if (v == ambientState.getShelf()) { continue; } - if (FooterViewRefactor.isEnabled()) { - if (v instanceof EmptyShadeView) { - emptyShadeVisible = true; - } - if (v instanceof FooterView footerView) { - if (emptyShadeVisible || notGoneIndex == 0) { - // if the empty shade is visible or the footer is the first visible - // view, we're in a transitory state so let's leave the footer alone. - if (Flags.notificationsFooterVisibilityFix() - && !SceneContainerFlag.isEnabled()) { - // ...except for the hidden state, to prevent it from flashing on - // the screen (this piece is copied from updateChild, and is not - // necessary in flexiglass). - if (footerView.shouldBeHidden() - || !ambientState.isShadeExpanded()) { - footerView.getViewState().hidden = true; - } + if (v instanceof EmptyShadeView) { + emptyShadeVisible = true; + } + if (v instanceof FooterView footerView) { + if (emptyShadeVisible || notGoneIndex == 0) { + // if the empty shade is visible or the footer is the first visible + // view, we're in a transitory state so let's leave the footer alone. + if (Flags.notificationsFooterVisibilityFix() + && !SceneContainerFlag.isEnabled()) { + // ...except for the hidden state, to prevent it from flashing on + // the screen (this piece is copied from updateChild, and is not + // necessary in flexiglass). + if (footerView.shouldBeHidden() || !ambientState.isShadeExpanded()) { + footerView.getViewState().hidden = true; } - continue; } + continue; } } @@ -699,44 +695,28 @@ public class StackScrollAlgorithm { viewEnd, /* hunMax */ ambientState.getMaxHeadsUpTranslation() ); if (view instanceof FooterView) { - if (FooterViewRefactor.isEnabled()) { - if (SceneContainerFlag.isEnabled()) { - final float footerEnd = - stackTop + viewState.getYTranslation() + view.getIntrinsicHeight(); - final boolean noSpaceForFooter = footerEnd > ambientState.getStackCutoff(); - ((FooterView.FooterViewState) viewState).hideContent = - noSpaceForFooter || (ambientState.isClearAllInProgress() - && !hasNonClearableNotifs(algorithmState)); - } else { - // TODO(b/333445519): shouldBeHidden should reflect whether the shade is closed - // already, so we shouldn't need to use ambientState here. However, - // currently it doesn't get updated quickly enough and can cause the footer to - // flash when closing the shade. As such, we temporarily also check the - // ambientState directly. - if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) { - viewState.hidden = true; - } else { - final float footerEnd = algorithmState.mCurrentExpandedYPosition - + view.getIntrinsicHeight(); - final boolean noSpaceForFooter = - footerEnd > ambientState.getStackEndHeight(); - ((FooterView.FooterViewState) viewState).hideContent = - noSpaceForFooter || (ambientState.isClearAllInProgress() - && !hasNonClearableNotifs(algorithmState)); - } - } + if (SceneContainerFlag.isEnabled()) { + final float footerEnd = + stackTop + viewState.getYTranslation() + view.getIntrinsicHeight(); + final boolean noSpaceForFooter = footerEnd > ambientState.getStackCutoff(); + ((FooterView.FooterViewState) viewState).hideContent = + noSpaceForFooter || (ambientState.isClearAllInProgress() + && !hasNonClearableNotifs(algorithmState)); } else { - final boolean shadeClosed = !ambientState.isShadeExpanded(); - final boolean isShelfShowing = algorithmState.firstViewInShelf != null; - if (shadeClosed) { + // TODO(b/333445519): shouldBeHidden should reflect whether the shade is closed + // already, so we shouldn't need to use ambientState here. However, + // currently it doesn't get updated quickly enough and can cause the footer to + // flash when closing the shade. As such, we temporarily also check the + // ambientState directly. + if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) { viewState.hidden = true; } else { final float footerEnd = algorithmState.mCurrentExpandedYPosition + view.getIntrinsicHeight(); - final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight(); + final boolean noSpaceForFooter = + footerEnd > ambientState.getStackEndHeight(); ((FooterView.FooterViewState) viewState).hideContent = - isShelfShowing || noSpaceForFooter - || (ambientState.isClearAllInProgress() + noSpaceForFooter || (ambientState.isClearAllInProgress() && !hasNonClearableNotifs(algorithmState)); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt index 53749ff24394..c8c798d00a06 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.stack.ui.view -import android.os.Trace import android.service.notification.NotificationListenerService import androidx.annotation.VisibleForTesting +import com.android.app.tracing.coroutines.TrackTracer import com.android.internal.statusbar.IStatusBarService import com.android.internal.statusbar.NotificationVisibility import com.android.systemui.dagger.SysUISingleton @@ -183,8 +183,8 @@ constructor( maybeLogVisibilityChanges(newlyVisible, noLongerVisible, activeNotifCount) updateExpansionStates(newlyVisible, noLongerVisible) - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", activeNotifCount) - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]", newVisibilities.size) + TrackTracer.instantForGroup("Notifications", "Active", activeNotifCount) + TrackTracer.instantForGroup("Notifications", "Visible", newVisibilities.size) lastLoggedVisibilities.clear() lastLoggedVisibilities.putAll(newVisibilities) 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 b4561686b7b2..1d7e658932ac 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 @@ -40,7 +40,6 @@ import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyS import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView import com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder.EmptyShadeViewBinder import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder @@ -108,25 +107,20 @@ constructor( launch { bindShelf(shelf) } bindHideList(viewController, viewModel, hiderTracker) - if (FooterViewRefactor.isEnabled) { - val hasNonClearableSilentNotifications: StateFlow<Boolean> = - viewModel.hasNonClearableSilentNotifications.stateIn(this) - launch { reinflateAndBindFooter(view, hasNonClearableSilentNotifications) } - launch { - if (ModesEmptyShadeFix.isEnabled) { - reinflateAndBindEmptyShade(view) - } else { - bindEmptyShadeLegacy(viewModel.emptyShadeViewFactory.create(), view) - } + val hasNonClearableSilentNotifications: StateFlow<Boolean> = + viewModel.hasNonClearableSilentNotifications.stateIn(this) + launch { reinflateAndBindFooter(view, hasNonClearableSilentNotifications) } + launch { + if (ModesEmptyShadeFix.isEnabled) { + reinflateAndBindEmptyShade(view) + } else { + bindEmptyShadeLegacy(viewModel.emptyShadeViewFactory.create(), view) } - launch { - bindSilentHeaderClickListener(view, hasNonClearableSilentNotifications) - } - launch { - viewModel.isImportantForAccessibility.collect { isImportantForAccessibility - -> - view.setImportantForAccessibilityYesNo(isImportantForAccessibility) - } + } + launch { bindSilentHeaderClickListener(view, hasNonClearableSilentNotifications) } + launch { + viewModel.isImportantForAccessibility.collect { isImportantForAccessibility -> + view.setImportantForAccessibilityYesNo(isImportantForAccessibility) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index ea714608ea66..3ea4d488357d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -28,7 +28,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer @@ -81,9 +80,6 @@ constructor( controller.setOverExpansion(0f) controller.setOverScrollAmount(0) - if (!FooterViewRefactor.isEnabled) { - controller.updateFooter() - } } } } @@ -183,6 +179,10 @@ constructor( } } + if (Flags.bouncerUiRevamp()) { + launch { viewModel.blurRadius.collect { controller.setBlurRadius(it) } } + } + if (communalSettingsInteractor.isCommunalFlagEnabled()) { launch { viewModel.glanceableHubAlpha.collect { 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 38390e7bdb39..fcc671a5bae6 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 @@ -25,7 +25,6 @@ import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotif import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel -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.shared.HeadsUpRowKey import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel @@ -75,46 +74,37 @@ constructor( * we want it to be important for accessibility to enable accessibility auto-scrolling in NSSL. * See b/242235264 for more details. */ - val isImportantForAccessibility: Flow<Boolean> by lazy { - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - flowOf(true) - } else { - combine( - activeNotificationsInteractor.areAnyNotificationsPresent, - notificationStackInteractor.isShowingOnLockscreen, - ) { hasNotifications, isShowingOnLockscreen -> - hasNotifications || !isShowingOnLockscreen - } - .distinctUntilChanged() - .dumpWhileCollecting("isImportantForAccessibility") - .flowOn(bgDispatcher) - } - } + val isImportantForAccessibility: Flow<Boolean> = + combine( + activeNotificationsInteractor.areAnyNotificationsPresent, + notificationStackInteractor.isShowingOnLockscreen, + ) { hasNotifications, isShowingOnLockscreen -> + hasNotifications || !isShowingOnLockscreen + } + .distinctUntilChanged() + .dumpWhileCollecting("isImportantForAccessibility") + .flowOn(bgDispatcher) val shouldShowEmptyShadeView: Flow<Boolean> by lazy { ModesEmptyShadeFix.assertInLegacyMode() - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - flowOf(false) - } else { - combine( - activeNotificationsInteractor.areAnyNotificationsPresent, - shadeInteractor.isQsFullscreen, - notificationStackInteractor.isShowingOnLockscreen, - ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen -> - when { - hasNotifications -> false - isQsFullScreen -> false - // Do not show the empty shade if the lockscreen is visible (including AOD - // b/228790482 and bouncer b/267060171), except if the shade is opened on - // top. - isShowingOnLockscreen -> false - else -> true - } + combine( + activeNotificationsInteractor.areAnyNotificationsPresent, + shadeInteractor.isQsFullscreen, + notificationStackInteractor.isShowingOnLockscreen, + ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen -> + when { + hasNotifications -> false + isQsFullScreen -> false + // Do not show the empty shade if the lockscreen is visible (including AOD + // b/228790482 and bouncer b/267060171), except if the shade is opened on + // top. + isShowingOnLockscreen -> false + else -> true } - .distinctUntilChanged() - .dumpWhileCollecting("shouldShowEmptyShadeView") - .flowOn(bgDispatcher) - } + } + .distinctUntilChanged() + .dumpWhileCollecting("shouldShowEmptyShadeView") + .flowOn(bgDispatcher) } val shouldShowEmptyShadeViewAnimated: Flow<AnimatedValue<Boolean>> by lazy { @@ -164,18 +154,14 @@ constructor( */ val shouldHideFooterView: Flow<Boolean> by lazy { SceneContainerFlag.assertInLegacyMode() - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - flowOf(false) - } else { - // When the shade is closed, the footer is still present in the list, but not visible. - // This prevents the footer from being shown when a HUN is present, while still allowing - // the footer to be counted as part of the shade for measurements. - shadeInteractor.shadeExpansion - .map { it == 0f } - .distinctUntilChanged() - .dumpWhileCollecting("shouldHideFooterView") - .flowOn(bgDispatcher) - } + // When the shade is closed, the footer is still present in the list, but not visible. + // This prevents the footer from being shown when a HUN is present, while still allowing + // the footer to be counted as part of the shade for measurements. + shadeInteractor.shadeExpansion + .map { it == 0f } + .distinctUntilChanged() + .dumpWhileCollecting("shouldHideFooterView") + .flowOn(bgDispatcher) } /** @@ -188,68 +174,64 @@ constructor( */ val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy { SceneContainerFlag.assertInLegacyMode() - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - flowOf(AnimatedValue.NotAnimating(false)) - } else { - combine( - activeNotificationsInteractor.areAnyNotificationsPresent, - userSetupInteractor.isUserSetUp, - notificationStackInteractor.isShowingOnLockscreen, - shadeInteractor.isQsFullscreen, - remoteInputInteractor.isRemoteInputActive, - ) { - hasNotifications, - isUserSetUp, - isShowingOnLockscreen, - qsFullScreen, - isRemoteInputActive -> - when { - !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION - // Hide the footer until the user setup is complete, to prevent access - // to settings (b/193149550). - !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION - // Do not show the footer if the lockscreen is visible (incl. AOD), - // except if the shade is opened on top. See also b/219680200. - // Do not animate, as that makes the footer appear briefly when - // transitioning between the shade and keyguard. - isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION - // Do not show the footer if quick settings are fully expanded (except - // for the foldable split shade view). See b/201427195 && b/222699879. - qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION - // Hide the footer if remote input is active (i.e. user is replying to a - // notification). See b/75984847. - isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION - else -> VisibilityChange.APPEAR_WITH_ANIMATION - } + combine( + activeNotificationsInteractor.areAnyNotificationsPresent, + userSetupInteractor.isUserSetUp, + notificationStackInteractor.isShowingOnLockscreen, + shadeInteractor.isQsFullscreen, + remoteInputInteractor.isRemoteInputActive, + ) { + hasNotifications, + isUserSetUp, + isShowingOnLockscreen, + qsFullScreen, + isRemoteInputActive -> + when { + !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + // Hide the footer until the user setup is complete, to prevent access + // to settings (b/193149550). + !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + // Do not show the footer if the lockscreen is visible (incl. AOD), + // except if the shade is opened on top. See also b/219680200. + // Do not animate, as that makes the footer appear briefly when + // transitioning between the shade and keyguard. + isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION + // Do not show the footer if quick settings are fully expanded (except + // for the foldable split shade view). See b/201427195 && b/222699879. + qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + // Hide the footer if remote input is active (i.e. user is replying to a + // notification). See b/75984847. + isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + else -> VisibilityChange.APPEAR_WITH_ANIMATION } - .distinctUntilChanged( - // Equivalent unless visibility changes - areEquivalent = { a: VisibilityChange, b: VisibilityChange -> - a.visible == b.visible - } - ) - // 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)) } - ) { visibilityChange, (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 && visibilityChange.canAnimate - AnimatableEvent(visibilityChange.visible, shouldAnimate) + } + .distinctUntilChanged( + // Equivalent unless visibility changes + areEquivalent = { a: VisibilityChange, b: VisibilityChange -> + a.visible == b.visible } - .toAnimatedValueFlow() - .dumpWhileCollecting("shouldIncludeFooterView") - .flowOn(bgDispatcher) - } + ) + // 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)) } + ) { visibilityChange, (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 && visibilityChange.canAnimate + AnimatableEvent(visibilityChange.visible, shouldAnimate) + } + .toAnimatedValueFlow() + .dumpWhileCollecting("shouldIncludeFooterView") + .flowOn(bgDispatcher) } // This flow replaces shouldHideFooterView+shouldIncludeFooterView in flexiglass. @@ -328,25 +310,15 @@ constructor( APPEAR_WITH_ANIMATION(visible = true, canAnimate = true), } - val hasClearableAlertingNotifications: Flow<Boolean> by lazy { - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - flowOf(false) - } else { - activeNotificationsInteractor.hasClearableAlertingNotifications.dumpWhileCollecting( - "hasClearableAlertingNotifications" - ) - } - } + val hasClearableAlertingNotifications: Flow<Boolean> = + activeNotificationsInteractor.hasClearableAlertingNotifications.dumpWhileCollecting( + "hasClearableAlertingNotifications" + ) - val hasNonClearableSilentNotifications: Flow<Boolean> by lazy { - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - flowOf(false) - } else { - activeNotificationsInteractor.hasNonClearableSilentNotifications.dumpWhileCollecting( - "hasNonClearableSilentNotifications" - ) - } - } + val hasNonClearableSilentNotifications: Flow<Boolean> = + activeNotificationsInteractor.hasNonClearableSilentNotifications.dumpWhileCollecting( + "hasNonClearableSilentNotifications" + ) val topHeadsUpRow: Flow<HeadsUpRowKey?> by lazy { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index fc8c70fb8e9a..f0455fc3a22b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -42,6 +42,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED +import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel @@ -154,6 +155,7 @@ constructor( private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, private val primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, + private val primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>, aodBurnInViewModel: AodBurnInViewModel, private val communalSceneInteractor: CommunalSceneInteractor, // Lazy because it's only used in the SceneContainer + Dual Shade configuration. @@ -562,7 +564,7 @@ constructor( lockscreenToDreamingTransitionViewModel.lockscreenAlpha, lockscreenToGoneTransitionViewModel.notificationAlpha(viewState), lockscreenToOccludedTransitionViewModel.lockscreenAlpha, - lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha, + lockscreenToPrimaryBouncerTransitionViewModel.notificationAlpha, alternateBouncerToPrimaryBouncerTransitionViewModel.notificationAlpha, occludedToAodTransitionViewModel.lockscreenAlpha, occludedToGoneTransitionViewModel.notificationAlpha(viewState), @@ -626,6 +628,12 @@ constructor( .dumpWhileCollecting("keyguardAlpha") } + val blurRadius = + primaryBouncerTransitions + .map { transition -> transition.notificationBlurRadius } + .merge() + .dumpWhileCollecting("blurRadius") + /** * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB or * DREAMING<->GLANCEABLE_HUB transition or idle on the hub. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 6f29f618ee0d..afc5bc67c0a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -750,7 +750,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp BiometricUnlockSource.Companion.fromBiometricSourceType(biometricSourceType) ); } else if (biometricSourceType == BiometricSourceType.FINGERPRINT - && mUpdateMonitor.isUdfpsSupported()) { + && mUpdateMonitor.isOpticalUdfpsSupported()) { long currUptimeMillis = mSystemClock.uptimeMillis(); if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) { mNumConsecutiveFpFailures += 1; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 1474789ea0e3..b146b92ed110 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1487,14 +1487,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mActivityTransitionAnimator.setCallback(mActivityTransitionAnimatorCallback); mActivityTransitionAnimator.addListener(mActivityTransitionAnimatorListener); mRemoteInputManager.addControllerCallback(mNotificationShadeWindowController); - mStackScrollerController.setNotificationActivityStarter( - mNotificationActivityStarterLazy.get()); mGutsManager.setNotificationActivityStarter(mNotificationActivityStarterLazy.get()); mShadeController.setNotificationPresenter(mPresenterLazy.get()); mNotificationsController.initialize( mPresenterLazy.get(), mNotifListContainer, - mStackScrollerController.getNotifStackController(), mNotificationActivityStarterLazy.get()); mWindowRootViewVisibilityInteractor.setUp(mPresenterLazy.get(), mNotificationsController); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 324db79a4078..d43fed0cbf59 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -43,6 +43,7 @@ import android.view.animation.Interpolator; import androidx.annotation.FloatRange; import androidx.annotation.Nullable; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.colorextraction.ColorExtractor.GradientColors; import com.android.internal.graphics.ColorUtils; @@ -554,7 +555,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump final ScrimState oldState = mState; mState = state; - Trace.traceCounter(Trace.TRACE_TAG_APP, "scrim_state", mState.ordinal()); + TrackTracer.instantForGroup("scrim", "state", mState.ordinal()); if (mCallback != null) { mCallback.onCancelled(); @@ -1279,10 +1280,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump tint = getDebugScrimTint(scrimView); } - Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_alpha", + TrackTracer.instantForGroup("scrim", getScrimName(scrimView) + "_alpha", (int) (alpha * 255)); - - Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_tint", + TrackTracer.instantForGroup("scrim", getScrimName(scrimView) + "_tint", Color.alpha(tint)); scrimView.setTint(tint); if (!mIsBouncerToGoneTransitionRunning) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 198859a9013d..8dcb66312558 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -17,8 +17,8 @@ package com.android.systemui.statusbar.phone; import android.graphics.Color; -import android.os.Trace; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.systemui.dock.DockManager; import com.android.systemui.res.R; import com.android.systemui.scrim.ScrimView; @@ -425,11 +425,11 @@ public enum ScrimState { tint = scrim == mScrimInFront ? ScrimController.DEBUG_FRONT_TINT : ScrimController.DEBUG_BEHIND_TINT; } - Trace.traceCounter(Trace.TRACE_TAG_APP, + TrackTracer.instantForGroup("scrim", scrim == mScrimInFront ? "front_scrim_alpha" : "back_scrim_alpha", (int) (alpha * 255)); - Trace.traceCounter(Trace.TRACE_TAG_APP, + TrackTracer.instantForGroup("scrim", scrim == mScrimInFront ? "front_scrim_tint" : "back_scrim_tint", Color.alpha(tint)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 3749b96199f6..8443edd6aa87 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.phone; import static android.view.WindowInsets.Type.navigationBars; -import static com.android.systemui.Flags.predictiveBackAnimateBouncer; import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN; import static com.android.systemui.plugins.ActivityStarter.OnDismissAction; import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; @@ -328,7 +327,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private float mQsExpansion; final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>(); - private boolean mIsBackAnimationEnabled; private final UdfpsOverlayInteractor mUdfpsOverlayInteractor; private final ActivityStarter mActivityStarter; @@ -434,7 +432,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null); mAlternateBouncerInteractor = alternateBouncerInteractor; mBouncerInteractor = bouncerInteractor; - mIsBackAnimationEnabled = predictiveBackAnimateBouncer(); mUdfpsOverlayInteractor = udfpsOverlayInteractor; mActivityStarter = activityStarter; mKeyguardTransitionInteractor = keyguardTransitionInteractor; @@ -630,7 +627,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private boolean shouldPlayBackAnimation() { // Suppress back animation when bouncer shouldn't be dismissed on back invocation. - return !needsFullscreenBouncer() && mIsBackAnimationEnabled; + return !needsFullscreenBouncer(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index 03324d2a3e6a..c47ed1722bb4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.phone; -import static com.android.systemui.Flags.predictiveBackAnimateDialogs; - import android.app.AlertDialog; import android.app.Dialog; import android.content.BroadcastReceiver; @@ -285,15 +283,13 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh for (int i = 0; i < mOnCreateRunnables.size(); i++) { mOnCreateRunnables.get(i).run(); } - if (predictiveBackAnimateDialogs()) { - View targetView = getWindow().getDecorView(); - DialogKt.registerAnimationOnBackInvoked( - /* dialog = */ this, - /* targetView = */ targetView, - /* backAnimationSpec= */mDelegate.getBackAnimationSpec( - () -> targetView.getResources().getDisplayMetrics()) - ); - } + View targetView = getWindow().getDecorView(); + DialogKt.registerAnimationOnBackInvoked( + /* dialog = */ this, + /* targetView = */ targetView, + /* backAnimationSpec= */mDelegate.getBackAnimationSpec( + () -> targetView.getResources().getDisplayMetrics()) + ); } private void updateWindowSize() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index c31e34c50b06..e622d8f52894 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -81,6 +81,7 @@ import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder; import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; @@ -142,6 +143,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private StatusBarVisibilityModel mLastModifiedVisibility = StatusBarVisibilityModel.createDefaultModel(); private DarkIconManager mDarkIconManager; + private HomeStatusBarViewModel mHomeStatusBarViewModel; + private final HomeStatusBarComponent.Factory mHomeStatusBarComponentFactory; private final CommandQueue mCommandQueue; private final CollapsedStatusBarFragmentLogger mCollapsedStatusBarFragmentLogger; @@ -151,8 +154,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final ShadeExpansionStateManager mShadeExpansionStateManager; private final StatusBarIconController mStatusBarIconController; private final CarrierConfigTracker mCarrierConfigTracker; - private final HomeStatusBarViewModel mHomeStatusBarViewModel; private final HomeStatusBarViewBinder mHomeStatusBarViewBinder; + private final HomeStatusBarViewModelFactory mHomeStatusBarViewModelFactory; private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; private final DarkIconManager.Factory mDarkIconManagerFactory; private final SecureSettings mSecureSettings; @@ -256,7 +259,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue ShadeExpansionStateManager shadeExpansionStateManager, StatusBarIconController statusBarIconController, DarkIconManager.Factory darkIconManagerFactory, - HomeStatusBarViewModel homeStatusBarViewModel, + HomeStatusBarViewModelFactory homeStatusBarViewModelFactory, HomeStatusBarViewBinder homeStatusBarViewBinder, StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager, KeyguardStateController keyguardStateController, @@ -281,7 +284,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mAnimationScheduler = animationScheduler; mShadeExpansionStateManager = shadeExpansionStateManager; mStatusBarIconController = statusBarIconController; - mHomeStatusBarViewModel = homeStatusBarViewModel; + mHomeStatusBarViewModelFactory = homeStatusBarViewModelFactory; mHomeStatusBarViewBinder = homeStatusBarViewBinder; mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager; mDarkIconManagerFactory = darkIconManagerFactory; @@ -410,6 +413,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mCarrierConfigTracker.addCallback(mCarrierConfigCallback); mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener); + mHomeStatusBarViewModel = mHomeStatusBarViewModelFactory.create(displayId); mHomeStatusBarViewBinder.bind( view.getContext().getDisplayId(), mStatusBar, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index c57cede754d3..f56c2d5dc5e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -18,8 +18,6 @@ package com.android.systemui.statusbar.phone.ongoingcall import android.app.ActivityManager import android.app.IActivityManager -import android.app.Notification -import android.app.Notification.CallStyle.CALL_TYPE_ONGOING import android.app.PendingIntent import android.app.UidObserver import android.content.Context @@ -44,9 +42,6 @@ import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.view.ChipChronometer import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.notification.shared.CallType @@ -60,7 +55,9 @@ import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -/** A controller to handle the ongoing call chip in the collapsed status bar. +/** + * A controller to handle the ongoing call chip in the collapsed status bar. + * * @deprecated Use [OngoingCallInteractor] instead, which follows recommended architecture patterns */ @Deprecated("Use OngoingCallInteractor instead") @@ -71,7 +68,6 @@ constructor( @Application private val scope: CoroutineScope, private val context: Context, private val ongoingCallRepository: OngoingCallRepository, - private val notifCollection: CommonNotifCollection, private val activeNotificationsInteractor: ActiveNotificationsInteractor, private val systemClock: SystemClock, private val activityStarter: ActivityStarter, @@ -90,105 +86,24 @@ constructor( private val mListeners: MutableList<OngoingCallListener> = mutableListOf() private val uidObserver = CallAppUidObserver() - private val notifListener = - object : NotifCollectionListener { - // Temporary workaround for b/178406514 for testing purposes. - // - // b/178406514 means that posting an incoming call notif then updating it to an ongoing - // call notif does not work (SysUI never receives the update). This workaround allows us - // to trigger the ongoing call chip when an ongoing call notif is *added* rather than - // *updated*, allowing us to test the chip. - // - // TODO(b/183229367): Remove this function override when b/178406514 is fixed. - override fun onEntryAdded(entry: NotificationEntry) { - onEntryUpdated(entry, true) - } - - override fun onEntryUpdated(entry: NotificationEntry) { - StatusBarUseReposForCallChip.assertInLegacyMode() - // We have a new call notification or our existing call notification has been - // updated. - // TODO(b/183229367): This likely won't work if you take a call from one app then - // switch to a call from another app. - if ( - callNotificationInfo == null && isCallNotification(entry) || - (entry.sbn.key == callNotificationInfo?.key) - ) { - val newOngoingCallInfo = - CallNotificationInfo( - entry.sbn.key, - entry.sbn.notification.getWhen(), - // In this old listener pattern, we don't have access to the - // notification icon. - notificationIconView = null, - entry.sbn.notification.contentIntent, - entry.sbn.uid, - entry.sbn.notification.extras.getInt( - Notification.EXTRA_CALL_TYPE, - -1, - ) == CALL_TYPE_ONGOING, - statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false, - ) - if (newOngoingCallInfo == callNotificationInfo) { - return - } - - callNotificationInfo = newOngoingCallInfo - if (newOngoingCallInfo.isOngoing) { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = newOngoingCallInfo.key }, - { "Call notif *is* ongoing -> showing chip. key=$str1" }, - ) - updateChip() - } else { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = newOngoingCallInfo.key }, - { "Call notif not ongoing -> hiding chip. key=$str1" }, - ) - removeChip() - } - } - } - - override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { - if (entry.sbn.key == callNotificationInfo?.key) { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = entry.sbn.key }, - { "Call notif removed -> hiding chip. key=$str1" }, - ) - removeChip() - } - } - } override fun start() { - if (StatusBarChipsModernization.isEnabled) - return + if (StatusBarChipsModernization.isEnabled) return dumpManager.registerDumpable(this) - if (Flags.statusBarUseReposForCallChip()) { - scope.launch { - // Listening to [ActiveNotificationsInteractor] instead of using - // [NotifCollectionListener#onEntryUpdated] is better for two reasons: - // 1. ActiveNotificationsInteractor automatically filters the notification list to - // just notifications for the current user, which ensures we don't show a call chip - // for User 1's call while User 2 is active (see b/328584859). - // 2. ActiveNotificationsInteractor only emits notifications that are currently - // present in the shade, which means we know we've already inflated the icon that we - // might use for the call chip (see b/354930838). - activeNotificationsInteractor.ongoingCallNotification.collect { - updateInfoFromNotifModel(it) - } + scope.launch { + // Listening to [ActiveNotificationsInteractor] instead of using + // [NotifCollectionListener#onEntryUpdated] is better for two reasons: + // 1. ActiveNotificationsInteractor automatically filters the notification list to + // just notifications for the current user, which ensures we don't show a call chip + // for User 1's call while User 2 is active (see b/328584859). + // 2. ActiveNotificationsInteractor only emits notifications that are currently + // present in the shade, which means we know we've already inflated the icon that we + // might use for the call chip (see b/354930838). + activeNotificationsInteractor.ongoingCallNotification.collect { + updateInfoFromNotifModel(it) } - } else { - notifCollection.addCollectionListener(notifListener) } scope.launch { @@ -244,21 +159,12 @@ constructor( logger.log( TAG, LogLevel.DEBUG, - { - bool1 = Flags.statusBarCallChipNotificationIcon() - bool2 = currentInfo.notificationIconView != null - }, - { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" }, + { bool1 = currentInfo.notificationIconView != null }, + { "Creating OngoingCallModel.InCall. hasIcon=$bool1" }, ) - val icon = - if (Flags.statusBarCallChipNotificationIcon()) { - currentInfo.notificationIconView - } else { - null - } return OngoingCallModel.InCall( startTimeMs = currentInfo.callStartTime, - notificationIconView = icon, + notificationIconView = currentInfo.notificationIconView, intent = currentInfo.intent, notificationKey = currentInfo.key, ) @@ -597,8 +503,4 @@ constructor( } } -private fun isCallNotification(entry: NotificationEntry): Boolean { - return entry.sbn.notification.isStyle(Notification.CallStyle::class.java) -} - private const val TAG = OngoingCallRepository.TAG diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt deleted file mode 100644 index 4bdd90ebff3e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone.ongoingcall - -import com.android.systemui.Flags -import com.android.systemui.flags.FlagToken -import com.android.systemui.flags.RefactorFlagUtils - -/** Helper for reading or using the status bar use repos for call chip flag state. */ -@Suppress("NOTHING_TO_INLINE") -object StatusBarUseReposForCallChip { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the refactor enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.statusBarUseReposForCallChip() - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an eng - * build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception if - * the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 96666d83b39b..c71162a22d2f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -56,8 +56,8 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinderImpl -import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel -import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl.HomeStatusBarViewModelFactoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher @@ -148,7 +148,9 @@ abstract class StatusBarPipelineModule { abstract fun bindCarrierConfigStartable(impl: CarrierConfigCoreStartable): CoreStartable @Binds - abstract fun homeStatusBarViewModel(impl: HomeStatusBarViewModelImpl): HomeStatusBarViewModel + abstract fun homeStatusBarViewModelFactory( + impl: HomeStatusBarViewModelFactoryImpl + ): HomeStatusBarViewModelFactory @Binds abstract fun homeStatusBarViewBinder(impl: HomeStatusBarViewBinderImpl): HomeStatusBarViewBinder diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 7e06c35315f9..31d6d86d1b37 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -41,9 +41,11 @@ import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationSt import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ConnectedDisplaysStatusBarNotificationIconViewStore import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel import javax.inject.Inject +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch /** @@ -106,32 +108,39 @@ constructor( } if (NotificationsLiveDataStoreRefactor.isEnabled) { - val displayId = view.display.displayId val lightsOutView: View = view.requireViewById(R.id.notification_lights_out) launch { - viewModel.areNotificationsLightsOut(displayId).collect { show -> + viewModel.areNotificationsLightsOut.collect { show -> animateLightsOutView(lightsOutView, show) } } } - if (Flags.statusBarScreenSharingChips() && !StatusBarNotifChips.isEnabled) { - val primaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_primary) + if ( + Flags.statusBarScreenSharingChips() && + !StatusBarNotifChips.isEnabled && + !StatusBarChipsModernization.isEnabled + ) { + val primaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_primary) + ) launch { viewModel.primaryOngoingActivityChip.collect { primaryChipModel -> OngoingActivityChipBinder.bind( primaryChipModel, - primaryChipView, + primaryChipViewBinding, iconViewStore, ) if (StatusBarRootModernization.isEnabled) { when (primaryChipModel) { is OngoingActivityChipModel.Shown -> - primaryChipView.show(shouldAnimateChange = true) + primaryChipViewBinding.rootView.show( + shouldAnimateChange = true + ) is OngoingActivityChipModel.Hidden -> - primaryChipView.hide( + primaryChipViewBinding.rootView.hide( state = View.GONE, shouldAnimateChange = primaryChipModel.shouldAnimate, ) @@ -157,29 +166,39 @@ constructor( } } - if (Flags.statusBarScreenSharingChips() && StatusBarNotifChips.isEnabled) { - val primaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_primary) - val secondaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_secondary) + if ( + Flags.statusBarScreenSharingChips() && + StatusBarNotifChips.isEnabled && + !StatusBarChipsModernization.isEnabled + ) { + // Create view bindings here so we don't keep re-fetching child views each time + // the chip model changes. + val primaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_primary) + ) + val secondaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_secondary) + ) launch { - viewModel.ongoingActivityChips.collect { chips -> + viewModel.ongoingActivityChips.collectLatest { chips -> OngoingActivityChipBinder.bind( chips.primary, - primaryChipView, + primaryChipViewBinding, iconViewStore, ) - // TODO(b/364653005): Don't show the secondary chip if there isn't - // enough space for it. OngoingActivityChipBinder.bind( chips.secondary, - secondaryChipView, + secondaryChipViewBinding, iconViewStore, ) if (StatusBarRootModernization.isEnabled) { - primaryChipView.adjustVisibility(chips.primary.toVisibilityModel()) - secondaryChipView.adjustVisibility( + primaryChipViewBinding.rootView.adjustVisibility( + chips.primary.toVisibilityModel() + ) + secondaryChipViewBinding.rootView.adjustVisibility( chips.secondary.toVisibilityModel() ) } else { @@ -192,6 +211,18 @@ constructor( shouldAnimate = true, ) } + + viewModel.contentArea.collect { _ -> + OngoingActivityChipBinder.resetPrimaryChipWidthRestrictions( + primaryChipViewBinding, + viewModel.ongoingActivityChips.value.primary, + ) + OngoingActivityChipBinder.resetSecondaryChipWidthRestrictions( + secondaryChipViewBinding, + viewModel.ongoingActivityChips.value.secondary, + ) + view.requestLayout() + } } } } @@ -209,7 +240,7 @@ constructor( StatusBarOperatorNameViewBinder.bind( operatorNameView, viewModel.operatorNameViewModel, - viewModel::areaTint, + viewModel.areaTint, ) launch { viewModel.shouldShowOperatorNameView.collect { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt index b7744d34560d..5dd76f4434f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt @@ -32,19 +32,16 @@ object StatusBarOperatorNameViewBinder { fun bind( operatorFrameView: View, viewModel: StatusBarOperatorNameViewModel, - areaTint: (Int) -> Flow<StatusBarTintColor>, + areaTint: Flow<StatusBarTintColor>, ) { operatorFrameView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { - val displayId = operatorFrameView.display.displayId - val operatorNameText = operatorFrameView.requireViewById<TextView>(R.id.operator_name) launch { viewModel.operatorName.collect { operatorNameText.text = it } } launch { - val tint = areaTint(displayId) - tint.collect { statusBarTintColors -> + areaTint.collect { statusBarTintColors -> operatorNameText.setTextColor( statusBarTintColors.tint(operatorNameText.viewBoundsOnScreen()) ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index 7243ba7def58..71e19188f309 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -33,9 +34,11 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.compose.theme.PlatformTheme import com.android.keyguard.AlphaOptimizedLinearLayout import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips @@ -53,13 +56,14 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarIco import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory import javax.inject.Inject /** Factory to simplify the dependency management for [StatusBarRoot] */ class StatusBarRootFactory @Inject constructor( - private val homeStatusBarViewModel: HomeStatusBarViewModel, + private val homeStatusBarViewModelFactory: HomeStatusBarViewModelFactory, private val homeStatusBarViewBinder: HomeStatusBarViewBinder, private val notificationIconsBinder: NotificationIconContainerStatusBarViewBinder, private val darkIconManagerFactory: DarkIconManager.Factory, @@ -70,13 +74,14 @@ constructor( ) { fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView { val composeView = ComposeView(root.context) + val displayId = root.context.displayId val darkIconDispatcher = darkIconDispatcherStore.forDisplay(root.context.displayId) ?: return composeView composeView.apply { setContent { StatusBarRoot( parent = root, - statusBarViewModel = homeStatusBarViewModel, + statusBarViewModel = homeStatusBarViewModelFactory.create(displayId), statusBarViewBinder = homeStatusBarViewBinder, notificationIconsBinder = notificationIconsBinder, darkIconManagerFactory = darkIconManagerFactory, @@ -158,17 +163,64 @@ fun StatusBarRoot( darkIconDispatcher, ) iconController.addIconGroup(darkIconManager) + + if (StatusBarChipsModernization.isEnabled) { + val startSideExceptHeadsUp = + phoneStatusBarView.requireViewById<LinearLayout>( + R.id.status_bar_start_side_except_heads_up + ) + + val composeView = + ComposeView(context).apply { + layoutParams = + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + ) + + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + + setContent { + PlatformTheme { + val chips by + statusBarViewModel.ongoingActivityChips + .collectAsStateWithLifecycle() + OngoingActivityChips(chips = chips) + } + } + } + + // Add the composable container for ongoingActivityChips before the + // notification_icon_area to maintain the same ordering for ongoing activity + // chips in the status bar layout. + val notificationIconAreaIndex = + startSideExceptHeadsUp.indexOfChild( + startSideExceptHeadsUp.findViewById(R.id.notification_icon_area) + ) + startSideExceptHeadsUp.addView(composeView, notificationIconAreaIndex) + } + HomeStatusBarIconBlockListBinder.bind( statusIconContainer, darkIconManager, statusBarViewModel.iconBlockList, ) - if (!StatusBarChipsModernization.isEnabled) { + if (StatusBarChipsModernization.isEnabled) { + // Make sure the primary chip is hidden when StatusBarChipsModernization is + // enabled. OngoingActivityChips will be shown in a composable container + // when this flag is enabled. + phoneStatusBarView + .requireViewById<View>(R.id.ongoing_activity_chip_primary) + .visibility = View.GONE + } else { ongoingCallController.setChipView( phoneStatusBarView.requireViewById(R.id.ongoing_activity_chip_primary) ) } + // For notifications, first inflate the [NotificationIconContainer] val notificationIconArea = phoneStatusBarView.requireViewById<ViewGroup>(R.id.notification_icon_area) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index c9cc17389c17..d9d9a29ee2b6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel import android.annotation.ColorInt import android.graphics.Rect import android.view.View -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -44,6 +43,7 @@ import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationSt import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipsViewModel +import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStore import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.headsup.PinnedStatus @@ -53,7 +53,9 @@ import com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteracto import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarIconBlockListInteractor import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarInteractor import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel -import javax.inject.Inject +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -118,6 +120,7 @@ interface HomeStatusBarViewModel { val shouldShowOperatorNameView: Flow<Boolean> val isClockVisible: Flow<VisibilityModel> val isNotificationIconContainerVisible: Flow<VisibilityModel> + /** * Pair of (system info visibility, event animation state). The animation state can be used to * respond to the system event chip animations. In all cases, system info visibility correctly @@ -128,6 +131,9 @@ interface HomeStatusBarViewModel { /** Which icons to block from the home status bar */ val iconBlockList: Flow<List<String>> + /** This status bar's current content area for the given rotation in absolute bounds. */ + val contentArea: Flow<Rect> + /** * Apps can request a low profile mode [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE] where * status bar and navigation icons dim. In this mode, a notification dot appears where the @@ -137,13 +143,13 @@ interface HomeStatusBarViewModel { * whether there are notifications when the device is in * [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE]. */ - fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> + val areNotificationsLightsOut: Flow<Boolean> /** - * Given a displayId, returns a flow of [StatusBarTintColor], a functional interface that will - * allow a view to calculate its correct tint depending on location + * A flow of [StatusBarTintColor], a functional interface that will allow a view to calculate + * its correct tint depending on location */ - fun areaTint(displayId: Int): Flow<StatusBarTintColor> + val areaTint: Flow<StatusBarTintColor> /** Models the current visibility for a specific child view of status bar. */ data class VisibilityModel( @@ -157,17 +163,22 @@ interface HomeStatusBarViewModel { val baseVisibility: VisibilityModel, val animationState: SystemEventAnimationState, ) + + /** Interface for the assisted factory, to allow for providing a fake in tests */ + interface HomeStatusBarViewModelFactory { + fun create(displayId: Int): HomeStatusBarViewModel + } } -@SysUISingleton class HomeStatusBarViewModelImpl -@Inject +@AssistedInject constructor( + @Assisted thisDisplayId: Int, homeStatusBarInteractor: HomeStatusBarInteractor, homeStatusBarIconBlockListInteractor: HomeStatusBarIconBlockListInteractor, - private val lightsOutInteractor: LightsOutInteractor, - private val notificationsInteractor: ActiveNotificationsInteractor, - private val darkIconInteractor: DarkIconInteractor, + lightsOutInteractor: LightsOutInteractor, + notificationsInteractor: ActiveNotificationsInteractor, + darkIconInteractor: DarkIconInteractor, headsUpNotificationInteractor: HeadsUpNotificationInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, keyguardInteractor: KeyguardInteractor, @@ -178,6 +189,7 @@ constructor( ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel, animations: SystemStatusEventAnimationInteractor, + statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore, @Application coroutineScope: CoroutineScope, ) : HomeStatusBarViewModel { override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> = @@ -211,22 +223,22 @@ constructor( } .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = false) - override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = + override val areNotificationsLightsOut: Flow<Boolean> = if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) { emptyFlow() } else { combine( notificationsInteractor.areAnyNotificationsPresent, - lightsOutInteractor.isLowProfile(displayId) ?: flowOf(false), + lightsOutInteractor.isLowProfile(thisDisplayId) ?: flowOf(false), ) { hasNotifications, isLowProfile -> hasNotifications && isLowProfile } .distinctUntilChanged() } - override fun areaTint(displayId: Int): Flow<StatusBarTintColor> = + override val areaTint: Flow<StatusBarTintColor> = darkIconInteractor - .darkState(displayId) + .darkState(thisDisplayId) .map { (areas: Collection<Rect>, tint: Int) -> StatusBarTintColor { viewBounds: Rect -> if (DarkIconDispatcher.isInAreas(areas, viewBounds)) { @@ -283,11 +295,12 @@ constructor( override val shouldShowOperatorNameView: Flow<Boolean> = combine( shouldHomeStatusBarBeVisible, - headsUpNotificationInteractor.statusBarHeadsUpState, + headsUpNotificationInteractor.statusBarHeadsUpStatus, homeStatusBarInteractor.visibilityViaDisableFlags, homeStatusBarInteractor.shouldShowOperatorName, - ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags, shouldShowOperator -> - val hideForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem + ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags, shouldShowOperator + -> + val hideForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem shouldStatusBarBeVisible && !hideForHeadsUp && visibilityViaDisableFlags.isSystemInfoAllowed && @@ -297,10 +310,10 @@ constructor( override val isClockVisible: Flow<VisibilityModel> = combine( shouldHomeStatusBarBeVisible, - headsUpNotificationInteractor.statusBarHeadsUpState, + headsUpNotificationInteractor.statusBarHeadsUpStatus, homeStatusBarInteractor.visibilityViaDisableFlags, - ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags -> - val hideClockForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem + ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags -> + val hideClockForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem val showClock = shouldStatusBarBeVisible && visibilityViaDisableFlags.isClockAllowed && @@ -356,6 +369,10 @@ constructor( override val iconBlockList: Flow<List<String>> = homeStatusBarIconBlockListInteractor.iconBlockList + override val contentArea: Flow<Rect> = + statusBarContentInsetsViewModelStore.forDisplay(thisDisplayId)?.contentArea + ?: flowOf(Rect(0, 0, 0, 0)) + @View.Visibility private fun Boolean.toVisibleOrGone(): Int { return if (this) View.VISIBLE else View.GONE @@ -364,6 +381,13 @@ constructor( // Similar to the above, but uses INVISIBLE in place of GONE @View.Visibility private fun Boolean.toVisibleOrInvisible(): Int = if (this) View.VISIBLE else View.INVISIBLE + + /** Inject this to create the display-dependent view model */ + @AssistedFactory + interface HomeStatusBarViewModelFactoryImpl : + HomeStatusBarViewModel.HomeStatusBarViewModelFactory { + override fun create(displayId: Int): HomeStatusBarViewModelImpl + } } /** Lookup the color for a given view in the status bar */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java index 31cae79c6b94..81d06a8db0b6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java @@ -32,6 +32,7 @@ import android.os.Trace; import androidx.annotation.VisibleForTesting; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; @@ -241,7 +242,7 @@ public class KeyguardStateControllerImpl implements KeyguardStateController { private void setKeyguardFadingAway(boolean keyguardFadingAway) { if (mKeyguardFadingAway != keyguardFadingAway) { - Trace.traceCounter(Trace.TRACE_TAG_APP, "keyguardFadingAway", + TrackTracer.instantForGroup("keyguard", "FadingAway", keyguardFadingAway ? 1 : 0); mKeyguardFadingAway = keyguardFadingAway; invokeForEachCallback(Callback::onKeyguardFadingAwayChanged); @@ -356,7 +357,7 @@ public class KeyguardStateControllerImpl implements KeyguardStateController { @Override public void notifyKeyguardGoingAway(boolean keyguardGoingAway) { if (mKeyguardGoingAway != keyguardGoingAway) { - Trace.traceCounter(Trace.TRACE_TAG_APP, "keyguardGoingAway", + Trace.traceCounter(Trace.TRACE_TAG_APP, "keyguard##GoingAway", keyguardGoingAway ? 1 : 0); mKeyguardGoingAway = keyguardGoingAway; mKeyguardInteractorLazy.get().setIsKeyguardGoingAway(keyguardGoingAway); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt index 56c9e9abbc36..cb26679434ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt @@ -41,6 +41,7 @@ import android.view.ViewGroup import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.Button +import com.android.systemui.Flags import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.shared.system.ActivityManagerWrapper @@ -52,6 +53,7 @@ import com.android.systemui.statusbar.SmartReplyController import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.notification.logging.NotificationLogger +import com.android.systemui.statusbar.notification.row.MagicActionBackgroundDrawable import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.policy.InflatedSmartReplyState.SuppressedActions import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions @@ -400,6 +402,15 @@ constructor( .apply { text = action.title + if (Flags.notificationMagicActionsTreatment()) { + if ( + smartActions.fromAssistant && + action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false) + ) { + background = MagicActionBackgroundDrawable(parent.context) + } + } + // We received the Icon from the application - so use the Context of the application // to // reference icon resources. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt index 12ed647fdee7..fdc2d8d96f9b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt @@ -16,8 +16,8 @@ package com.android.systemui.statusbar.policy.domain.interactor -import android.app.NotificationManager.INTERRUPTION_FILTER_NONE import android.content.Context +import android.media.AudioManager import android.provider.Settings import android.provider.Settings.Secure.ZEN_DURATION_FOREVER import android.provider.Settings.Secure.ZEN_DURATION_PROMPT @@ -29,6 +29,7 @@ import com.android.settingslib.notification.data.repository.ZenModeRepository import com.android.settingslib.notification.modes.ZenIcon import com.android.settingslib.notification.modes.ZenIconLoader import com.android.settingslib.notification.modes.ZenMode +import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.modes.shared.ModesUi import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository @@ -67,6 +68,17 @@ constructor( deviceProvisioningRepository: DeviceProvisioningRepository, userSetupRepository: UserSetupRepository, ) { + /** + * List of predicates to determine if the [ZenMode] blocks an audio stream. Typical use case + * would be: `zenModeByStreamPredicates[stream](zenMode)` + */ + private val zenModeByStreamPredicates = + mapOf<Int, (ZenMode) -> Boolean>( + AudioManager.STREAM_MUSIC to { it.policy.priorityCategoryMedia == STATE_DISALLOW }, + AudioManager.STREAM_ALARM to { it.policy.priorityCategoryAlarms == STATE_DISALLOW }, + AudioManager.STREAM_SYSTEM to { it.policy.priorityCategorySystem == STATE_DISALLOW }, + ) + val isZenAvailable: Flow<Boolean> = combine( deviceProvisioningRepository.isDeviceProvisioned, @@ -125,21 +137,16 @@ constructor( .flowOn(bgDispatcher) .distinctUntilChanged() - val activeModesBlockingEverything: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode -> - mode.interruptionFilter == INTERRUPTION_FILTER_NONE - } - - val activeModesBlockingMedia: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode -> - mode.policy.priorityCategoryMedia == STATE_DISALLOW - } - - val activeModesBlockingAlarms: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode -> - mode.policy.priorityCategoryAlarms == STATE_DISALLOW - } + fun canBeBlockedByZenMode(stream: AudioStream): Boolean = + zenModeByStreamPredicates.containsKey(stream.value) - private fun getFilteredActiveModesFlow(predicate: (ZenMode) -> Boolean): Flow<ActiveZenModes> { + fun activeModesBlockingStream(stream: AudioStream): Flow<ActiveZenModes> { + val isBlockingStream = zenModeByStreamPredicates[stream.value] + require(isBlockingStream != null) { + "$stream is unsupported. Use canBeBlockedByZenMode to check if the stream can be affected by the Zen Mode." + } return modes - .map { modes -> modes.filter { mode -> predicate(mode) } } + .map { modes -> modes.filter { isBlockingStream(it) } } .map { modes -> buildActiveZenModes(modes) } .flowOn(bgDispatcher) .distinctUntilChanged() @@ -194,7 +201,6 @@ constructor( ) null } - ZEN_DURATION_FOREVER -> null else -> Duration.ofMinutes(zenDuration.toLong()) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt new file mode 100644 index 000000000000..8e81d78d60f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 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.ui + +import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStoreModule +import dagger.Module + +@Module(includes = [StatusBarContentInsetsViewModelStoreModule::class]) +object StatusBarUiLayerModule diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt index 6175ea190697..a98a9e0c16d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt @@ -60,7 +60,7 @@ constructor( private val showingHeadsUpStatusBar: Flow<Boolean> = if (SceneContainerFlag.isEnabled) { - headsUpNotificationInteractor.statusBarHeadsUpState.map { it.isPinned } + headsUpNotificationInteractor.statusBarHeadsUpStatus.map { it.isPinned } } else { flowOf(false) } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index ae32b7a6175c..bce55cbdcc4a 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -50,7 +50,7 @@ fun BackGestureTutorialScreen( ) GestureTutorialScreen( screenConfig = screenConfig, - gestureUiStateFlow = viewModel.gestureUiState, + tutorialStateFlow = viewModel.tutorialState, motionEventConsumer = { easterEggGestureViewModel.accept(it) viewModel.handleEvent(it) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt index 73c54af595d9..284e23e5a288 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt @@ -18,7 +18,6 @@ package com.android.systemui.touchpad.tutorial.ui.composable import android.view.MotionEvent import androidx.activity.compose.BackHandler -import androidx.annotation.RawRes import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box @@ -27,77 +26,21 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.systemui.inputdevice.tutorial.ui.composable.ActionTutorialContent import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.NotStarted -import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import kotlinx.coroutines.flow.Flow -sealed interface GestureUiState { - data object NotStarted : GestureUiState - - data class Finished(@RawRes val successAnimation: Int) : GestureUiState - - data class InProgress( - val progress: Float = 0f, - val progressStartMarker: String, - val progressEndMarker: String, - ) : GestureUiState - - data object Error : GestureUiState -} - -fun GestureState.toGestureUiState( - progressStartMarker: String, - progressEndMarker: String, - successAnimation: Int, -): GestureUiState { - return when (this) { - GestureState.NotStarted -> NotStarted - is GestureState.InProgress -> - GestureUiState.InProgress(this.progress, progressStartMarker, progressEndMarker) - is GestureState.Finished -> GestureUiState.Finished(successAnimation) - GestureState.Error -> GestureUiState.Error - } -} - -fun GestureUiState.toTutorialActionState(previousState: TutorialActionState): TutorialActionState { - return when (this) { - NotStarted -> TutorialActionState.NotStarted - is GestureUiState.InProgress -> { - val inProgress = - TutorialActionState.InProgress( - progress = progress, - startMarker = progressStartMarker, - endMarker = progressEndMarker, - ) - if ( - previousState is TutorialActionState.InProgressAfterError || - previousState is TutorialActionState.Error - ) { - return TutorialActionState.InProgressAfterError(inProgress) - } else { - return inProgress - } - } - is Finished -> TutorialActionState.Finished(successAnimation) - GestureUiState.Error -> TutorialActionState.Error - } -} - @Composable fun GestureTutorialScreen( screenConfig: TutorialScreenConfig, - gestureUiStateFlow: Flow<GestureUiState>, + tutorialStateFlow: Flow<TutorialActionState>, motionEventConsumer: (MotionEvent) -> Boolean, easterEggTriggeredFlow: Flow<Boolean>, onEasterEggFinished: () -> Unit, @@ -106,25 +49,21 @@ fun GestureTutorialScreen( ) { BackHandler(onBack = onBack) val easterEggTriggered by easterEggTriggeredFlow.collectAsStateWithLifecycle(false) - val gestureState by gestureUiStateFlow.collectAsStateWithLifecycle(NotStarted) + val tutorialState by tutorialStateFlow.collectAsStateWithLifecycle(NotStarted) TouchpadGesturesHandlingBox( motionEventConsumer, - gestureState, + tutorialState, easterEggTriggered, onEasterEggFinished, ) { - var lastState: TutorialActionState by remember { - mutableStateOf(TutorialActionState.NotStarted) - } - lastState = gestureState.toTutorialActionState(lastState) - ActionTutorialContent(lastState, onDoneButtonClicked, screenConfig) + ActionTutorialContent(tutorialState, onDoneButtonClicked, screenConfig) } } @Composable private fun TouchpadGesturesHandlingBox( motionEventConsumer: (MotionEvent) -> Boolean, - gestureState: GestureUiState, + tutorialState: TutorialActionState, easterEggTriggered: Boolean, onEasterEggFinished: () -> Unit, modifier: Modifier = Modifier, @@ -150,7 +89,7 @@ private fun TouchpadGesturesHandlingBox( .pointerInteropFilter( onTouchEvent = { event -> // FINISHED is the final state so we don't need to process touches anymore - if (gestureState is Finished) { + if (tutorialState is TutorialActionState.Finished) { false } else { motionEventConsumer(event) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt index 4f1f40dc4c05..4acdb6070200 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt @@ -49,7 +49,7 @@ fun HomeGestureTutorialScreen( ) GestureTutorialScreen( screenConfig = screenConfig, - gestureUiStateFlow = viewModel.gestureUiState, + tutorialStateFlow = viewModel.tutorialState, motionEventConsumer = { easterEggGestureViewModel.accept(it) viewModel.handleEvent(it) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt index 6c9e26c4b7ea..8dd53a7fb815 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt @@ -50,7 +50,7 @@ fun RecentAppsGestureTutorialScreen( ) GestureTutorialScreen( screenConfig = screenConfig, - gestureUiStateFlow = viewModel.gestureUiState, + tutorialStateFlow = viewModel.tutorialState, motionEventConsumer = { easterEggGestureViewModel.accept(it) viewModel.handleEvent(it) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt index 8e53669a7841..7a3d4d1ba88a 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt @@ -17,12 +17,12 @@ package com.android.systemui.touchpad.tutorial.ui.viewmodel import android.view.MotionEvent +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState import com.android.systemui.res.R -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState -import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent import com.android.systemui.util.kotlin.pairwiseBy import kotlinx.coroutines.flow.Flow @@ -30,21 +30,26 @@ import kotlinx.coroutines.flow.Flow class BackGestureScreenViewModel(val gestureRecognizer: GestureRecognizerAdapter) : TouchpadTutorialScreenViewModel { - override val gestureUiState: Flow<GestureUiState> = - gestureRecognizer.gestureState.pairwiseBy(GestureState.NotStarted) { previous, current -> - toGestureUiState(current, previous) - } + override val tutorialState: Flow<TutorialActionState> = + gestureRecognizer.gestureState + .pairwiseBy(NotStarted) { previous, current -> + current to toAnimationProperties(current, previous) + } + .mapToTutorialState() override fun handleEvent(event: MotionEvent): Boolean { return gestureRecognizer.handleTouchpadMotionEvent(event) } - private fun toGestureUiState(current: GestureState, previous: GestureState): GestureUiState { + private fun toAnimationProperties( + current: GestureState, + previous: GestureState, + ): TutorialAnimationProperties { val (startMarker, endMarker) = if (current is InProgress && current.direction == GestureDirection.LEFT) { "gesture to L" to "end progress L" } else "gesture to R" to "end progress R" - return current.toGestureUiState( + return TutorialAnimationProperties( progressStartMarker = startMarker, progressEndMarker = endMarker, successAnimation = successAnimation(previous), diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt index 9d6f568fa1b1..c75d44f01e8c 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt @@ -17,9 +17,8 @@ package com.android.systemui.touchpad.tutorial.ui.viewmodel import android.view.MotionEvent +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState import com.android.systemui.res.R -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState -import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -27,14 +26,17 @@ import kotlinx.coroutines.flow.map class HomeGestureScreenViewModel(private val gestureRecognizer: GestureRecognizerAdapter) : TouchpadTutorialScreenViewModel { - override val gestureUiState: Flow<GestureUiState> = - gestureRecognizer.gestureState.map { - it.toGestureUiState( - progressStartMarker = "drag with gesture", - progressEndMarker = "release playback realtime", - successAnimation = R.raw.trackpad_home_success, - ) - } + override val tutorialState: Flow<TutorialActionState> = + gestureRecognizer.gestureState + .map { + it to + TutorialAnimationProperties( + progressStartMarker = "drag with gesture", + progressEndMarker = "release playback realtime", + successAnimation = R.raw.trackpad_home_success, + ) + } + .mapToTutorialState() override fun handleEvent(event: MotionEvent): Boolean { return gestureRecognizer.handleTouchpadMotionEvent(event) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt index 97528583277f..9fab5f3641a4 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt @@ -17,9 +17,8 @@ package com.android.systemui.touchpad.tutorial.ui.viewmodel import android.view.MotionEvent +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState import com.android.systemui.res.R -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState -import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -27,14 +26,17 @@ import kotlinx.coroutines.flow.map class RecentAppsGestureScreenViewModel(private val gestureRecognizer: GestureRecognizerAdapter) : TouchpadTutorialScreenViewModel { - override val gestureUiState: Flow<GestureUiState> = - gestureRecognizer.gestureState.map { - it.toGestureUiState( - progressStartMarker = "drag with gesture", - progressEndMarker = "onPause", - successAnimation = R.raw.trackpad_recent_apps_success, - ) - } + override val tutorialState: Flow<TutorialActionState> = + gestureRecognizer.gestureState + .map { + it to + TutorialAnimationProperties( + progressStartMarker = "drag with gesture", + progressEndMarker = "onPause", + successAnimation = R.raw.trackpad_recent_apps_success, + ) + } + .mapToTutorialState() override fun handleEvent(event: MotionEvent): Boolean { return gestureRecognizer.handleTouchpadMotionEvent(event) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt index 31e953d6643c..3b6e3c76cdeb 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt @@ -17,11 +17,62 @@ package com.android.systemui.touchpad.tutorial.ui.viewmodel import android.view.MotionEvent -import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState +import androidx.annotation.RawRes +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Finished +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow interface TouchpadTutorialScreenViewModel { - val gestureUiState: Flow<GestureUiState> + val tutorialState: Flow<TutorialActionState> fun handleEvent(event: MotionEvent): Boolean } + +data class TutorialAnimationProperties( + val progressStartMarker: String, + val progressEndMarker: String, + @RawRes val successAnimation: Int, +) + +fun Flow<Pair<GestureState, TutorialAnimationProperties>>.mapToTutorialState(): + Flow<TutorialActionState> { + return flow<TutorialActionState> { + var lastState: TutorialActionState = TutorialActionState.NotStarted + collect { (gestureState, animationProperties) -> + val newState = gestureState.toTutorialActionState(animationProperties, lastState) + lastState = newState + emit(newState) + } + } +} + +fun GestureState.toTutorialActionState( + properties: TutorialAnimationProperties, + previousState: TutorialActionState, +): TutorialActionState { + return when (this) { + NotStarted -> TutorialActionState.NotStarted + is InProgress -> { + val inProgress = + TutorialActionState.InProgress( + progress = progress, + startMarker = properties.progressStartMarker, + endMarker = properties.progressEndMarker, + ) + if ( + previousState is TutorialActionState.InProgressAfterError || + previousState is TutorialActionState.Error + ) { + TutorialActionState.InProgressAfterError(inProgress) + } else { + inProgress + } + } + is Finished -> TutorialActionState.Finished(properties.successAnimation) + GestureState.Error -> TutorialActionState.Error + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt index 65970978b4ec..7d3966b98782 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt @@ -17,8 +17,9 @@ package com.android.systemui.unfold import android.content.Context import android.hardware.devicestate.DeviceStateManager -import android.os.Trace import com.android.app.tracing.TraceStateLogger +import com.android.app.tracing.coroutines.TrackTracer +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -29,7 +30,6 @@ import com.android.systemui.util.Utils.isDeviceFoldable import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.plus /** @@ -45,7 +45,7 @@ constructor( @Application applicationScope: CoroutineScope, @Background private val coroutineContext: CoroutineContext, private val deviceStateRepository: DeviceStateRepository, - private val deviceStateManager: DeviceStateManager + private val deviceStateManager: DeviceStateManager, ) : CoreStartable { private val isFoldable: Boolean = isDeviceFoldable(context.resources, deviceStateManager) @@ -61,7 +61,7 @@ constructor( bgScope.launch { foldStateRepository.hingeAngle.collect { - Trace.traceCounter(Trace.TRACE_TAG_APP, "hingeAngle", it.toInt()) + TrackTracer.instantForGroup("unfold", "hingeAngle", it.toInt()) } } bgScope.launch { diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt index d9a2e956cc86..a88b127ae157 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt @@ -17,10 +17,14 @@ package com.android.systemui.util.kotlin import android.content.Context +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn class Utils { companion object { @@ -32,6 +36,7 @@ class Utils { fun <A, B, C, D> toQuad(a: A, bcd: Triple<B, C, D>) = Quad(a, bcd.first, bcd.second, bcd.third) + fun <A, B, C, D> toQuad(abc: Triple<A, B, C>, d: D) = Quad(abc.first, abc.second, abc.third, d) @@ -51,7 +56,7 @@ class Utils { bcdefg.third, bcdefg.fourth, bcdefg.fifth, - bcdefg.sixth + bcdefg.sixth, ) /** @@ -81,7 +86,7 @@ class Utils { fun <A, B, C, D> Flow<A>.sample( b: Flow<B>, c: Flow<C>, - d: Flow<D> + d: Flow<D>, ): Flow<Quad<A, B, C, D>> { return this.sample(combine(b, c, d, ::Triple), ::toQuad) } @@ -134,6 +139,20 @@ class Utils { ): Flow<Septuple<A, B, C, D, E, F, G>> { return this.sample(combine(b, c, d, e, f, g, ::Sextuple), ::toSeptuple) } + + /** + * Combines 2 state flows, applying [transform] between the initial values to set the + * initial value of the resulting StateFlow. + */ + fun <A, B, R> combineState( + f1: StateFlow<A>, + f2: StateFlow<B>, + scope: CoroutineScope, + sharingStarted: SharingStarted, + transform: (A, B) -> R, + ): StateFlow<R> = + combine(f1, f2) { a, b -> transform(a, b) } + .stateIn(scope, sharingStarted, transform(f1.value, f2.value)) } } @@ -144,7 +163,7 @@ data class Quint<A, B, C, D, E>( val second: B, val third: C, val fourth: D, - val fifth: E + val fifth: E, ) data class Sextuple<A, B, C, D, E, F>( diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt index fa1088426351..3b0c8a6b46f8 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt @@ -46,7 +46,7 @@ class VolumeDialogCallbacksInteractor constructor( private val volumeDialogController: VolumeDialogController, @VolumeDialogPlugin private val coroutineScope: CoroutineScope, - @Background private val bgHandler: Handler, + @Background private val bgHandler: Handler?, ) { @SuppressLint("SharedFlowCreation") // event-bus needed diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt index e8d19dd5e0e4..96630ca36b97 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt @@ -51,6 +51,8 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch private const val CLOSE_DRAWER_DELAY = 300L +// Ensure roundness and color of button is updated when progress is changed by a minimum fraction. +private const val BUTTON_MIN_VISIBLE_CHANGE = 0.05F @OptIn(ExperimentalCoroutinesApi::class) @VolumeDialogScope @@ -58,12 +60,12 @@ class VolumeDialogRingerViewBinder @Inject constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { private val roundnessSpringForce = - SpringForce(0F).apply { + SpringForce(1F).apply { stiffness = 800F dampingRatio = 0.6F } private val colorSpringForce = - SpringForce(0F).apply { + SpringForce(1F).apply { stiffness = 3800F dampingRatio = 1F } @@ -257,30 +259,35 @@ constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { // We only need to execute on roundness animation end and volume dialog background // progress update once because these changes should be applied once on volume dialog // background and ringer drawer views. - val selectedCornerRadius = (selectedButton.background as GradientDrawable).cornerRadius - if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) { - selectedButton.animateTo( - selectedButtonUiModel, - if (uiModel.currentButtonIndex == count - 1) { - onProgressChanged - } else { - { _, _ -> } - }, - ) - } - val unselectedCornerRadius = - (unselectedButton.background as GradientDrawable).cornerRadius - if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) { - unselectedButton.animateTo( - unselectedButtonUiModel, - if (previousIndex == count - 1) { - onProgressChanged - } else { - { _, _ -> } - }, - ) - } coroutineScope { + val selectedCornerRadius = + (selectedButton.background as GradientDrawable).cornerRadius + if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) { + launch { + selectedButton.animateTo( + selectedButtonUiModel, + if (uiModel.currentButtonIndex == count - 1) { + onProgressChanged + } else { + { _, _ -> } + }, + ) + } + } + val unselectedCornerRadius = + (unselectedButton.background as GradientDrawable).cornerRadius + if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) { + launch { + unselectedButton.animateTo( + unselectedButtonUiModel, + if (previousIndex == count - 1) { + onProgressChanged + } else { + { _, _ -> } + }, + ) + } + } launch { delay(CLOSE_DRAWER_DELAY) bindButtons(viewModel, uiModel, onAnimationEnd, isAnimated = true) @@ -383,11 +390,14 @@ constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { onProgressChanged: (Float, Boolean) -> Unit = { _, _ -> }, ) { val roundnessAnimation = - SpringAnimation(FloatValueHolder(0F)).setSpring(roundnessSpringForce) - val colorAnimation = SpringAnimation(FloatValueHolder(0F)).setSpring(colorSpringForce) + SpringAnimation(FloatValueHolder(0F), 1F).setSpring(roundnessSpringForce) + val colorAnimation = SpringAnimation(FloatValueHolder(0F), 1F).setSpring(colorSpringForce) val radius = (background as GradientDrawable).cornerRadius val cornerRadiusDiff = ringerButtonUiModel.cornerRadius - (background as GradientDrawable).cornerRadius + + roundnessAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE + colorAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE coroutineScope { launch { colorAnimation.suspendAnimate { value -> diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt index 88af210b6a36..940c79c78d76 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt @@ -19,7 +19,6 @@ package com.android.systemui.volume.dialog.sliders.dagger import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder -import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderTouchesViewBinder import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder import dagger.BindsInstance import dagger.Subcomponent @@ -34,8 +33,6 @@ interface VolumeDialogSliderComponent { fun sliderViewBinder(): VolumeDialogSliderViewBinder - fun sliderTouchesViewBinder(): VolumeDialogSliderTouchesViewBinder - fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt index 04dc80c45a18..3988acbea7c2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt @@ -16,7 +16,9 @@ package com.android.systemui.volume.dialog.sliders.domain.interactor +import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.plugins.VolumeDialogController +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel @@ -27,6 +29,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn @@ -39,8 +43,17 @@ constructor( @VolumeDialog private val coroutineScope: CoroutineScope, volumeDialogStateInteractor: VolumeDialogStateInteractor, private val volumeDialogController: VolumeDialogController, + zenModeInteractor: ZenModeInteractor, ) { + val isDisabledByZenMode: Flow<Boolean> = + if (sliderType is VolumeDialogSliderType.Stream) { + zenModeInteractor.activeModesBlockingStream(AudioStream(sliderType.audioStream)).map { + it.mainMode != null + } + } else { + flowOf(false) + } val slider: Flow<VolumeDialogStreamModel> = volumeDialogStateInteractor.volumeDialogState .mapNotNull { diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderTouchesViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderTouchesViewBinder.kt deleted file mode 100644 index 4ecac7a81893..000000000000 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderTouchesViewBinder.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.volume.dialog.sliders.ui - -import android.annotation.SuppressLint -import android.view.View -import com.android.systemui.res.R -import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope -import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderInputEventsViewModel -import com.google.android.material.slider.Slider -import javax.inject.Inject - -@VolumeDialogSliderScope -class VolumeDialogSliderTouchesViewBinder -@Inject -constructor(private val viewModel: VolumeDialogSliderInputEventsViewModel) { - - @SuppressLint("ClickableViewAccessibility") - fun bind(view: View) { - with(view.requireViewById<Slider>(R.id.volume_dialog_slider)) { - setOnTouchListener { _, event -> - viewModel.onTouchEvent(event) - false - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index 67ffb0602860..3b964fdec1b8 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -23,6 +23,7 @@ import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce import com.android.systemui.res.R import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderInputEventsViewModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderStateModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel import com.google.android.material.slider.Slider @@ -35,7 +36,10 @@ import kotlinx.coroutines.flow.onEach @VolumeDialogSliderScope class VolumeDialogSliderViewBinder @Inject -constructor(private val viewModel: VolumeDialogSliderViewModel) { +constructor( + private val viewModel: VolumeDialogSliderViewModel, + private val inputViewModel: VolumeDialogSliderInputEventsViewModel, +) { private val sliderValueProperty = object : FloatPropertyCompat<Slider>("value") { @@ -51,16 +55,21 @@ constructor(private val viewModel: VolumeDialogSliderViewModel) { dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY } + @SuppressLint("ClickableViewAccessibility") fun CoroutineScope.bind(view: View) { var isInitialUpdate = true val sliderView: Slider = view.requireViewById(R.id.volume_dialog_slider) val animation = SpringAnimation(sliderView, sliderValueProperty) animation.spring = springForce - + sliderView.setOnTouchListener { _, event -> + inputViewModel.onTouchEvent(event) + false + } sliderView.addOnChangeListener { _, value, fromUser -> viewModel.setStreamVolume(value.roundToInt(), fromUser) } + viewModel.isDisabledByZenMode.onEach { sliderView.isEnabled = !it }.launchIn(this) viewModel.state .onEach { sliderView.setModel(it, animation, isInitialUpdate) @@ -82,7 +91,7 @@ constructor(private val viewModel: VolumeDialogSliderViewModel) { // coerce the current value to the new value range before animating it. This prevents // animating from the value that is outside of current [valueFrom, valueTo]. value = value.coerceIn(valueFrom, valueTo) - setTrackIconActiveStart(model.iconRes) + trackIconActiveStart = model.icon if (isInitialUpdate) { value = model.value } else { diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt index f066b56e7de0..75d427acc05b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt @@ -71,7 +71,6 @@ constructor(private val viewModel: VolumeDialogSlidersViewModel) { viewsToAnimate: Array<View>, ) { with(component.sliderViewBinder()) { bind(sliderContainer) } - with(component.sliderTouchesViewBinder()) { bind(sliderContainer) } with(component.sliderHapticsViewBinder()) { bind(sliderContainer) } with(component.overscrollViewBinder()) { bind(sliderContainer, viewsToAnimate) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt index 5c39b6f9359c..daf4c8275d20 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt @@ -16,13 +16,16 @@ package com.android.systemui.volume.dialog.sliders.ui.viewmodel +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.Drawable import android.media.AudioManager import androidx.annotation.DrawableRes -import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -31,11 +34,12 @@ import kotlinx.coroutines.flow.flowOf class VolumeDialogSliderIconProvider @Inject constructor( - private val notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor, + private val context: Context, + private val zenModeInteractor: ZenModeInteractor, private val audioVolumeInteractor: AudioVolumeInteractor, ) { - @DrawableRes + @SuppressLint("UseCompatLoadingForDrawables") fun getStreamIcon( stream: Int, level: Int, @@ -43,54 +47,71 @@ constructor( levelMax: Int, isMuted: Boolean, isRoutedToBluetooth: Boolean, - ): Flow<Int> { + ): Flow<Drawable> { return combine( - notificationsSoundPolicyInteractor.isZenMuted(AudioStream(stream)), + zenModeInteractor.activeModesBlockingStream(AudioStream(stream)), ringerModeForStream(stream), - ) { isZenMuted, ringerMode -> - val isStreamOffline = level == 0 || isMuted - if (isZenMuted) { - // TODO(b/372466264) use icon for the corresponding zenmode - return@combine com.android.internal.R.drawable.ic_qs_dnd - } - when (ringerMode?.value) { - AudioManager.RINGER_MODE_VIBRATE -> - return@combine R.drawable.ic_volume_ringer_vibrate - AudioManager.RINGER_MODE_SILENT -> return@combine R.drawable.ic_ring_volume_off - } - if (isRoutedToBluetooth) { - return@combine if (stream == AudioManager.STREAM_VOICE_CALL) { - R.drawable.ic_volume_bt_sco - } else { - if (isStreamOffline) { - R.drawable.ic_volume_media_bt_mute - } else { - R.drawable.ic_volume_media_bt - } - } + ) { activeModesBlockingStream, ringerMode -> + if (activeModesBlockingStream.mainMode?.icon != null) { + return@combine activeModesBlockingStream.mainMode.icon.drawable + } else { + context.getDrawable( + getIconRes( + stream, + level, + levelMin, + levelMax, + isMuted, + isRoutedToBluetooth, + ringerMode, + ) + )!! } + } + } - return@combine if (isStreamOffline) { - getMutedIconForStream(stream) ?: getIconForStream(stream) + @DrawableRes + private fun getIconRes( + stream: Int, + level: Int, + levelMin: Int, + levelMax: Int, + isMuted: Boolean, + isRoutedToBluetooth: Boolean, + ringerMode: RingerMode?, + ): Int { + val isStreamOffline = level == 0 || isMuted + when (ringerMode?.value) { + AudioManager.RINGER_MODE_VIBRATE -> return R.drawable.ic_volume_ringer_vibrate + AudioManager.RINGER_MODE_SILENT -> return R.drawable.ic_ring_volume_off + } + if (isRoutedToBluetooth) { + return if (stream == AudioManager.STREAM_VOICE_CALL) { + R.drawable.ic_volume_bt_sco } else { - if (level < (levelMax + levelMin) / 2) { - // This icon is different on TV - R.drawable.ic_volume_media_low + if (isStreamOffline) { + R.drawable.ic_volume_media_bt_mute } else { - getIconForStream(stream) + R.drawable.ic_volume_media_bt } } } - } - @DrawableRes - private fun getMutedIconForStream(stream: Int): Int? { - return when (stream) { - AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute - AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute - AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute - AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute - else -> null + return if (isStreamOffline) { + when (stream) { + AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute + AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute + AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute + AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute + else -> null + } ?: getIconForStream(stream) + } else { + if (level < (levelMax + levelMin) / 2) { + // This icon is different on TV + R.drawable.ic_volume_media_low + } else { + getIconForStream(stream) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt index 5750c049082f..8df9e788905c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt @@ -16,21 +16,21 @@ package com.android.systemui.volume.dialog.sliders.ui.viewmodel -import androidx.annotation.DrawableRes +import android.graphics.drawable.Drawable import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel data class VolumeDialogSliderStateModel( val minValue: Float, val maxValue: Float, val value: Float, - @DrawableRes val iconRes: Int, + val icon: Drawable, ) -fun VolumeDialogStreamModel.toStateModel(@DrawableRes iconRes: Int): VolumeDialogSliderStateModel { +fun VolumeDialogStreamModel.toStateModel(icon: Drawable): VolumeDialogSliderStateModel { return VolumeDialogSliderStateModel( minValue = levelMin.toFloat(), value = level.toFloat(), maxValue = levelMax.toFloat(), - iconRes = iconRes, + icon = icon, ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt index 6d8457be1014..d999910675b0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt @@ -66,12 +66,14 @@ constructor( private val model: Flow<VolumeDialogStreamModel> = interactor.slider .filter { - val lastVolumeUpdateTime = userVolumeUpdates.value?.timestampMillis ?: 0 + val currentVolumeUpdate = userVolumeUpdates.value ?: return@filter true + val lastVolumeUpdateTime = currentVolumeUpdate.timestampMillis getTimestampMillis() - lastVolumeUpdateTime > VOLUME_UPDATE_GRACE_PERIOD } .stateIn(coroutineScope, SharingStarted.Eagerly, null) .filterNotNull() + val isDisabledByZenMode: Flow<Boolean> = interactor.isDisabledByZenMode val state: Flow<VolumeDialogSliderStateModel> = model .flatMapLatest { streamModel -> @@ -81,7 +83,7 @@ constructor( level = level, levelMin = levelMin, levelMax = levelMax, - isMuted = muted, + isMuted = muteSupported && muted, isRoutedToBluetooth = routedToBluetooth, ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/VolumeDialogResources.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/VolumeDialogResources.kt deleted file mode 100644 index e5cf62b91677..000000000000 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/VolumeDialogResources.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.volume.dialog.ui - -import android.content.Context -import android.content.res.Resources -import com.android.systemui.dagger.qualifiers.UiBackground -import com.android.systemui.res.R -import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.statusbar.policy.onConfigChanged -import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog -import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope -import javax.inject.Inject -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.stateIn - -/** - * Provides cached resources [Flow]s that update when the configuration changes. - * - * Consume or use [kotlinx.coroutines.flow.first] to get the value. - */ -@VolumeDialogScope -class VolumeDialogResources -@Inject -constructor( - @VolumeDialog private val coroutineScope: CoroutineScope, - @UiBackground private val uiBackgroundContext: CoroutineContext, - private val context: Context, - private val configurationController: ConfigurationController, -) { - - val dialogShowDurationMillis: Flow<Long> = configurationResource { - getInteger(R.integer.config_dialogShowAnimationDurationMs).toLong() - } - - val dialogHideDurationMillis: Flow<Long> = configurationResource { - getInteger(R.integer.config_dialogHideAnimationDurationMs).toLong() - } - - private fun <T> configurationResource(get: Resources.() -> T): Flow<T> = - configurationController.onConfigChanged - .map { context.resources.get() } - .onStart { emit(context.resources.get()) } - .flowOn(uiBackgroundContext) - .stateIn(coroutineScope, SharingStarted.Eagerly, null) - .filterNotNull() -} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt index a3166a9978f4..46d7d5f680ce 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt @@ -17,23 +17,28 @@ package com.android.systemui.volume.dialog.ui.binder import android.app.Dialog +import android.content.res.Resources import android.graphics.Rect import android.graphics.Region import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.view.ViewTreeObserver.InternalInsetsInfo +import android.view.WindowInsets import androidx.constraintlayout.motion.widget.MotionLayout +import androidx.core.view.updatePadding import com.android.internal.view.RotationPolicy +import com.android.systemui.common.ui.view.onApplyWindowInsets +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.util.children +import com.android.systemui.util.kotlin.awaitCancellationThenDispose import com.android.systemui.volume.SystemUIInterpolators import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder import com.android.systemui.volume.dialog.settings.ui.binder.VolumeDialogSettingsButtonViewBinder import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSlidersViewBinder -import com.android.systemui.volume.dialog.ui.VolumeDialogResources import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory import com.android.systemui.volume.dialog.ui.utils.suspendAnimate import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel @@ -42,7 +47,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach @@ -56,7 +61,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine class VolumeDialogViewBinder @Inject constructor( - private val volumeResources: VolumeDialogResources, + @Main resources: Resources, private val viewModel: VolumeDialogViewModel, private val jankListenerFactory: JankListenerFactory, private val tracer: VolumeTracer, @@ -65,7 +70,14 @@ constructor( private val settingsButtonViewBinder: VolumeDialogSettingsButtonViewBinder, ) { + private val dialogShowAnimationDurationMs = + resources.getInteger(R.integer.config_dialogShowAnimationDurationMs).toLong() + private val dialogHideAnimationDurationMs = + resources.getInteger(R.integer.config_dialogHideAnimationDurationMs).toLong() + fun CoroutineScope.bind(dialog: Dialog) { + val insets: MutableStateFlow<WindowInsets> = + MutableStateFlow(WindowInsets.Builder().build()) // Root view of the Volume Dialog. val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root) root.alpha = 0f @@ -83,6 +95,22 @@ constructor( launch { root.viewTreeObserver.computeInternalInsetsListener(root) } + launch { + root + .onApplyWindowInsets { v, newInsets -> + val insetsValues = newInsets.getInsets(WindowInsets.Type.displayCutout()) + v.updatePadding( + left = insetsValues.left, + top = insetsValues.top, + right = insetsValues.right, + bottom = insetsValues.bottom, + ) + insets.value = newInsets + WindowInsets.CONSUMED + } + .awaitCancellationThenDispose() + } + with(volumeDialogRingerViewBinder) { bind(root) } with(slidersViewBinder) { bind(root) } with(settingsButtonViewBinder) { bind(root) } @@ -98,13 +126,15 @@ constructor( when (it) { is VolumeDialogVisibilityModel.Visible -> { tracer.traceVisibilityEnd(it) - calculateTranslationX(view)?.let(view::setTranslationX) - view.animateShow(volumeResources.dialogShowDurationMillis.first()) + view.animateShow( + duration = dialogShowAnimationDurationMs, + translationX = calculateTranslationX(view), + ) } is VolumeDialogVisibilityModel.Dismissed -> { tracer.traceVisibilityEnd(it) view.animateHide( - duration = volumeResources.dialogHideDurationMillis.first(), + duration = dialogHideAnimationDurationMs, translationX = calculateTranslationX(view), ) dialog.dismiss() @@ -129,24 +159,15 @@ constructor( } } - private suspend fun View.animateShow(duration: Long) { + private suspend fun View.animateShow(duration: Long, translationX: Float?) { + translationX?.let { setTranslationX(translationX) } + alpha = 0f animate() .alpha(1f) .translationX(0f) .setDuration(duration) .setInterpolator(SystemUIInterpolators.LogDecelerateInterpolator()) .suspendAnimate(jankListenerFactory.show(this, duration)) - /* TODO(b/369993851) - .withEndAction(Runnable { - if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) { - if (mRingerIcon != null) { - mRingerIcon.postOnAnimationDelayed( - getSinglePressFor(mRingerIcon), 1500 - ) - } - } - }) - */ } private suspend fun View.animateHide(duration: Long, translationX: Float?) { @@ -155,22 +176,7 @@ constructor( .alpha(0f) .setDuration(duration) .setInterpolator(SystemUIInterpolators.LogAccelerateInterpolator()) - /* TODO(b/369993851) - .withEndAction( - Runnable { - mHandler.postDelayed( - Runnable { - hideRingerDrawer() - - }, - 50 - ) - } - ) - */ - if (translationX != null) { - animator.translationX(translationX) - } + translationX?.let { animator.translationX(it) } animator.suspendAnimate(jankListenerFactory.dismiss(this, duration)) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt index b20dffb8ac33..7a6ede4c8b9c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt @@ -44,9 +44,9 @@ class VolumeDialogViewModel @Inject constructor( private val context: Context, - private val dialogVisibilityInteractor: VolumeDialogVisibilityInteractor, + dialogVisibilityInteractor: VolumeDialogVisibilityInteractor, volumeDialogSlidersInteractor: VolumeDialogSlidersInteractor, - private val volumeDialogStateInteractor: VolumeDialogStateInteractor, + volumeDialogStateInteractor: VolumeDialogStateInteractor, devicePostureController: DevicePostureController, configurationController: ConfigurationController, ) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index cec3d1eb86f0..5b8d9b045475 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -18,9 +18,6 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.Context import android.media.AudioManager -import android.media.AudioManager.STREAM_ALARM -import android.media.AudioManager.STREAM_MUSIC -import android.media.AudioManager.STREAM_NOTIFICATION import android.util.Log import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.logging.UiEventLogger @@ -34,8 +31,6 @@ import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.modes.shared.ModesUiIcons import com.android.systemui.res.R import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor -import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes -import com.android.systemui.util.kotlin.combine import com.android.systemui.volume.panel.shared.VolumePanelLogger import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import dagger.assisted.Assisted @@ -43,12 +38,15 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -101,48 +99,16 @@ constructor( ) override val slider: StateFlow<SliderState> = - if (ModesUiIcons.isEnabled) { - combine( - audioVolumeInteractor.getAudioStream(audioStream), - audioVolumeInteractor.canChangeVolume(audioStream), - audioVolumeInteractor.ringerMode, - zenModeInteractor.activeModesBlockingEverything, - zenModeInteractor.activeModesBlockingAlarms, - zenModeInteractor.activeModesBlockingMedia, - ) { - model, - isEnabled, - ringerMode, - modesBlockingEverything, - modesBlockingAlarms, - modesBlockingMedia -> - volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume) - model.toState( - isEnabled, - ringerMode, - getStreamDisabledMessage( - modesBlockingEverything, - modesBlockingAlarms, - modesBlockingMedia, - ), - ) - } - .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) - } else { - combine( - audioVolumeInteractor.getAudioStream(audioStream), - audioVolumeInteractor.canChangeVolume(audioStream), - audioVolumeInteractor.ringerMode, - ) { model, isEnabled, ringerMode -> - volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume) - model.toState( - isEnabled, - ringerMode, - getStreamDisabledMessageWithoutModes(audioStream), - ) - } - .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) - } + combine( + audioVolumeInteractor.getAudioStream(audioStream), + audioVolumeInteractor.canChangeVolume(audioStream), + audioVolumeInteractor.ringerMode, + streamDisabledMessage(), + ) { model, isEnabled, ringerMode, streamDisabledMessage -> + volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume) + model.toState(isEnabled, ringerMode, streamDisabledMessage) + } + .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) init { volumeChanges @@ -229,40 +195,32 @@ constructor( ) } - private fun getStreamDisabledMessage( - blockingEverything: ActiveZenModes, - blockingAlarms: ActiveZenModes, - blockingMedia: ActiveZenModes, - ): String { - // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING. - // In fact, VOICE_CALL should not be affected by interruption filtering at all. - return if (audioStream.value == STREAM_NOTIFICATION) { - context.getString(R.string.stream_notification_unavailable) - } else { - val blockingModeName = - when { - blockingEverything.mainMode != null -> blockingEverything.mainMode.name - audioStream.value == STREAM_ALARM -> blockingAlarms.mainMode?.name - audioStream.value == STREAM_MUSIC -> blockingMedia.mainMode?.name - else -> null - } - - if (blockingModeName != null) { - context.getString(R.string.stream_unavailable_by_modes, blockingModeName) + // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING. + // In fact, VOICE_CALL should not be affected by interruption filtering at all. + private fun streamDisabledMessage(): Flow<String> { + return if (ModesUiIcons.isEnabled) { + if (audioStream.value == AudioManager.STREAM_NOTIFICATION) { + flowOf(context.getString(R.string.stream_notification_unavailable)) } else { - // Should not actually be visible, but as a catch-all. - context.getString(R.string.stream_unavailable_by_unknown) + if (zenModeInteractor.canBeBlockedByZenMode(audioStream)) { + zenModeInteractor.activeModesBlockingStream(audioStream).map { blockingZenModes + -> + blockingZenModes.mainMode?.name?.let { + context.getString(R.string.stream_unavailable_by_modes, it) + } ?: context.getString(R.string.stream_unavailable_by_unknown) + } + } else { + flowOf(context.getString(R.string.stream_unavailable_by_unknown)) + } } - } - } - - private fun getStreamDisabledMessageWithoutModes(audioStream: AudioStream): String { - // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING. - // In fact, VOICE_CALL should not be affected by interruption filtering at all. - return if (audioStream.value == STREAM_NOTIFICATION) { - context.getString(R.string.stream_notification_unavailable) } else { - context.getString(R.string.stream_alarm_unavailable) + flowOf( + if (audioStream.value == AudioManager.STREAM_NOTIFICATION) { + context.getString(R.string.stream_notification_unavailable) + } else { + context.getString(R.string.stream_alarm_unavailable) + } + ) } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 4abbbacd800b..bac2c47f51c7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -24,13 +24,15 @@ import android.view.ViewTreeObserver import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND_INACTIVE +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher 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.domain.interactor.KeyguardInteractorFactory +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING @@ -97,8 +99,9 @@ import org.mockito.kotlin.clearInvocations class ClockEventControllerTest : SysuiTestCase() { private val kosmos = testKosmos() - private val zenModeRepository = kosmos.fakeZenModeRepository private val testScope = kosmos.testScope + private val zenModeRepository by lazy { kosmos.fakeZenModeRepository } + private val zenModeInteractor by lazy { kosmos.zenModeInteractor } @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -106,7 +109,6 @@ class ClockEventControllerTest : SysuiTestCase() { private lateinit var repository: FakeKeyguardRepository private val clockBuffers = ClockMessageBuffers(LogcatOnlyMessageBuffer(LogLevel.DEBUG)) private lateinit var underTest: ClockEventController - private lateinit var dndModeId: String @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var batteryController: BatteryController @@ -156,17 +158,12 @@ class ClockEventControllerTest : SysuiTestCase() { whenever(largeClockController.theme).thenReturn(ThemeConfig(true, null)) whenever(userTracker.userId).thenReturn(1) - dndModeId = MANUAL_DND_INACTIVE.id - zenModeRepository.addMode(MANUAL_DND_INACTIVE) + repository = kosmos.fakeKeyguardRepository - repository = FakeKeyguardRepository() - - val withDeps = KeyguardInteractorFactory.create(repository = repository) - - withDeps.featureFlags.apply { set(Flags.REGION_SAMPLING, false) } + kosmos.fakeFeatureFlagsClassic.set(Flags.REGION_SAMPLING, false) underTest = ClockEventController( - withDeps.keyguardInteractor, + kosmos.keyguardInteractor, keyguardTransitionInteractor, broadcastDispatcher, batteryController, @@ -177,9 +174,9 @@ class ClockEventControllerTest : SysuiTestCase() { mainExecutor, bgExecutor, clockBuffers, - withDeps.featureFlags, + kosmos.fakeFeatureFlagsClassic, zenModeController, - kosmos.zenModeInteractor, + zenModeInteractor, userTracker, ) underTest.clock = clock @@ -504,7 +501,7 @@ class ClockEventControllerTest : SysuiTestCase() { runCurrent() clearInvocations(events) - zenModeRepository.activateMode(dndModeId) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() verify(events) @@ -512,7 +509,7 @@ class ClockEventControllerTest : SysuiTestCase() { eq(ZenData(ZenMode.IMPORTANT_INTERRUPTIONS, R.string::dnd_is_on.name)) ) - zenModeRepository.deactivateMode(dndModeId) + zenModeRepository.deactivateMode(MANUAL_DND) runCurrent() verify(events).onZenDataChanged(eq(ZenData(ZenMode.OFF, R.string::dnd_is_off.name))) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt index 5d622eaeb1aa..e61acc4e1d0b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -224,6 +225,30 @@ class AudioSharingDeviceItemActionInteractorTest : SysuiTestCase() { } } + @Test + fun testOnActionIconClick_audioSharingMediaDevice_stopBroadcast() { + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + actionInteractorImpl.onActionIconClick(inAudioSharingMediaDeviceItem) {} + assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted) + .isEqualTo(false) + } + } + } + + @Test + fun testOnActionIconClick_availableAudioSharingMediaDevice_startBroadcast() { + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + actionInteractorImpl.onActionIconClick(connectedAudioSharingMediaDeviceItem) {} + assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted) + .isEqualTo(true) + } + } + } + private companion object { const val DEVICE_NAME = "device" const val DEVICE_CONNECTION_SUMMARY = "active" diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt index 6bfd08025833..4396b0a42ae6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt @@ -32,6 +32,9 @@ import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.model.SysUiState import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeDialogContextInteractor @@ -43,9 +46,8 @@ 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.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -93,7 +95,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { private val fakeSystemClock = FakeSystemClock() - private lateinit var scheduler: TestCoroutineScheduler private lateinit var dispatcher: CoroutineDispatcher private lateinit var testScope: TestScope private lateinit var icon: Pair<Drawable, String> @@ -104,9 +105,8 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { @Before fun setUp() { - scheduler = TestCoroutineScheduler() - dispatcher = UnconfinedTestDispatcher(scheduler) - testScope = TestScope(dispatcher) + dispatcher = kosmos.testDispatcher + testScope = kosmos.testScope whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState) @@ -124,23 +124,19 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { kosmos.shadeDialogContextInteractor, ) - whenever( - sysuiDialogFactory.create( - any(SystemUIDialog.Delegate::class.java), - any() - ) - ).thenAnswer { - SystemUIDialog( - mContext, - 0, - SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK, - dialogManager, - sysuiState, - fakeBroadcastDispatcher, - dialogTransitionAnimator, - it.getArgument(0), - ) - } + whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java), any())) + .thenAnswer { + SystemUIDialog( + mContext, + 0, + SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK, + dialogManager, + sysuiState, + fakeBroadcastDispatcher, + dialogTransitionAnimator, + it.getArgument(0), + ) + } icon = Pair(drawable, DEVICE_NAME) deviceItem = @@ -194,20 +190,29 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { @Test fun testDeviceItemViewHolder_cachedDeviceNotBusy() { - deviceItem.isEnabled = true - - val view = - LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) - val viewHolder = - mBluetoothTileDialogDelegate - .Adapter(bluetoothTileDialogCallback) - .DeviceItemViewHolder(view) - viewHolder.bind(deviceItem, bluetoothTileDialogCallback) - val container = view.requireViewById<View>(R.id.bluetooth_device_row) - - assertThat(container).isNotNull() - assertThat(container.isEnabled).isTrue() - assertThat(container.hasOnClickListeners()).isTrue() + testScope.runTest { + deviceItem.isEnabled = true + + val view = + LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) + val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view) + viewHolder.bind(deviceItem) + val container = view.requireViewById<View>(R.id.bluetooth_device_row) + + assertThat(container).isNotNull() + assertThat(container.isEnabled).isTrue() + assertThat(container.hasOnClickListeners()).isTrue() + val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick) + runCurrent() + container.performClick() + runCurrent() + assertThat(value).isNotNull() + value?.let { + assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW) + assertThat(it.clickedView).isEqualTo(container) + assertThat(it.deviceItem).isEqualTo(deviceItem) + } + } } @Test @@ -229,9 +234,9 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { sysuiDialogFactory, kosmos.shadeDialogContextInteractor, ) - .Adapter(bluetoothTileDialogCallback) + .Adapter() .DeviceItemViewHolder(view) - viewHolder.bind(deviceItem, bluetoothTileDialogCallback) + viewHolder.bind(deviceItem) val container = view.requireViewById<View>(R.id.bluetooth_device_row) assertThat(container).isNotNull() @@ -240,6 +245,32 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { } @Test + fun testDeviceItemViewHolder_clickActionIcon() { + testScope.runTest { + deviceItem.isEnabled = true + + val view = + LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) + val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view) + viewHolder.bind(deviceItem) + val actionIconView = view.requireViewById<View>(R.id.gear_icon) + + assertThat(actionIconView).isNotNull() + assertThat(actionIconView.hasOnClickListeners()).isTrue() + val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick) + runCurrent() + actionIconView.performClick() + runCurrent() + assertThat(value).isNotNull() + value?.let { + assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON) + assertThat(it.clickedView).isEqualTo(actionIconView) + assertThat(it.deviceItem).isEqualTo(deviceItem) + } + } + } + + @Test fun testOnDeviceUpdated_hideSeeAll_showPairNew() { testScope.runTest { val dialog = mBluetoothTileDialogDelegate.createDialog() diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt index 5bf15137b834..0aa5199cb20e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt @@ -118,6 +118,7 @@ class DeviceItemFactoryTest : SysuiTestCase() { .isEqualTo(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE) assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice) assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME) + assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_add) assertThat(deviceItem.isActive).isFalse() assertThat(deviceItem.connectionSummary) .isEqualTo( @@ -292,6 +293,7 @@ class DeviceItemFactoryTest : SysuiTestCase() { assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice) assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME) assertThat(deviceItem.connectionSummary).isEqualTo(CONNECTION_SUMMARY) + assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_settings_24dp) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt index 387cc084f9cd..1320223cabf3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt @@ -33,7 +33,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.ui.BouncerDialogFactory -import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModelFactory import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt index 7a3089f33276..77c40a1e8eef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.data.repository +import android.animation.AnimationHandler import android.animation.Animator import android.animation.ValueAnimator import android.platform.test.annotations.EnableFlags @@ -36,6 +37,7 @@ import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.util.FrameCallbackProvider import com.android.systemui.keyguard.util.KeyguardTransitionRunner import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope @@ -52,9 +54,12 @@ import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -70,13 +75,29 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { private lateinit var underTest: KeyguardTransitionRepository private lateinit var runner: KeyguardTransitionRunner + private lateinit var callbackProvider: FrameCallbackProvider private val animatorListener = mock<Animator.AnimatorListener>() @Before fun setUp() { underTest = KeyguardTransitionRepositoryImpl(Dispatchers.Main) - runner = KeyguardTransitionRunner(underTest) + runBlocking { + callbackProvider = FrameCallbackProvider(testScope.backgroundScope) + withContext(Dispatchers.Main) { + // AnimationHandler uses ThreadLocal storage, and ValueAnimators MUST start from + // main thread + AnimationHandler.getInstance().setProvider(callbackProvider) + } + runner = KeyguardTransitionRunner(callbackProvider.frames, underTest) + } + } + + @After + fun tearDown() { + runBlocking { + withContext(Dispatchers.Main) { AnimationHandler.getInstance().setProvider(null) } + } } @Test @@ -84,13 +105,11 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { testScope.runTest { val steps = mutableListOf<TransitionStep>() val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) - runner.startTransition( this, TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()), maxFrames = 100, ) - assertSteps(steps, listWithStep(BigDecimal(.1)), AOD, LOCKSCREEN) job.cancel() } @@ -119,12 +138,12 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { ), ) - val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.1)) - assertSteps(steps.subList(0, 4), firstTransitionSteps, AOD, LOCKSCREEN) + val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.2)) + assertSteps(steps.subList(0, 5), firstTransitionSteps, AOD, LOCKSCREEN) - // Second transition starts from .1 (LAST_VALUE) - val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.1)) - assertSteps(steps.subList(4, steps.size), secondTransitionSteps, LOCKSCREEN, AOD) + // Second transition starts from .2 (LAST_VALUE) + val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.2)) + assertSteps(steps.subList(5, steps.size), secondTransitionSteps, LOCKSCREEN, AOD) job.cancel() job2.cancel() @@ -154,12 +173,12 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { ), ) - val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.1)) - assertSteps(steps.subList(0, 4), firstTransitionSteps, AOD, LOCKSCREEN) + val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.2)) + assertSteps(steps.subList(0, 5), firstTransitionSteps, AOD, LOCKSCREEN) // Second transition starts from 0 (RESET) val secondTransitionSteps = listWithStep(start = BigDecimal(0), step = BigDecimal(.1)) - assertSteps(steps.subList(4, steps.size), secondTransitionSteps, LOCKSCREEN, AOD) + assertSteps(steps.subList(5, steps.size), secondTransitionSteps, LOCKSCREEN, AOD) job.cancel() job2.cancel() @@ -173,7 +192,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { runner.startTransition( this, TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()), - maxFrames = 3, + maxFrames = 2, ) // Now start 2nd transition, which will interrupt the first diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt new file mode 100644 index 000000000000..a192446e535b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt @@ -0,0 +1,819 @@ +/* + * 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.qs.tiles.dialog + +import android.content.Intent +import android.os.Handler +import android.os.fakeExecutorHandler +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.telephony.telephonyManager +import android.testing.TestableLooper.RunWithLooper +import android.view.LayoutInflater +import android.view.View +import android.widget.Button +import android.widget.LinearLayout +import android.widget.Switch +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import androidx.test.annotation.UiThreadTest +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.internal.logging.UiEventLogger +import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.flags.setFlagValue +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.android.wifitrackerlib.WifiEntry +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.MockitoSession +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +@RunWithLooper(setAsMainLooper = true) +@UiThreadTest +class InternetDetailsContentManagerTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val handler: Handler = kosmos.fakeExecutorHandler + private val scope: CoroutineScope = mock<CoroutineScope>() + private val telephonyManager: TelephonyManager = kosmos.telephonyManager + private val internetWifiEntry: WifiEntry = mock<WifiEntry>() + private val wifiEntries: List<WifiEntry> = mock<List<WifiEntry>>() + private val internetAdapter = mock<InternetAdapter>() + private val internetDetailsContentController: InternetDetailsContentController = + mock<InternetDetailsContentController>() + private val keyguard: KeyguardStateController = mock<KeyguardStateController>() + private val dialogTransitionAnimator: DialogTransitionAnimator = + mock<DialogTransitionAnimator>() + private val bgExecutor = FakeExecutor(FakeSystemClock()) + private lateinit var internetDetailsContentManager: InternetDetailsContentManager + private var subTitle: View? = null + private var ethernet: LinearLayout? = null + private var mobileDataLayout: LinearLayout? = null + private var mobileToggleSwitch: Switch? = null + private var wifiToggle: LinearLayout? = null + private var wifiToggleSwitch: Switch? = null + private var wifiToggleSummary: TextView? = null + private var connectedWifi: LinearLayout? = null + private var wifiList: RecyclerView? = null + private var seeAll: LinearLayout? = null + private var wifiScanNotify: LinearLayout? = null + private var airplaneModeSummaryText: TextView? = null + private var mockitoSession: MockitoSession? = null + private var sharedWifiButton: Button? = null + private lateinit var contentView: View + + @Before + fun setUp() { + // TODO: b/377388104 enable this flag after integrating with details view. + mSetFlagsRule.setFlagValue(Flags.FLAG_QS_TILE_DETAILED_VIEW, false) + whenever(telephonyManager.createForSubscriptionId(ArgumentMatchers.anyInt())) + .thenReturn(telephonyManager) + whenever(internetWifiEntry.title).thenReturn(WIFI_TITLE) + whenever(internetWifiEntry.getSummary(false)).thenReturn(WIFI_SUMMARY) + whenever(internetWifiEntry.isDefaultNetwork).thenReturn(true) + whenever(internetWifiEntry.hasInternetAccess()).thenReturn(true) + whenever(wifiEntries.size).thenReturn(1) + whenever(internetDetailsContentController.getDialogTitleText()).thenReturn(TITLE) + whenever(internetDetailsContentController.getMobileNetworkTitle(ArgumentMatchers.anyInt())) + .thenReturn(MOBILE_NETWORK_TITLE) + whenever( + internetDetailsContentController.getMobileNetworkSummary(ArgumentMatchers.anyInt()) + ) + .thenReturn(MOBILE_NETWORK_SUMMARY) + whenever(internetDetailsContentController.isWifiEnabled).thenReturn(true) + whenever(internetDetailsContentController.activeAutoSwitchNonDdsSubId) + .thenReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + mockitoSession = + ExtendedMockito.mockitoSession() + .spyStatic(WifiEnterpriseRestrictionUtils::class.java) + .startMocking() + whenever(WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(mContext)).thenReturn(true) + createView() + } + + private fun createView() { + contentView = + LayoutInflater.from(mContext).inflate(R.layout.internet_connectivity_dialog, null) + internetDetailsContentManager = + InternetDetailsContentManager( + internetDetailsContentController, + canConfigMobileData = true, + canConfigWifi = true, + coroutineScope = scope, + context = mContext, + internetDialog = null, + uiEventLogger = mock<UiEventLogger>(), + dialogTransitionAnimator = dialogTransitionAnimator, + handler = handler, + backgroundExecutor = bgExecutor, + keyguard = keyguard, + ) + + internetDetailsContentManager.bind(contentView) + internetDetailsContentManager.adapter = internetAdapter + internetDetailsContentManager.connectedWifiEntry = internetWifiEntry + internetDetailsContentManager.wifiEntriesCount = wifiEntries.size + + subTitle = contentView.requireViewById(R.id.internet_dialog_subtitle) + ethernet = contentView.requireViewById(R.id.ethernet_layout) + mobileDataLayout = contentView.requireViewById(R.id.mobile_network_layout) + mobileToggleSwitch = contentView.requireViewById(R.id.mobile_toggle) + wifiToggle = contentView.requireViewById(R.id.turn_on_wifi_layout) + wifiToggleSwitch = contentView.requireViewById(R.id.wifi_toggle) + wifiToggleSummary = contentView.requireViewById(R.id.wifi_toggle_summary) + connectedWifi = contentView.requireViewById(R.id.wifi_connected_layout) + wifiList = contentView.requireViewById(R.id.wifi_list_layout) + seeAll = contentView.requireViewById(R.id.see_all_layout) + wifiScanNotify = contentView.requireViewById(R.id.wifi_scan_notify_layout) + airplaneModeSummaryText = contentView.requireViewById(R.id.airplane_mode_summary) + sharedWifiButton = contentView.requireViewById(R.id.share_wifi_button) + } + + @After + fun tearDown() { + internetDetailsContentManager.unBind() + mockitoSession!!.finishMocking() + } + + @Test + fun createView_setAccessibilityPaneTitleToQuickSettings() { + assertThat(contentView.accessibilityPaneTitle) + .isEqualTo(mContext.getText(R.string.accessibility_desc_quick_settings)) + } + + @Test + fun hideWifiViews_WifiViewsGone() { + internetDetailsContentManager.hideWifiViews() + + assertThat(internetDetailsContentManager.isProgressBarVisible).isFalse() + assertThat(wifiToggle!!.visibility).isEqualTo(View.GONE) + assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE) + assertThat(wifiList!!.visibility).isEqualTo(View.GONE) + assertThat(seeAll!!.visibility).isEqualTo(View.GONE) + } + + @Test + fun updateContent_withApmOn_internetDialogSubTitleGone() { + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(subTitle!!.visibility).isEqualTo(View.VISIBLE) + } + } + + @Test + fun updateContent_withApmOff_internetDialogSubTitleVisible() { + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(subTitle!!.visibility).isEqualTo(View.VISIBLE) + } + } + + @Test + fun updateContent_apmOffAndHasEthernet_showEthernet() { + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false) + whenever(internetDetailsContentController.hasEthernet()).thenReturn(true) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(ethernet!!.visibility).isEqualTo(View.VISIBLE) + } + } + + @Test + fun updateContent_apmOffAndNoEthernet_hideEthernet() { + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false) + whenever(internetDetailsContentController.hasEthernet()).thenReturn(false) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(ethernet!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_apmOnAndHasEthernet_showEthernet() { + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true) + whenever(internetDetailsContentController.hasEthernet()).thenReturn(true) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(ethernet!!.visibility).isEqualTo(View.VISIBLE) + } + } + + @Test + fun updateContent_apmOnAndNoEthernet_hideEthernet() { + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true) + whenever(internetDetailsContentController.hasEthernet()).thenReturn(false) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(ethernet!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_apmOffAndNotCarrierNetwork_mobileDataLayoutGone() { + // Mobile network should be gone if the list of active subscriptionId is null. + whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(false) + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false) + whenever(internetDetailsContentController.hasActiveSubIdOnDds()).thenReturn(false) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(mobileDataLayout!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_apmOnWithCarrierNetworkAndWifiStatus_mobileDataLayoutVisible() { + // Carrier network should be visible if airplane mode ON and Wi-Fi is ON. + whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true) + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true) + whenever(internetDetailsContentController.isWifiEnabled).thenReturn(true) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(mobileDataLayout!!.visibility).isEqualTo(View.VISIBLE) + } + } + + @Test + fun updateContent_apmOnWithCarrierNetworkAndWifiStatus_mobileDataLayoutGone() { + // Carrier network should be gone if airplane mode ON and Wi-Fi is off. + whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true) + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true) + whenever(internetDetailsContentController.isWifiEnabled).thenReturn(false) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(mobileDataLayout!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_apmOnAndNoCarrierNetwork_mobileDataLayoutGone() { + whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(false) + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(mobileDataLayout!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_apmOnAndWifiOnHasCarrierNetwork_showAirplaneSummary() { + whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true) + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true) + internetDetailsContentManager.connectedWifiEntry = null + whenever(internetDetailsContentController.activeNetworkIsCellular()).thenReturn(false) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(mobileDataLayout!!.visibility).isEqualTo(View.VISIBLE) + assertThat(airplaneModeSummaryText!!.visibility).isEqualTo(View.VISIBLE) + } + } + + @Test + fun updateContent_apmOffAndWifiOnHasCarrierNetwork_notShowApmSummary() { + whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true) + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false) + internetDetailsContentManager.connectedWifiEntry = null + whenever(internetDetailsContentController.activeNetworkIsCellular()).thenReturn(false) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(airplaneModeSummaryText!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_apmOffAndHasCarrierNetwork_notShowApmSummary() { + whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true) + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(airplaneModeSummaryText!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_apmOnAndNoCarrierNetwork_notShowApmSummary() { + whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(false) + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(airplaneModeSummaryText!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_mobileDataIsEnabled_checkMobileDataSwitch() { + whenever(internetDetailsContentController.hasActiveSubIdOnDds()).thenReturn(true) + whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true) + whenever(internetDetailsContentController.isMobileDataEnabled).thenReturn(true) + mobileToggleSwitch!!.isChecked = false + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(mobileToggleSwitch!!.isChecked).isTrue() + } + } + + @Test + fun updateContent_mobileDataIsNotChanged_checkMobileDataSwitch() { + whenever(internetDetailsContentController.hasActiveSubIdOnDds()).thenReturn(true) + whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true) + whenever(internetDetailsContentController.isMobileDataEnabled).thenReturn(false) + mobileToggleSwitch!!.isChecked = false + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(mobileToggleSwitch!!.isChecked).isFalse() + } + } + + @Test + fun updateContent_wifiOnAndHasInternetWifi_showConnectedWifi() { + whenever(internetDetailsContentController.activeAutoSwitchNonDdsSubId).thenReturn(1) + whenever(internetDetailsContentController.hasActiveSubIdOnDds()).thenReturn(true) + + // The preconditions WiFi ON and Internet WiFi are already in setUp() + whenever(internetDetailsContentController.activeNetworkIsCellular()).thenReturn(false) + + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(connectedWifi!!.visibility).isEqualTo(View.VISIBLE) + val secondaryLayout = + contentView.requireViewById<LinearLayout>(R.id.secondary_mobile_network_layout) + assertThat(secondaryLayout.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_wifiOnAndNoConnectedWifi_hideConnectedWifi() { + // The precondition WiFi ON is already in setUp() + internetDetailsContentManager.connectedWifiEntry = null + whenever(internetDetailsContentController.activeNetworkIsCellular()).thenReturn(false) + + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_wifiOnAndNoWifiEntry_showWifiListAndSeeAllArea() { + // The precondition WiFi ON is already in setUp() + internetDetailsContentManager.connectedWifiEntry = null + internetDetailsContentManager.wifiEntriesCount = 0 + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE) + // Show a blank block to fix the details content height even if there is no WiFi list + assertThat(wifiList!!.visibility).isEqualTo(View.VISIBLE) + verify(internetAdapter).setMaxEntriesCount(3) + assertThat(seeAll!!.visibility).isEqualTo(View.INVISIBLE) + } + } + + @Test + fun updateContent_wifiOnAndOneWifiEntry_showWifiListAndSeeAllArea() { + // The precondition WiFi ON is already in setUp() + internetDetailsContentManager.connectedWifiEntry = null + internetDetailsContentManager.wifiEntriesCount = 1 + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE) + // Show a blank block to fix the details content height even if there is no WiFi list + assertThat(wifiList!!.visibility).isEqualTo(View.VISIBLE) + verify(internetAdapter).setMaxEntriesCount(3) + assertThat(seeAll!!.visibility).isEqualTo(View.INVISIBLE) + } + } + + @Test + fun updateContent_wifiOnAndHasConnectedWifi_showAllWifiAndSeeAllArea() { + // The preconditions WiFi ON and WiFi entries are already in setUp() + internetDetailsContentManager.wifiEntriesCount = 0 + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(connectedWifi!!.visibility).isEqualTo(View.VISIBLE) + // Show a blank block to fix the details content height even if there is no WiFi list + assertThat(wifiList!!.visibility).isEqualTo(View.VISIBLE) + verify(internetAdapter).setMaxEntriesCount(2) + assertThat(seeAll!!.visibility).isEqualTo(View.INVISIBLE) + } + } + + @Test + fun updateContent_wifiOnAndHasMaxWifiList_showWifiListAndSeeAll() { + // The preconditions WiFi ON and WiFi entries are already in setUp() + internetDetailsContentManager.connectedWifiEntry = null + internetDetailsContentManager.wifiEntriesCount = + InternetDetailsContentController.MAX_WIFI_ENTRY_COUNT + internetDetailsContentManager.hasMoreWifiEntries = true + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE) + assertThat(wifiList!!.visibility).isEqualTo(View.VISIBLE) + verify(internetAdapter).setMaxEntriesCount(3) + assertThat(seeAll!!.visibility).isEqualTo(View.VISIBLE) + } + } + + @Test + fun updateContent_wifiOnAndHasBothWifiEntry_showBothWifiEntryAndSeeAll() { + // The preconditions WiFi ON and WiFi entries are already in setUp() + internetDetailsContentManager.wifiEntriesCount = + InternetDetailsContentController.MAX_WIFI_ENTRY_COUNT - 1 + internetDetailsContentManager.hasMoreWifiEntries = true + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(connectedWifi!!.visibility).isEqualTo(View.VISIBLE) + assertThat(wifiList!!.visibility).isEqualTo(View.VISIBLE) + verify(internetAdapter).setMaxEntriesCount(2) + assertThat(seeAll!!.visibility).isEqualTo(View.VISIBLE) + } + } + + @Test + fun updateContent_deviceLockedAndNoConnectedWifi_showWifiToggle() { + // The preconditions WiFi entries are already in setUp() + whenever(internetDetailsContentController.isDeviceLocked).thenReturn(true) + internetDetailsContentManager.connectedWifiEntry = null + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + // Show WiFi Toggle without background + assertThat(wifiToggle!!.visibility).isEqualTo(View.VISIBLE) + assertThat(wifiToggle!!.background).isNull() + // Hide Wi-Fi networks and See all + assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE) + assertThat(wifiList!!.visibility).isEqualTo(View.GONE) + assertThat(seeAll!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_deviceLockedAndHasConnectedWifi_showWifiToggleWithBackground() { + // The preconditions WiFi ON and WiFi entries are already in setUp() + whenever(internetDetailsContentController.isDeviceLocked).thenReturn(true) + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + // Show WiFi Toggle with highlight background + assertThat(wifiToggle!!.visibility).isEqualTo(View.VISIBLE) + assertThat(wifiToggle!!.background).isNotNull() + // Hide Wi-Fi networks and See all + assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE) + assertThat(wifiList!!.visibility).isEqualTo(View.GONE) + assertThat(seeAll!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_disallowChangeWifiState_disableWifiSwitch() { + whenever(WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(mContext)) + .thenReturn(false) + createView() + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + // Disable Wi-Fi switch and show restriction message in summary. + assertThat(wifiToggleSwitch!!.isEnabled).isFalse() + assertThat(wifiToggleSummary!!.visibility).isEqualTo(View.VISIBLE) + assertThat(wifiToggleSummary!!.text.length).isNotEqualTo(0) + } + } + + @Test + fun updateContent_allowChangeWifiState_enableWifiSwitch() { + whenever(WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(mContext)).thenReturn(true) + createView() + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + // Enable Wi-Fi switch and hide restriction message in summary. + assertThat(wifiToggleSwitch!!.isEnabled).isTrue() + assertThat(wifiToggleSummary!!.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_showSecondaryDataSub() { + whenever(internetDetailsContentController.activeAutoSwitchNonDdsSubId).thenReturn(1) + whenever(internetDetailsContentController.hasActiveSubIdOnDds()).thenReturn(true) + whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false) + + clearInvocations(internetDetailsContentController) + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + val primaryLayout = + contentView.requireViewById<LinearLayout>(R.id.mobile_network_layout) + val secondaryLayout = + contentView.requireViewById<LinearLayout>(R.id.secondary_mobile_network_layout) + + verify(internetDetailsContentController).getMobileNetworkSummary(1) + assertThat(primaryLayout.background).isNotEqualTo(secondaryLayout.background) + } + } + + @Test + fun updateContent_wifiOn_hideWifiScanNotify() { + // The preconditions WiFi ON and WiFi entries are already in setUp() + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE) + } + + assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE) + } + + @Test + fun updateContent_wifiOffAndWifiScanOff_hideWifiScanNotify() { + whenever(internetDetailsContentController.isWifiEnabled).thenReturn(false) + whenever(internetDetailsContentController.isWifiScanEnabled).thenReturn(false) + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE) + } + + assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE) + } + + @Test + fun updateContent_wifiOffAndWifiScanOnAndDeviceLocked_hideWifiScanNotify() { + whenever(internetDetailsContentController.isWifiEnabled).thenReturn(false) + whenever(internetDetailsContentController.isWifiScanEnabled).thenReturn(true) + whenever(internetDetailsContentController.isDeviceLocked).thenReturn(true) + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE) + } + + assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE) + } + + @Test + fun updateContent_wifiOffAndWifiScanOnAndDeviceUnlocked_showWifiScanNotify() { + whenever(internetDetailsContentController.isWifiEnabled).thenReturn(false) + whenever(internetDetailsContentController.isWifiScanEnabled).thenReturn(true) + whenever(internetDetailsContentController.isDeviceLocked).thenReturn(false) + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(wifiScanNotify!!.visibility).isEqualTo(View.VISIBLE) + val wifiScanNotifyText = + contentView.requireViewById<TextView>(R.id.wifi_scan_notify_text) + assertThat(wifiScanNotifyText.text.length).isNotEqualTo(0) + assertThat(wifiScanNotifyText.movementMethod).isNotNull() + } + } + + @Test + fun updateContent_wifiIsDisabled_uncheckWifiSwitch() { + whenever(internetDetailsContentController.isWifiEnabled).thenReturn(false) + wifiToggleSwitch!!.isChecked = true + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(wifiToggleSwitch!!.isChecked).isFalse() + } + } + + @Test + @Throws(Exception::class) + fun updateContent_wifiIsEnabled_checkWifiSwitch() { + whenever(internetDetailsContentController.isWifiEnabled).thenReturn(true) + wifiToggleSwitch!!.isChecked = false + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(wifiToggleSwitch!!.isChecked).isTrue() + } + } + + @Test + fun onClickSeeMoreButton_clickSeeAll_verifyLaunchNetworkSetting() { + seeAll!!.performClick() + + verify(internetDetailsContentController) + .launchNetworkSetting(contentView.requireViewById(R.id.see_all_layout)) + } + + @Test + fun onWifiScan_isScanTrue_setProgressBarVisibleTrue() { + internetDetailsContentManager.isProgressBarVisible = false + + internetDetailsContentManager.internetDetailsCallback.onWifiScan(true) + + assertThat(internetDetailsContentManager.isProgressBarVisible).isTrue() + } + + @Test + fun onWifiScan_isScanFalse_setProgressBarVisibleFalse() { + internetDetailsContentManager.isProgressBarVisible = true + + internetDetailsContentManager.internetDetailsCallback.onWifiScan(false) + + assertThat(internetDetailsContentManager.isProgressBarVisible).isFalse() + } + + @Test + fun updateContent_shareWifiIntentNull_hideButton() { + whenever( + internetDetailsContentController.getConfiguratorQrCodeGeneratorIntentOrNull( + ArgumentMatchers.any() + ) + ) + .thenReturn(null) + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(sharedWifiButton?.visibility).isEqualTo(View.GONE) + } + } + + @Test + fun updateContent_shareWifiShareable_showButton() { + whenever( + internetDetailsContentController.getConfiguratorQrCodeGeneratorIntentOrNull( + ArgumentMatchers.any() + ) + ) + .thenReturn(Intent()) + internetDetailsContentManager.updateContent(false) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(sharedWifiButton?.visibility).isEqualTo(View.VISIBLE) + } + } + + companion object { + private const val TITLE = "Internet" + private const val MOBILE_NETWORK_TITLE = "Mobile Title" + private const val MOBILE_NETWORK_SUMMARY = "Mobile Summary" + private const val WIFI_TITLE = "Connected Wi-Fi Title" + private const val WIFI_SUMMARY = "Connected Wi-Fi Summary" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt index a3c518128b47..f31d49094ac4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt @@ -26,7 +26,6 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -48,7 +47,6 @@ class DataStoreCoordinatorTest : SysuiTestCase() { private val pipeline: NotifPipeline = mock() private val notifLiveDataStoreImpl: NotifLiveDataStoreImpl = mock() - private val stackController: NotifStackController = mock() private val section: NotifSection = mock() @Before @@ -63,7 +61,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() { @Test fun testUpdateDataStore_withOneEntry() { - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(notifLiveDataStoreImpl).setActiveNotifList(eq(listOf(entry))) verifyNoMoreInteractions(notifLiveDataStoreImpl) } @@ -86,8 +84,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() { .setSection(section) .build(), notificationEntry("baz", 1), - ), - stackController, + ) ) val list: List<NotificationEntry> = withArgCaptor { verify(notifLiveDataStoreImpl).setActiveNotifList(capture()) @@ -111,7 +108,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() { @Test fun testUpdateDataStore_withZeroEntries_whenNewPipelineEnabled() { - afterRenderListListener.onAfterRenderList(listOf(), stackController) + afterRenderListListener.onAfterRenderList(listOf()) verify(notifLiveDataStoreImpl).setActiveNotifList(eq(listOf())) verifyNoMoreInteractions(notifLiveDataStoreImpl) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt index 2c37f510a45c..97e99b95f80e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt @@ -15,7 +15,6 @@ */ package com.android.systemui.statusbar.notification.collection.coordinator -import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -29,23 +28,20 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl -import com.android.systemui.statusbar.notification.collection.render.NotifStackController -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController +import com.android.systemui.util.mockito.withArgCaptor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever @SmallTest @@ -63,7 +59,6 @@ class StackCoordinatorTest : SysuiTestCase() { private val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController = mock() - private val stackController: NotifStackController = mock() private val section: NotifSection = mock() private val row: ExpandableNotificationRow = mock() @@ -82,198 +77,94 @@ class StackCoordinatorTest : SysuiTestCase() { sensitiveNotificationProtectionController, ) coordinator.attach(pipeline) - val captor = argumentCaptor<OnAfterRenderListListener>() - verify(pipeline).addOnAfterRenderListListener(captor.capture()) - afterRenderListListener = captor.lastValue + afterRenderListListener = withArgCaptor { + verify(pipeline).addOnAfterRenderListListener(capture()) + } } @Test fun testSetRenderedListOnInteractor() { - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(renderListInteractor).setRenderedList(eq(listOf(entry))) } @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) - fun testSetRenderedListOnInteractor_footerFlagOn() { - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) - verify(renderListInteractor).setRenderedList(eq(listOf(entry))) - } - - @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) fun testSetNotificationStats_clearableAlerting() { whenever(section.bucket).thenReturn(BUCKET_ALERTING) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) - verify(stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) + verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(activeNotificationsInteractor) } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING, FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX) fun testSetNotificationStats_isSensitiveStateActive_nonClearableAlerting() { whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) whenever(section.bucket).thenReturn(BUCKET_ALERTING) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) - verify(stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) + verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = true, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(activeNotificationsInteractor) } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) fun testSetNotificationStats_clearableSilent() { whenever(section.bucket).thenReturn(BUCKET_SILENT) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) - verify(stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) + verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = true, ) ) - verifyNoMoreInteractions(activeNotificationsInteractor) } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING, FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX) fun testSetNotificationStats_isSensitiveStateActive_nonClearableSilent() { whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) whenever(section.bucket).thenReturn(BUCKET_SILENT) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) - verify(stackController) - .setNotifStats( - NotifStats( - 1, - hasNonClearableAlertingNotifs = false, - hasClearableAlertingNotifs = false, - hasNonClearableSilentNotifs = true, - hasClearableSilentNotifs = false, - ) - ) - verifyNoMoreInteractions(activeNotificationsInteractor) - } - - @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) - fun testSetNotificationStats_footerFlagOn_clearableAlerting() { - whenever(section.bucket).thenReturn(BUCKET_ALERTING) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) - verify(activeNotificationsInteractor) - .setNotifStats( - NotifStats( - 1, - hasNonClearableAlertingNotifs = false, - hasClearableAlertingNotifs = true, - hasNonClearableSilentNotifs = false, - hasClearableSilentNotifs = false, - ) - ) - verifyNoMoreInteractions(stackController) - } - - @Test - @EnableFlags( - FooterViewRefactor.FLAG_NAME, - FLAG_SCREENSHARE_NOTIFICATION_HIDING, - FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, - ) - fun testSetNotificationStats_footerFlagOn_isSensitiveStateActive_nonClearableAlerting() { - whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) - whenever(section.bucket).thenReturn(BUCKET_ALERTING) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) - verify(activeNotificationsInteractor) - .setNotifStats( - NotifStats( - 1, - hasNonClearableAlertingNotifs = true, - hasClearableAlertingNotifs = false, - hasNonClearableSilentNotifs = false, - hasClearableSilentNotifs = false, - ) - ) - verifyNoMoreInteractions(stackController) - } - - @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) - fun testSetNotificationStats_footerFlagOn_clearableSilent() { - whenever(section.bucket).thenReturn(BUCKET_SILENT) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) - verify(activeNotificationsInteractor) - .setNotifStats( - NotifStats( - 1, - hasNonClearableAlertingNotifs = false, - hasClearableAlertingNotifs = false, - hasNonClearableSilentNotifs = false, - hasClearableSilentNotifs = true, - ) - ) - verifyNoMoreInteractions(stackController) - } - - @Test - @EnableFlags( - FooterViewRefactor.FLAG_NAME, - FLAG_SCREENSHARE_NOTIFICATION_HIDING, - FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, - ) - fun testSetNotificationStats_footerFlagOn_isSensitiveStateActive_nonClearableSilent() { - whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) - whenever(section.bucket).thenReturn(BUCKET_SILENT) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = true, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(stackController) } @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) - fun testSetNotificationStats_footerFlagOn_nonClearableRedacted() { + fun testSetNotificationStats_nonClearableRedacted() { entry.setSensitive(true, true) whenever(section.bucket).thenReturn(BUCKET_ALERTING) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = true, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(stackController) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt index 25138fd0ff83..c4ef4f978ff8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt @@ -35,9 +35,9 @@ import android.platform.test.annotations.EnableFlags import androidx.test.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.SysuiTestCase import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapperTest.Companion.any +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection @@ -111,25 +111,25 @@ class IconManagerTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun testCreateIcons_chipNotifIconFlagDisabled_statusBarChipIconIsNull() { + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun testCreateIcons_cdFlagDisabled_statusBarChipIconIsNotNull() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) entry?.let { iconManager.createIcons(it) } testScope.runCurrent() - assertThat(entry?.icons?.statusBarChipIcon).isNull() + assertThat(entry?.icons?.statusBarChipIcon).isNotNull() } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun testCreateIcons_chipNotifIconFlagEnabled_statusBarChipIconIsNull() { + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun testCreateIcons_cdFlagEnabled_statusBarChipIconIsNull() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) entry?.let { iconManager.createIcons(it) } testScope.runCurrent() - assertThat(entry?.icons?.statusBarChipIcon).isNotNull() + assertThat(entry?.icons?.statusBarChipIcon).isNull() } @Test @@ -158,7 +158,7 @@ class IconManagerTest : SysuiTestCase() { notificationEntry( hasShortcut = false, hasMessageSenderIcon = false, - hasLargeIcon = true + hasLargeIcon = true, ) entry?.channel?.isImportantConversation = true entry?.let { iconManager.createIcons(it) } @@ -172,7 +172,7 @@ class IconManagerTest : SysuiTestCase() { notificationEntry( hasShortcut = false, hasMessageSenderIcon = false, - hasLargeIcon = false + hasLargeIcon = false, ) entry?.channel?.isImportantConversation = true entry?.let { iconManager.createIcons(it) } @@ -187,7 +187,7 @@ class IconManagerTest : SysuiTestCase() { hasShortcut = true, hasMessageSenderIcon = true, useMessagingStyle = false, - hasLargeIcon = true + hasLargeIcon = true, ) entry?.channel?.isImportantConversation = true entry?.let { iconManager.createIcons(it) } @@ -204,8 +204,8 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun testCreateIcons_sensitiveImportantConversation() { + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun testCreateIcons_cdFlagDisabled_sensitiveImportantConversation() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) entry?.setSensitive(true, true) @@ -219,8 +219,23 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun testUpdateIcons_sensitiveImportantConversation() { + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun testCreateIcons_cdFlagEnabled_sensitiveImportantConversation() { + val entry = + notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) + entry?.setSensitive(true, true) + entry?.channel?.isImportantConversation = true + entry?.let { iconManager.createIcons(it) } + testScope.runCurrent() + assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc) + assertThat(entry?.icons?.statusBarChipIcon).isNull() + assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc) + assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc) + } + + @Test + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun testUpdateIcons_cdFlagDisabled_sensitiveImportantConversation() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) entry?.setSensitive(true, true) @@ -236,6 +251,23 @@ class IconManagerTest : SysuiTestCase() { } @Test + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun testUpdateIcons_cdFlagEnabled_sensitiveImportantConversation() { + val entry = + notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) + entry?.setSensitive(true, true) + entry?.channel?.isImportantConversation = true + entry?.let { iconManager.createIcons(it) } + // Updating the icons after creation shouldn't break anything + entry?.let { iconManager.updateIcons(it) } + testScope.runCurrent() + assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc) + assertThat(entry?.icons?.statusBarChipIcon).isNull() + assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc) + assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc) + } + + @Test fun testUpdateIcons_sensitivityChange() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) @@ -254,7 +286,7 @@ class IconManagerTest : SysuiTestCase() { hasShortcut: Boolean, hasMessageSenderIcon: Boolean, useMessagingStyle: Boolean = true, - hasLargeIcon: Boolean + hasLargeIcon: Boolean, ): NotificationEntry? { val n = Notification.Builder(mContext, "id") @@ -270,7 +302,7 @@ class IconManagerTest : SysuiTestCase() { SystemClock.currentThreadTimeMillis(), Person.Builder() .setIcon(if (hasMessageSenderIcon) messageIc else null) - .build() + .build(), ) ) if (useMessagingStyle) { 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 e1a891662889..3763282cdebc 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 @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification.stack; -import static android.view.View.GONE; import static android.view.WindowInsets.Type.ime; import static com.android.systemui.flags.SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag; @@ -28,17 +27,14 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertFalse; -import static org.mockito.AdditionalMatchers.not; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; @@ -64,7 +60,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowInsetsAnimation; -import android.widget.TextView; import androidx.test.filters.SmallTest; @@ -92,8 +87,6 @@ import com.android.systemui.statusbar.notification.collection.render.GroupExpans import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix; import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView; -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; -import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter; import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; import com.android.systemui.statusbar.notification.headsup.AvalancheController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -603,158 +596,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void manageNotifications_visible() { - FooterView view = mock(FooterView.class); - mStackScroller.setFooterView(view); - when(view.willBeGone()).thenReturn(true); - - mStackScroller.updateFooterView(true, false, true); - - verify(view).setVisible(eq(true), anyBoolean()); - verify(view).setClearAllButtonVisible(eq(false), anyBoolean()); - } - - @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void clearAll_visible() { - FooterView view = mock(FooterView.class); - mStackScroller.setFooterView(view); - when(view.willBeGone()).thenReturn(true); - - mStackScroller.updateFooterView(true, true, true); - - verify(view).setVisible(eq(true), anyBoolean()); - verify(view).setClearAllButtonVisible(eq(true), anyBoolean()); - } - - @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void testInflateFooterView() { - mStackScroller.inflateFooterView(); - ArgumentCaptor<FooterView> captor = ArgumentCaptor.forClass(FooterView.class); - verify(mStackScroller).setFooterView(captor.capture()); - - assertNotNull(captor.getValue().findViewById(R.id.manage_text)); - assertNotNull(captor.getValue().findViewById(R.id.dismiss_text)); - } - - @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void testUpdateFooter_noNotifications() { - setBarStateForTest(StatusBarState.SHADE); - mStackScroller.setCurrentUserSetup(true); - - FooterView view = mock(FooterView.class); - mStackScroller.setFooterView(view); - mStackScroller.updateFooter(); - verify(mStackScroller, atLeastOnce()).updateFooterView(false, false, true); - } - - @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - @DisableSceneContainer - public void testUpdateFooter_remoteInput() { - setBarStateForTest(StatusBarState.SHADE); - mStackScroller.setCurrentUserSetup(true); - - mStackScroller.setIsRemoteInputActive(true); - when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); - when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL))) - .thenReturn(true); - - FooterView view = mock(FooterView.class); - mStackScroller.setFooterView(view); - mStackScroller.updateFooter(); - verify(mStackScroller, atLeastOnce()).updateFooterView(false, true, true); - } - - @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void testUpdateFooter_withoutNotifications() { - setBarStateForTest(StatusBarState.SHADE); - mStackScroller.setCurrentUserSetup(true); - - when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); - when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL))) - .thenReturn(false); - - FooterView view = mock(FooterView.class); - mStackScroller.setFooterView(view); - mStackScroller.updateFooter(); - verify(mStackScroller, atLeastOnce()).updateFooterView(false, false, true); - } - - @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - @DisableSceneContainer - public void testUpdateFooter_oneClearableNotification() { - setBarStateForTest(StatusBarState.SHADE); - mStackScroller.setCurrentUserSetup(true); - - when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); - when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL))) - .thenReturn(true); - - FooterView view = mock(FooterView.class); - mStackScroller.setFooterView(view); - mStackScroller.updateFooter(); - verify(mStackScroller, atLeastOnce()).updateFooterView(true, true, true); - } - - @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - @DisableSceneContainer - public void testUpdateFooter_withoutHistory() { - setBarStateForTest(StatusBarState.SHADE); - mStackScroller.setCurrentUserSetup(true); - - when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(false); - when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); - when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL))) - .thenReturn(true); - - FooterView view = mock(FooterView.class); - mStackScroller.setFooterView(view); - mStackScroller.updateFooter(); - verify(mStackScroller, atLeastOnce()).updateFooterView(true, true, false); - } - - @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void testUpdateFooter_oneClearableNotification_beforeUserSetup() { - setBarStateForTest(StatusBarState.SHADE); - mStackScroller.setCurrentUserSetup(false); - - when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); - when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL))) - .thenReturn(true); - - FooterView view = mock(FooterView.class); - mStackScroller.setFooterView(view); - mStackScroller.updateFooter(); - verify(mStackScroller, atLeastOnce()).updateFooterView(false, true, true); - } - - @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - @DisableSceneContainer - public void testUpdateFooter_oneNonClearableNotification() { - setBarStateForTest(StatusBarState.SHADE); - mStackScroller.setCurrentUserSetup(true); - - when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); - when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL))) - .thenReturn(false); - when(mEmptyShadeView.getVisibility()).thenReturn(GONE); - - FooterView view = mock(FooterView.class); - mStackScroller.setFooterView(view); - mStackScroller.updateFooter(); - verify(mStackScroller, atLeastOnce()).updateFooterView(true, false, true); - } - - @Test public void testFooterPosition_atEnd() { // add footer FooterView view = mock(FooterView.class); @@ -772,19 +613,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, - ModesEmptyShadeFix.FLAG_NAME, - NotifRedesignFooter.FLAG_NAME}) - public void testReInflatesFooterViews() { - when(mEmptyShadeView.getTextResource()).thenReturn(R.string.empty_shade_text); - clearInvocations(mStackScroller); - mStackScroller.reinflateViews(); - verify(mStackScroller).setFooterView(any()); - verify(mStackScroller).setEmptyShadeView(any()); - } - - @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) @DisableFlags(ModesEmptyShadeFix.FLAG_NAME) public void testReInflatesEmptyShadeView() { when(mEmptyShadeView.getTextResource()).thenReturn(R.string.empty_shade_text); @@ -1231,31 +1059,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) - public void hasFilteredOutSeenNotifs_updateFooter() { - mStackScroller.setCurrentUserSetup(true); - - // add footer - mStackScroller.inflateFooterView(); - TextView footerLabel = - mStackScroller.mFooterView.requireViewById(R.id.unlock_prompt_footer); - - mStackScroller.setHasFilteredOutSeenNotifications(true); - mStackScroller.updateFooter(); - - assertThat(footerLabel.getVisibility()).isEqualTo(View.VISIBLE); - } - - @Test - @DisableFlags({FooterViewRefactor.FLAG_NAME, ModesEmptyShadeFix.FLAG_NAME}) - public void hasFilteredOutSeenNotifs_updateEmptyShadeView() { - mStackScroller.setHasFilteredOutSeenNotifications(true); - mStackScroller.updateEmptyShadeView(true, false); - - verify(mEmptyShadeView).setFooterText(not(eq(0))); - } - - @Test @DisableSceneContainer public void testWindowInsetAnimationProgress_updatesBottomInset() { int imeInset = 100; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 3a99328fa8ed..30ab416b1cbd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -42,6 +42,7 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.view.View; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; @@ -77,6 +78,8 @@ import com.android.systemui.statusbar.phone.ui.DarkIconManager; import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewBinder; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.StatusBarOperatorNameViewModel; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowController; @@ -1268,6 +1271,15 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mock(StatusBarOperatorNameViewModel.class)); mCollapsedStatusBarViewBinder = new FakeHomeStatusBarViewBinder(); + HomeStatusBarViewModelFactory homeStatusBarViewModelFactory = + new HomeStatusBarViewModelFactory() { + @NonNull + @Override + public HomeStatusBarViewModel create(int displayId) { + return mCollapsedStatusBarViewModel; + } + }; + return new CollapsedStatusBarFragment( mStatusBarFragmentComponentFactory, mOngoingCallController, @@ -1275,7 +1287,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mShadeExpansionStateManager, mStatusBarIconController, mIconManagerFactory, - mCollapsedStatusBarViewModel, + homeStatusBarViewModelFactory, mCollapsedStatusBarViewBinder, mStatusBarHideIconsForBouncerManager, mKeyguardStateController, diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt index 25d1c377ecbd..7ed736158a53 100644 --- a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt +++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt @@ -435,6 +435,8 @@ class FakeStatusBarService : IStatusBarService.Stub() { override fun unbundleNotification(key: String) {} + override fun rebundleNotification(key: String) {} + companion object { const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY const val SECONDARY_DISPLAY_ID = 2 diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt index 5ac41ec6741c..f380789968f5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt @@ -23,11 +23,11 @@ import com.android.systemui.util.mockito.mock val Kosmos.mockActivityTransitionAnimatorController by Kosmos.Fixture { mock<ActivityTransitionAnimator.Controller>() } -val Kosmos.activityTransitionAnimator by +var Kosmos.activityTransitionAnimator by Kosmos.Fixture { ActivityTransitionAnimator( // The main thread is checked in a bunch of places inside the different transitions // animators, so we have to pass the real main executor here. - mainExecutor = testCase.context.mainExecutor, + mainExecutor = testCase.context.mainExecutor ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt index 17093291e8b0..2a1877adc172 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt @@ -24,7 +24,6 @@ fun fakeDialogTransitionAnimator( @Main mainExecutor: Executor, isUnlocked: Boolean = true, isShowingAlternateAuthOnUnlock: Boolean = false, - isPredictiveBackQsDialogAnim: Boolean = false, interactionJankMonitor: InteractionJankMonitor, ): DialogTransitionAnimator { return DialogTransitionAnimator( @@ -35,10 +34,6 @@ fun fakeDialogTransitionAnimator( isShowingAlternateAuthOnUnlock = isShowingAlternateAuthOnUnlock, ), interactionJankMonitor = interactionJankMonitor, - featureFlags = - object : AnimationFeatureFlags { - override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim - }, transitionAnimator = fakeTransitionAnimator(mainExecutor), isForTesting = true, ) @@ -50,6 +45,8 @@ private class FakeCallback( private val isShowingAlternateAuthOnUnlock: Boolean = false, ) : DialogTransitionAnimator.Callback { override fun isDreaming(): Boolean = isDreaming + override fun isUnlocked(): Boolean = isUnlocked + override fun isShowingAlternateAuthOnUnlock() = isShowingAlternateAuthOnUnlock } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt index a839f17aad82..c744eacfa3f4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt @@ -33,6 +33,9 @@ class FakeAudioSharingRepository : AudioSharingRepository { var sourceAdded: Boolean = false private set + var audioSharingStarted: Boolean = false + private set + private var profile: LocalBluetoothLeBroadcast? = null override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? @@ -50,7 +53,13 @@ class FakeAudioSharingRepository : AudioSharingRepository { override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {} - override suspend fun startAudioSharing() {} + override suspend fun startAudioSharing() { + audioSharingStarted = true + } + + override suspend fun stopAudioSharing() { + audioSharingStarted = false + } fun setAudioSharingAvailable(available: Boolean) { mutableAvailable = available diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/compose/Snapshot.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/compose/Snapshot.kt new file mode 100644 index 000000000000..fb6699c44d62 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/compose/Snapshot.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 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.compose + +import androidx.compose.runtime.snapshots.Snapshot +import com.android.systemui.kosmos.runCurrent +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest + +/** + * Runs the given test [block] in a [TestScope] that's set up such that the Compose snapshot state + * is settled eagerly. This is the Compose equivalent to using an [UnconfinedTestDispatcher] or + * using [runCurrent] a lot. + * + * Note that this shouldn't be needed or used in a Compose test environment. + */ +fun TestScope.runTestWithSnapshots(block: suspend TestScope.() -> Unit) { + val handle = Snapshot.registerGlobalWriteObserver { Snapshot.sendApplyNotifications() } + + try { + runTest { block() } + } finally { + handle.dispose() + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt index 2a7e3e903737..490b89bf6b13 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt @@ -23,6 +23,7 @@ import com.android.systemui.biometrics.data.repository.fingerprintPropertyReposi import com.android.systemui.dump.dumpManager import com.android.systemui.keyevent.domain.interactor.keyEventInteractor import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.util.time.systemClock @@ -34,6 +35,8 @@ val Kosmos.deviceEntryHapticsInteractor by DeviceEntryHapticsInteractor( biometricSettingsRepository = biometricSettingsRepository, deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor, + deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, + keyguardBypassInteractor = keyguardBypassInteractor, deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor = deviceEntrySourceInteractor, fingerprintPropertyRepository = fingerprintPropertyRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt index 3fc60e339543..a64fc2413246 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt @@ -115,3 +115,6 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { interface FakeDisplayRepositoryModule { @Binds fun bindFake(fake: FakeDisplayRepository): DisplayRepository } + +val DisplayRepository.fake + get() = this as FakeDisplayRepository diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt index 47991b3b9689..3df3ee983ecf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt @@ -154,6 +154,7 @@ val Kosmos.shortcutHelperCoreStartable by shortcutHelperStateRepository, activityStarter, testScope, + customInputGesturesRepository ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 1288d3151051..8489d8380041 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -46,9 +46,6 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { MutableSharedFlow(extraBufferCapacity = 1) override val keyguardDoneAnimationsFinished: Flow<Unit> = _keyguardDoneAnimationsFinished - private val _clockShouldBeCentered = MutableStateFlow<Boolean>(true) - override val clockShouldBeCentered: Flow<Boolean> = _clockShouldBeCentered - private val _dismissAction = MutableStateFlow<DismissAction>(DismissAction.None) override val dismissAction: StateFlow<DismissAction> = _dismissAction @@ -192,10 +189,6 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { _keyguardDoneAnimationsFinished.tryEmit(Unit) } - override fun setClockShouldBeCentered(shouldBeCentered: Boolean) { - _clockShouldBeCentered.value = shouldBeCentered - } - override fun setKeyguardEnabled(enabled: Boolean) { _isKeyguardEnabled.value = enabled } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt index 8209ee12ad9a..f4791003c828 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -402,6 +402,12 @@ class FakeKeyguardTransitionRepository( ) ) } + + suspend fun transitionTo(from: KeyguardState, to: KeyguardState) { + sendTransitionStep(TransitionStep(from, to, 0f, TransitionState.STARTED)) + sendTransitionStep(TransitionStep(from, to, 0.5f, TransitionState.RUNNING)) + sendTransitionStep(TransitionStep(from, to, 1f, TransitionState.FINISHED)) + } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt index 3de809308702..ee21bdc0b4c2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt @@ -24,8 +24,6 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.util.mockito.mock @@ -55,7 +53,6 @@ object KeyguardInteractorFactory { fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor = mock(), fromOccludedTransitionInteractor: FromOccludedTransitionInteractor = mock(), fromAlternateBouncerTransitionInteractor: FromAlternateBouncerTransitionInteractor = mock(), - powerInteractor: PowerInteractor = PowerInteractorFactory.create().powerInteractor, testScope: CoroutineScope = TestScope(), ): WithDependencies { // Mock these until they are replaced by kosmos @@ -73,10 +70,8 @@ object KeyguardInteractorFactory { bouncerRepository = bouncerRepository, configurationRepository = configurationRepository, shadeRepository = shadeRepository, - powerInteractor = powerInteractor, KeyguardInteractor( repository = repository, - powerInteractor = powerInteractor, bouncerRepository = bouncerRepository, configurationInteractor = ConfigurationInteractorImpl(configurationRepository), shadeRepository = shadeRepository, @@ -99,7 +94,6 @@ object KeyguardInteractorFactory { val bouncerRepository: FakeKeyguardBouncerRepository, val configurationRepository: FakeConfigurationRepository, val shadeRepository: FakeShadeRepository, - val powerInteractor: PowerInteractor, val keyguardInteractor: KeyguardInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt index f5f8ef75065f..869bae236d5c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt @@ -21,7 +21,6 @@ import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope -import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.data.repository.shadeRepository @@ -29,7 +28,6 @@ val Kosmos.keyguardInteractor: KeyguardInteractor by Kosmos.Fixture { KeyguardInteractor( repository = keyguardRepository, - powerInteractor = powerInteractor, bouncerRepository = keyguardBouncerRepository, configurationInteractor = configurationInteractor, shadeRepository = shadeRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt index 15d00d9f6994..edc1cce326c3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt @@ -20,4 +20,5 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeBouncerTransition : PrimaryBouncerTransition { override val windowBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f) + override val notificationBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index 1881a94c8984..439df543b9fb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -1,7 +1,7 @@ package com.android.systemui.kosmos -import androidx.compose.runtime.snapshots.Snapshot import com.android.systemui.SysuiTestCase +import com.android.systemui.compose.runTestWithSnapshots import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -17,7 +17,6 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest import org.mockito.kotlin.verify var Kosmos.testDispatcher by Fixture { StandardTestDispatcher() } @@ -53,26 +52,10 @@ var Kosmos.brightnessWarningToast: BrightnessWarningToast by /** * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in - * that kosmos instance + * that Kosmos instance */ -fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) = - testScope.runTest testBody@{ this@runTest.testBody() } - -/** - * Runs the given [Kosmos]-scoped test [block] in an environment where compose snapshot state is - * settled eagerly. This is the compose equivalent to using an [UnconfinedTestDispatcher] or using - * [runCurrent] a lot. - * - * Note that this shouldn't be needed or used in a compose test environment. - */ -fun Kosmos.runTestWithSnapshots(block: suspend Kosmos.() -> Unit) { - val handle = Snapshot.registerGlobalWriteObserver { Snapshot.sendApplyNotifications() } - - try { - testScope.runTest { block() } - } finally { - handle.dispose() - } +fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) = let { kosmos -> + testScope.runTestWithSnapshots { kosmos.testBody() } } fun Kosmos.runCurrent() = testScope.runCurrent() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index 82b5f6332b23..72c75000ebf4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.scene.domain.startable import com.android.internal.logging.uiEventLogger +import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor @@ -85,5 +86,6 @@ val Kosmos.sceneContainerStartable by Fixture { vibratorHelper = vibratorHelper, msdlPlayer = msdlPlayer, disabledContentInteractor = disabledContentInteractor, + activityTransitionAnimator = activityTransitionAnimator, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/SceneJankMonitorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/SceneJankMonitorKosmos.kt new file mode 100644 index 000000000000..bcba5ee50b8c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/SceneJankMonitorKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 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.scene.ui.view + +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.sceneJankMonitorFactory: SceneJankMonitor.Factory by Fixture { + object : SceneJankMonitor.Factory { + override fun create(): SceneJankMonitor { + return SceneJankMonitor( + authenticationInteractor = authenticationInteractor, + deviceUnlockedInteractor = deviceUnlockedInteractor, + interactionJankMonitor = interactionJankMonitor, + ) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt index ab193d294b8c..b3d89dbb834d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt @@ -203,6 +203,7 @@ class ShadeTestUtilSceneImpl( val isUserInputOngoing = MutableStateFlow(true) override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) { + shadeRepository.setLegacyIsQsExpanded(qsExpansion > 0f) if (shadeExpansion == 1f) { setIdleScene(Scenes.Shade) } else if (qsExpansion == 1f) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt index 4af5e7d9d725..6e44df833582 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt @@ -23,11 +23,21 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker import com.android.systemui.shade.ShadeWindowLayoutParams import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository +import java.util.Optional +import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever val Kosmos.shadeLayoutParams by Kosmos.Fixture { ShadeWindowLayoutParams.create(mockedContext) } -val Kosmos.mockedWindowContext by Kosmos.Fixture { mock<WindowContext>() } +val Kosmos.mockedWindowContext by + Kosmos.Fixture { + mock<WindowContext>().apply { + whenever(reparentToDisplay(any())).thenAnswer { displayIdParam -> + whenever(displayId).thenReturn(displayIdParam.arguments[0] as Int) + } + } + } val Kosmos.mockedShadeDisplayChangeLatencyTracker by Kosmos.Fixture { mock<ShadeDisplayChangeLatencyTracker>() } val Kosmos.shadeDisplaysInteractor by @@ -38,5 +48,6 @@ val Kosmos.shadeDisplaysInteractor by testScope.backgroundScope, testScope.backgroundScope.coroutineContext, mockedShadeDisplayChangeLatencyTracker, + Optional.of(shadeExpandedStateInteractor), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index af6d6249b4a8..1dc7229a6506 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.ShadeModule @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.policy.data.repository.userSetupRepository import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.user.domain.interactor.userSwitcherInteractor +import org.mockito.kotlin.mock var Kosmos.baseShadeInteractor: BaseShadeInteractor by Kosmos.Fixture { @@ -71,3 +73,7 @@ val Kosmos.shadeInteractorImpl by shadeModeInteractor = shadeModeInteractor, ) } +var Kosmos.mockShadeInteractor: ShadeInteractor by Kosmos.Fixture { mock() } +val Kosmos.shadeExpandedStateInteractor by + Kosmos.Fixture { ShadeExpandedStateInteractorImpl(shadeInteractor, testScope.backgroundScope) } +val Kosmos.fakeShadeExpandedStateInteractor by Kosmos.Fixture { FakeShadeExpandedStateInteractor() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt index 69e215dcba6a..90897faaa6f8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt @@ -16,13 +16,28 @@ package com.android.systemui.statusbar.layout +import android.content.applicationContext +import com.android.systemui.SysUICutoutProvider +import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.commandline.commandRegistry +import com.android.systemui.statusbar.policy.configurationController +import com.android.systemui.statusbar.policy.fake import org.mockito.kotlin.mock val Kosmos.mockStatusBarContentInsetsProvider by Kosmos.Fixture { mock<StatusBarContentInsetsProvider>() } -var Kosmos.statusBarContentInsetsProvider by Kosmos.Fixture { mockStatusBarContentInsetsProvider } +val Kosmos.statusBarContentInsetsProvider by + Kosmos.Fixture { + StatusBarContentInsetsProviderImpl( + applicationContext, + configurationController.fake, + dumpManager, + commandRegistry, + mock<SysUICutoutProvider>(), + ) + } val Kosmos.fakeStatusBarContentInsetsProviderFactory by Kosmos.Fixture { FakeStatusBarContentInsetsProviderFactory() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt new file mode 100644 index 000000000000..889d469489f2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.data.repository.multiDisplayStatusBarContentInsetsProviderStore +import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider + +val Kosmos.statusBarContentInsetsViewModel by + Kosmos.Fixture { StatusBarContentInsetsViewModel(statusBarContentInsetsProvider) } + +val Kosmos.multiDisplayStatusBarContentInsetsViewModelStore by + Kosmos.Fixture { + MultiDisplayStatusBarContentInsetsViewModelStore( + applicationCoroutineScope, + displayRepository, + multiDisplayStatusBarContentInsetsProviderStore, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index d1619b7959f2..60e092c9709b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.heads import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor +import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalCoroutinesApi::class) @@ -99,6 +100,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel, primaryBouncerToLockscreenTransitionViewModel = primaryBouncerToLockscreenTransitionViewModel, + primaryBouncerTransitions = fakeBouncerTransitions, aodBurnInViewModel = aodBurnInViewModel, communalSceneInteractor = communalSceneInteractor, headsUpNotificationInteractor = { headsUpNotificationInteractor }, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt index b38a723f1fa7..db7e31bb2cb6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel +import android.content.testableContext import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos @@ -26,6 +27,7 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModel +import com.android.systemui.statusbar.layout.ui.viewmodel.multiDisplayStatusBarContentInsetsViewModelStore import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.phone.domain.interactor.darkIconInteractor @@ -36,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStat var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by Kosmos.Fixture { HomeStatusBarViewModelImpl( + testableContext.displayId, homeStatusBarInteractor, homeStatusBarIconBlockListInteractor, lightsOutInteractor, @@ -51,6 +54,7 @@ var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by ongoingActivityChipsViewModel, statusBarPopupChipsViewModel, systemStatusEventAnimationInteractor, + multiDisplayStatusBarContentInsetsViewModelStore, applicationCoroutineScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt index 282f5947636c..1e4701333857 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt @@ -25,3 +25,6 @@ val Kosmos.fakeConfigurationController: FakeConfigurationController by Kosmos.Fixture { FakeConfigurationController() } val Kosmos.statusBarConfigurationController: StatusBarConfigurationController by Kosmos.Fixture { fakeConfigurationController } + +val ConfigurationController.fake + get() = this as FakeConfigurationController diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt index 1ba5ddbf0337..fc0c92e974f7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt @@ -20,5 +20,5 @@ import com.android.settingslib.notification.data.repository.FakeZenModeRepositor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.zenModeRepository by Fixture { fakeZenModeRepository } +var Kosmos.zenModeRepository by Fixture { fakeZenModeRepository } val Kosmos.fakeZenModeRepository by Fixture { FakeZenModeRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt index ed5322ed098e..db19d6ee9077 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt @@ -39,7 +39,7 @@ val Kosmos.localMediaRepositoryFactory by val Kosmos.mediaOutputActionsInteractor by Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogManager) } -val Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() } +var Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() } val Kosmos.mediaOutputInteractor by Kosmos.Fixture { MediaOutputInteractor( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt index 712ec41bbf2d..3f2b47948c1c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt @@ -19,4 +19,4 @@ package com.android.systemui.volume.data.repository import com.android.systemui.kosmos.Kosmos val Kosmos.fakeAudioRepository by Kosmos.Fixture { FakeAudioRepository() } -val Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository } +var Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt new file mode 100644 index 000000000000..e2431934bc40 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.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.volume.dialog + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.dagger.volumeDialogComponentFactory +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor + +val Kosmos.volumeDialog by + Kosmos.Fixture { + VolumeDialog( + context = applicationContext, + visibilityInteractor = volumeDialogVisibilityInteractor, + componentFactory = volumeDialogComponentFactory, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt new file mode 100644 index 000000000000..73e5d8d40985 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt @@ -0,0 +1,41 @@ +/* + * 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.volume.dialog.dagger + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderComponent +import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory +import com.android.systemui.volume.dialog.ui.binder.VolumeDialogViewBinder +import com.android.systemui.volume.dialog.ui.binder.volumeDialogViewBinder +import kotlinx.coroutines.CoroutineScope + +val Kosmos.volumeDialogComponentFactory by + Kosmos.Fixture { + object : VolumeDialogComponent.Factory { + override fun create(scope: CoroutineScope): VolumeDialogComponent = + volumeDialogComponent + } + } +val Kosmos.volumeDialogComponent by + Kosmos.Fixture { + object : VolumeDialogComponent { + override fun volumeDialogViewBinder(): VolumeDialogViewBinder = volumeDialogViewBinder + + override fun sliderComponentFactory(): VolumeDialogSliderComponent.Factory = + volumeDialogSliderComponentFactory + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt index 291dfc0430e2..3d5698b193e1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt @@ -19,4 +19,4 @@ package com.android.systemui.volume.dialog.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.volume.dialog.data.VolumeDialogVisibilityRepository -val Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() } +var Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt index db9c48d9be6f..8f122b57e9d4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt @@ -16,8 +16,6 @@ package com.android.systemui.volume.dialog.domain.interactor -import android.os.Handler -import android.os.looper import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.volumeDialogController @@ -27,6 +25,6 @@ val Kosmos.volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor by VolumeDialogCallbacksInteractor( volumeDialogController = volumeDialogController, coroutineScope = applicationCoroutineScope, - bgHandler = Handler(looper), + bgHandler = null, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt new file mode 100644 index 000000000000..7cbdc3d9f6ee --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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.volume.dialog.ringer + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder +import com.android.systemui.volume.dialog.ringer.ui.viewmodel.volumeDialogRingerDrawerViewModel + +val Kosmos.volumeDialogRingerViewBinder by + Kosmos.Fixture { VolumeDialogRingerViewBinder(volumeDialogRingerDrawerViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt index 44371b4615df..cf357b498621 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt @@ -20,5 +20,5 @@ import com.android.systemui.kosmos.Kosmos val Kosmos.fakeVolumeDialogRingerFeedbackRepository by Kosmos.Fixture { FakeVolumeDialogRingerFeedbackRepository() } -val Kosmos.volumeDialogRingerFeedbackRepository by +var Kosmos.volumeDialogRingerFeedbackRepository: VolumeDialogRingerFeedbackRepository by Kosmos.Fixture { fakeVolumeDialogRingerFeedbackRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt index a494d04ec741..4bebf8911613 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt @@ -21,7 +21,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.volumeDialogController import com.android.systemui.volume.data.repository.audioSystemRepository import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor -import com.android.systemui.volume.dialog.ringer.data.repository.fakeVolumeDialogRingerFeedbackRepository +import com.android.systemui.volume.dialog.ringer.data.repository.volumeDialogRingerFeedbackRepository val Kosmos.volumeDialogRingerInteractor by Kosmos.Fixture { @@ -30,6 +30,6 @@ val Kosmos.volumeDialogRingerInteractor by volumeDialogStateInteractor = volumeDialogStateInteractor, controller = volumeDialogController, audioSystemRepository = audioSystemRepository, - ringerFeedbackRepository = fakeVolumeDialogRingerFeedbackRepository, + ringerFeedbackRepository = volumeDialogRingerFeedbackRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt new file mode 100644 index 000000000000..26b8bca6344b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.settings.domain + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.policy.deviceProvisionedController +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor +import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor + +val Kosmos.volumeDialogSettingsButtonInteractor by + Kosmos.Fixture { + VolumeDialogSettingsButtonInteractor( + applicationCoroutineScope, + deviceProvisionedController, + volumePanelGlobalStateInteractor, + volumeDialogVisibilityInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt new file mode 100644 index 000000000000..f9e128ddd810 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.volume.dialog.settings.ui.binder + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.settings.ui.viewmodel.volumeDialogSettingsButtonViewModel + +val Kosmos.volumeDialogSettingsButtonViewBinder by + Kosmos.Fixture { VolumeDialogSettingsButtonViewBinder(volumeDialogSettingsButtonViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt new file mode 100644 index 000000000000..0ae3b037b50a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt @@ -0,0 +1,37 @@ +/* + * 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.volume.dialog.settings.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.volume.dialog.settings.domain.volumeDialogSettingsButtonInteractor +import com.android.systemui.volume.mediaDeviceSessionInteractor +import com.android.systemui.volume.mediaOutputInteractor + +val Kosmos.volumeDialogSettingsButtonViewModel by + Kosmos.Fixture { + VolumeDialogSettingsButtonViewModel( + applicationContext, + testScope.testScheduler, + applicationCoroutineScope, + mediaOutputInteractor, + mediaDeviceSessionInteractor, + volumeDialogSettingsButtonInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt new file mode 100644 index 000000000000..4f79f7b4b41a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt @@ -0,0 +1,84 @@ +/* + * 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.volume.dialog.sliders.dagger + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.volumeDialogController +import com.android.systemui.statusbar.policy.data.repository.zenModeRepository +import com.android.systemui.volume.data.repository.audioRepository +import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository +import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType +import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogOverscrollViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderHapticsViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderViewBinder +import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.mediaControllerInteractor + +private val Kosmos.mutableSliderComponentKosmoses: MutableMap<VolumeDialogSliderType, Kosmos> by + Kosmos.Fixture { mutableMapOf() } + +val Kosmos.volumeDialogSliderComponentFactory by + Kosmos.Fixture { + object : VolumeDialogSliderComponent.Factory { + override fun create(sliderType: VolumeDialogSliderType): VolumeDialogSliderComponent = + volumeDialogSliderComponent(sliderType) + } + } + +fun Kosmos.volumeDialogSliderComponent(type: VolumeDialogSliderType): VolumeDialogSliderComponent { + return object : VolumeDialogSliderComponent { + + private val localKosmos + get() = + mutableSliderComponentKosmoses.getOrPut(type) { + Kosmos().also { + it.setupVolumeDialogSliderComponent(this@volumeDialogSliderComponent, type) + } + } + + override fun sliderViewBinder(): VolumeDialogSliderViewBinder = + localKosmos.volumeDialogSliderViewBinder + + override fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder = + localKosmos.volumeDialogSliderHapticsViewBinder + + override fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder = + localKosmos.volumeDialogOverscrollViewBinder + } +} + +private fun Kosmos.setupVolumeDialogSliderComponent( + parentKosmos: Kosmos, + type: VolumeDialogSliderType, +) { + volumeDialogSliderType = type + applicationContext = parentKosmos.applicationContext + testScope = parentKosmos.testScope + + volumeDialogController = parentKosmos.volumeDialogController + mediaControllerInteractor = parentKosmos.mediaControllerInteractor + mediaControllerRepository = parentKosmos.mediaControllerRepository + zenModeRepository = parentKosmos.zenModeRepository + volumeDialogVisibilityRepository = parentKosmos.volumeDialogVisibilityRepository + audioRepository = parentKosmos.audioRepository +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorKosmos.kt index 44917dd4ba48..198d72a41fa4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.dialog.sliders.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.volumeDialogController +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType @@ -29,5 +30,6 @@ val Kosmos.volumeDialogSliderInteractor: VolumeDialogSliderInteractor by applicationCoroutineScope, volumeDialogStateInteractor, volumeDialogController, + zenModeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt new file mode 100644 index 000000000000..13d6ca9732d1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.volume.dialog.sliders.ui + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogOverscrollViewModel + +val Kosmos.volumeDialogOverscrollViewBinder by + Kosmos.Fixture { VolumeDialogOverscrollViewBinder(volumeDialogOverscrollViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt new file mode 100644 index 000000000000..d6845b1ff7e3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.sliders.ui + +import com.android.systemui.haptics.msdl.msdlPlayer +import com.android.systemui.haptics.vibratorHelper +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.time.systemClock +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel + +val Kosmos.volumeDialogSliderHapticsViewBinder by + Kosmos.Fixture { + VolumeDialogSliderHapticsViewBinder( + volumeDialogSliderInputEventsViewModel, + vibratorHelper, + msdlPlayer, + systemClock, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt new file mode 100644 index 000000000000..c6db717e004f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.volume.dialog.sliders.ui + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderViewModel + +val Kosmos.volumeDialogSliderViewBinder by + Kosmos.Fixture { + VolumeDialogSliderViewBinder( + volumeDialogSliderViewModel, + volumeDialogSliderInputEventsViewModel, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt new file mode 100644 index 000000000000..83527d994e70 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.volume.dialog.sliders.ui + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSlidersViewModel + +val Kosmos.volumeDialogSlidersViewBinder by + Kosmos.Fixture { VolumeDialogSlidersViewBinder(volumeDialogSlidersViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt new file mode 100644 index 000000000000..fe2f3d806b6a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.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.systemui.volume.dialog.sliders.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor + +val Kosmos.volumeDialogOverscrollViewModel by + Kosmos.Fixture { + VolumeDialogOverscrollViewModel(applicationContext, volumeDialogSliderInputEventsInteractor) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt new file mode 100644 index 000000000000..09f9f1c6362e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.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.volume.dialog.sliders.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.volume.domain.interactor.audioVolumeInteractor + +val Kosmos.volumeDialogSliderIconProvider by + Kosmos.Fixture { + VolumeDialogSliderIconProvider( + context = applicationContext, + audioVolumeInteractor = audioVolumeInteractor, + zenModeInteractor = zenModeInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt new file mode 100644 index 000000000000..2de0e8f76a4b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.volume.dialog.sliders.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor + +val Kosmos.volumeDialogSliderInputEventsViewModel by + Kosmos.Fixture { + VolumeDialogSliderInputEventsViewModel( + applicationCoroutineScope, + volumeDialogSliderInputEventsInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt new file mode 100644 index 000000000000..63cd440a8633 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt @@ -0,0 +1,34 @@ +/* + * 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.volume.dialog.sliders.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.util.time.systemClock +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInteractor + +val Kosmos.volumeDialogSliderViewModel by + Kosmos.Fixture { + VolumeDialogSliderViewModel( + interactor = volumeDialogSliderInteractor, + visibilityInteractor = volumeDialogVisibilityInteractor, + coroutineScope = applicationCoroutineScope, + volumeDialogSliderIconProvider = volumeDialogSliderIconProvider, + systemClock = systemClock, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt new file mode 100644 index 000000000000..5531f7608b69 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.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.volume.dialog.sliders.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor + +val Kosmos.volumeDialogSlidersViewModel by + Kosmos.Fixture { + VolumeDialogSlidersViewModel( + applicationCoroutineScope, + volumeDialogSlidersInteractor, + volumeDialogSliderComponentFactory, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt new file mode 100644 index 000000000000..dc09e3233b1e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.ui.binder + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.ringer.volumeDialogRingerViewBinder +import com.android.systemui.volume.dialog.settings.ui.binder.volumeDialogSettingsButtonViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSlidersViewBinder +import com.android.systemui.volume.dialog.ui.utils.jankListenerFactory +import com.android.systemui.volume.dialog.ui.viewmodel.volumeDialogViewModel +import com.android.systemui.volume.dialog.utils.volumeTracer + +val Kosmos.volumeDialogViewBinder by + Kosmos.Fixture { + VolumeDialogViewBinder( + applicationContext.resources, + volumeDialogViewModel, + jankListenerFactory, + volumeTracer, + volumeDialogRingerViewBinder, + volumeDialogSlidersViewBinder, + volumeDialogSettingsButtonViewBinder, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt new file mode 100644 index 000000000000..35ec5d3cc9af --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.volume.dialog.ui.utils + +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.kosmos.Kosmos + +val Kosmos.jankListenerFactory by Kosmos.Fixture { JankListenerFactory(interactionJankMonitor) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt new file mode 100644 index 000000000000..05ef462d4998 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt @@ -0,0 +1,37 @@ +/* + * 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.volume.dialog.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.policy.configurationController +import com.android.systemui.statusbar.policy.devicePostureController +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor + +val Kosmos.volumeDialogViewModel by + Kosmos.Fixture { + VolumeDialogViewModel( + applicationContext, + volumeDialogVisibilityInteractor, + volumeDialogSlidersInteractor, + volumeDialogStateInteractor, + devicePostureController, + configurationController, + ) + } diff --git a/packages/Vcn/service-b/Android.bp b/packages/Vcn/service-b/Android.bp index 1370b0678cc5..97574e6e35e3 100644 --- a/packages/Vcn/service-b/Android.bp +++ b/packages/Vcn/service-b/Android.bp @@ -39,9 +39,7 @@ java_library { name: "connectivity-utils-service-vcn-internal", sdk_version: "module_current", min_sdk_version: "30", - srcs: [ - ":framework-connectivity-shared-srcs", - ], + srcs: ["service-utils/**/*.java"], libs: [ "framework-annotations-lib", "unsupportedappusage", diff --git a/packages/Vcn/service-b/service-utils/android/util/LocalLog.java b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java new file mode 100644 index 000000000000..5955d930aab1 --- /dev/null +++ b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2025 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.util; + +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.SystemClock; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +/** + * @hide + */ +// Exported to Mainline modules; cannot use annotations +// @android.ravenwood.annotation.RavenwoodKeepWholeClass +// TODO: b/374174952 This is an exact copy of frameworks/base/core/java/android/util/LocalLog.java. +// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag +// is fully ramped. When the flag is fully ramped and the development is finalized, this file can +// be removed. +public final class LocalLog { + + private final Deque<String> mLog; + private final int mMaxLines; + + /** + * {@code true} to use log timestamps expressed in local date/time, {@code false} to use log + * timestamped expressed with the elapsed realtime clock and UTC system clock. {@code false} is + * useful when logging behavior that modifies device time zone or system clock. + */ + private final boolean mUseLocalTimestamps; + + @UnsupportedAppUsage + public LocalLog(int maxLines) { + this(maxLines, true /* useLocalTimestamps */); + } + + public LocalLog(int maxLines, boolean useLocalTimestamps) { + mMaxLines = Math.max(0, maxLines); + mLog = new ArrayDeque<>(mMaxLines); + mUseLocalTimestamps = useLocalTimestamps; + } + + @UnsupportedAppUsage + public void log(String msg) { + if (mMaxLines <= 0) { + return; + } + final String logLine; + if (mUseLocalTimestamps) { + logLine = LocalDateTime.now() + " - " + msg; + } else { + logLine = Duration.ofMillis(SystemClock.elapsedRealtime()) + + " / " + Instant.now() + " - " + msg; + } + append(logLine); + } + + private synchronized void append(String logLine) { + while (mLog.size() >= mMaxLines) { + mLog.remove(); + } + mLog.add(logLine); + } + + @UnsupportedAppUsage + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + dump(pw); + } + + public synchronized void dump(PrintWriter pw) { + dump("", pw); + } + + /** + * Dumps the content of local log to print writer with each log entry predeced with indent + * + * @param indent indent that precedes each log entry + * @param pw printer writer to write into + */ + public synchronized void dump(String indent, PrintWriter pw) { + Iterator<String> itr = mLog.iterator(); + while (itr.hasNext()) { + pw.printf("%s%s\n", indent, itr.next()); + } + } + + public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) { + reverseDump(pw); + } + + public synchronized void reverseDump(PrintWriter pw) { + Iterator<String> itr = mLog.descendingIterator(); + while (itr.hasNext()) { + pw.println(itr.next()); + } + } + + // @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public synchronized void clear() { + mLog.clear(); + } + + public static class ReadOnlyLocalLog { + private final LocalLog mLog; + ReadOnlyLocalLog(LocalLog log) { + mLog = log; + } + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mLog.dump(pw); + } + public void dump(PrintWriter pw) { + mLog.dump(pw); + } + public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) { + mLog.reverseDump(pw); + } + public void reverseDump(PrintWriter pw) { + mLog.reverseDump(pw); + } + } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public ReadOnlyLocalLog readOnlyLocalLog() { + return new ReadOnlyLocalLog(this); + } +}
\ No newline at end of file diff --git a/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java new file mode 100644 index 000000000000..7db62f8e9ffc --- /dev/null +++ b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2025 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.internal.util; + +import android.app.AlarmManager; +import android.content.Context; +import android.os.Handler; +import android.os.Message; + +import com.android.internal.annotations.VisibleForTesting; + + /** + * An AlarmListener that sends the specified message to a Handler and keeps the system awake until + * the message is processed. + * + * This is useful when using the AlarmManager direct callback interface to wake up the system and + * request that an object whose API consists of messages (such as a StateMachine) perform some + * action. + * + * In this situation, using AlarmManager.onAlarmListener by itself will wake up the system to send + * the message, but does not guarantee that the system will be awake until the target object has + * processed it. This is because as soon as the onAlarmListener sends the message and returns, the + * AlarmManager releases its wakelock and the system is free to go to sleep again. + */ +// TODO: b/374174952 This is an exact copy of +// frameworks/base/core/java/com/android/internal/util/WakeupMessage.java. +// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag +// is fully ramped. When the flag is fully ramped and the development is finalized, this file can +// be removed. +public class WakeupMessage implements AlarmManager.OnAlarmListener { + private final AlarmManager mAlarmManager; + + @VisibleForTesting + protected final Handler mHandler; + @VisibleForTesting + protected final String mCmdName; + @VisibleForTesting + protected final int mCmd, mArg1, mArg2; + @VisibleForTesting + protected final Object mObj; + private final Runnable mRunnable; + private boolean mScheduled; + + public WakeupMessage(Context context, Handler handler, + String cmdName, int cmd, int arg1, int arg2, Object obj) { + mAlarmManager = getAlarmManager(context); + mHandler = handler; + mCmdName = cmdName; + mCmd = cmd; + mArg1 = arg1; + mArg2 = arg2; + mObj = obj; + mRunnable = null; + } + + public WakeupMessage(Context context, Handler handler, String cmdName, int cmd, int arg1) { + this(context, handler, cmdName, cmd, arg1, 0, null); + } + + public WakeupMessage(Context context, Handler handler, + String cmdName, int cmd, int arg1, int arg2) { + this(context, handler, cmdName, cmd, arg1, arg2, null); + } + + public WakeupMessage(Context context, Handler handler, String cmdName, int cmd) { + this(context, handler, cmdName, cmd, 0, 0, null); + } + + public WakeupMessage(Context context, Handler handler, String cmdName, Runnable runnable) { + mAlarmManager = getAlarmManager(context); + mHandler = handler; + mCmdName = cmdName; + mCmd = 0; + mArg1 = 0; + mArg2 = 0; + mObj = null; + mRunnable = runnable; + } + + private static AlarmManager getAlarmManager(Context context) { + return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + } + + /** + * Schedule the message to be delivered at the time in milliseconds of the + * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()} clock and wakeup + * the device when it goes off. If schedule is called multiple times without the message being + * dispatched then the alarm is rescheduled to the new time. + */ + public synchronized void schedule(long when) { + mAlarmManager.setExact( + AlarmManager.ELAPSED_REALTIME_WAKEUP, when, mCmdName, this, mHandler); + mScheduled = true; + } + + /** + * Cancel all pending messages. This includes alarms that may have been fired, but have not been + * run on the handler yet. + */ + public synchronized void cancel() { + if (mScheduled) { + mAlarmManager.cancel(this); + mScheduled = false; + } + } + + @Override + public void onAlarm() { + // Once this method is called the alarm has already been fired and removed from + // AlarmManager (it is still partially tracked, but only for statistics). The alarm can now + // be marked as unscheduled so that it can be rescheduled in the message handler. + final boolean stillScheduled; + synchronized (this) { + stillScheduled = mScheduled; + mScheduled = false; + } + if (stillScheduled) { + Message msg; + if (mRunnable == null) { + msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj); + } else { + msg = Message.obtain(mHandler, mRunnable); + } + mHandler.dispatchMessage(msg); + msg.recycle(); + } + } +}
\ No newline at end of file diff --git a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt index 36307277b4b9..6ec39d953266 100644 --- a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt +++ b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt @@ -1,5 +1,2 @@ -rule android.util.IndentingPrintWriter android.net.vcn.module.repackaged.android.util.IndentingPrintWriter rule android.util.LocalLog android.net.vcn.module.repackaged.android.util.LocalLog -rule com.android.internal.util.IndentingPrintWriter android.net.vcn.module.repackaged.com.android.internal.util.IndentingPrintWriter -rule com.android.internal.util.MessageUtils android.net.vcn.module.repackaged.com.android.internal.util.MessageUtils rule com.android.internal.util.WakeupMessage android.net.vcn.module.repackaged.com.android.internal.util.WakeupMessage
\ No newline at end of file diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto index 648990588d29..3a38152825c9 100644 --- a/proto/src/system_messages.proto +++ b/proto/src/system_messages.proto @@ -420,5 +420,9 @@ message SystemMessage { // Notify the user that accessibility floating menu is hidden. // Package: com.android.systemui NOTE_A11Y_FLOATING_MENU_HIDDEN = 1009; + + // Notify the hearing aid user that input device can be changed to builtin device or hearing device. + // Package: android + NOTE_HEARING_DEVICE_INPUT_SWITCH = 1012; } } diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index 59043a8356ae..8e998426685b 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -345,14 +345,41 @@ java_library { ], } +// We define our own version of platform_compat_config's here, because: +// - The original version (e.g. "framework-platform-compat-config) is built from +// the output file of the device side jar, rather than the host jar, meaning +// they're slow to build because they depend on D8/R8 output. +// - The original services one ("services-platform-compat-config") is built from services.jar, +// which includes service.permission, which is very slow to rebuild because of kotlin. +// +// Because we're re-defining the same compat-IDs that are defined elsewhere, +// they should all have `include_in_merged_xml: false`. Otherwise, generating +// merged_compat_config.xml would fail due to duplicate IDs. +// +// These module names must end with "compat-config" because these will be used as the filename, +// and at runtime, we only loads files that match `*compat-config.xml`. +platform_compat_config { + name: "ravenwood-framework-platform-compat-config", + src: ":framework-minus-apex-for-host", + include_in_merged_xml: false, + visibility: ["//visibility:private"], +} + +platform_compat_config { + name: "ravenwood-services.core-platform-compat-config", + src: ":services.core-for-host", + include_in_merged_xml: false, + visibility: ["//visibility:private"], +} + filegroup { name: "ravenwood-data", device_common_srcs: [ ":system-build.prop", ":framework-res", ":ravenwood-empty-res", - ":framework-platform-compat-config", - ":services-platform-compat-config", + ":ravenwood-framework-platform-compat-config", + ":ravenwood-services.core-platform-compat-config", "texts/ravenwood-build.prop", ], device_first_srcs: [ @@ -616,6 +643,10 @@ android_ravenwood_libgroup { "android.test.mock.ravenwood", "ravenwood-helper-runtime", "hoststubgen-helper-runtime.ravenwood", + + // Note, when we include other services.* jars, we'll need to add + // platform_compat_config for that module too. + // See ravenwood-services.core-platform-compat-config above. "services.core.ravenwood-jarjar", "services.fakes.ravenwood-jarjar", diff --git a/ravenwood/CleanSpec.mk b/ravenwood/CleanSpec.mk new file mode 100644 index 000000000000..50d2fab40326 --- /dev/null +++ b/ravenwood/CleanSpec.mk @@ -0,0 +1,45 @@ +# 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. +# + +# If you don't need to do a full clean build but would like to touch +# a file or delete some intermediate files, add a clean step to the end +# of the list. These steps will only be run once, if they haven't been +# run before. +# +# E.g.: +# $(call add-clean-step, touch -c external/sqlite/sqlite3.h) +# $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates) +# +# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with +# files that are missing or have been moved. +# +# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory. +# Use $(OUT_DIR) to refer to the "out" directory. +# +# If you need to re-do something that's already mentioned, just copy +# the command and add it to the bottom of the list. E.g., if a change +# that you made last week required touching a file and a change you +# made today requires touching the same file, just copy the old +# touch step and add it to the end of the list. +# +# ***************************************************************** +# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THE BANNER +# ***************************************************************** + +$(call add-clean-step, rm -rf $(OUT_DIR)/host/linux-x86/testcases/ravenwood-runtime) + +# ****************************************************************** +# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THIS BANNER +# ****************************************************************** diff --git a/ravenwood/tools/hoststubgen/scripts/dump-jar b/ravenwood/tools/hoststubgen/scripts/dump-jar index 87652451359d..998357b70dff 100755 --- a/ravenwood/tools/hoststubgen/scripts/dump-jar +++ b/ravenwood/tools/hoststubgen/scripts/dump-jar @@ -89,7 +89,7 @@ filter_output() { # - Some other transient lines # - Sometimes the javap shows mysterious warnings, so remove them too. # - # `/PATTERN-1/,/PATTERN-1/{//!d}` is a trick to delete lines between two patterns, without + # `/PATTERN-1/,/PATTERN-2/{//!d}` is a trick to delete lines between two patterns, without # the start and the end lines. sed -e 's/#[0-9][0-9]*/#x/g' \ -e 's/^\( *\)[0-9][0-9]*:/\1x:/' \ diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 6d8d7b768b91..cc704b2b32ed 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -27,7 +27,7 @@ import com.android.hoststubgen.filters.ImplicitOutputFilter import com.android.hoststubgen.filters.KeepNativeFilter import com.android.hoststubgen.filters.OutputFilter import com.android.hoststubgen.filters.SanitizationFilter -import com.android.hoststubgen.filters.TextFileFilterPolicyParser +import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder import com.android.hoststubgen.filters.printAsTextPolicy import com.android.hoststubgen.utils.ClassFilter import com.android.hoststubgen.visitors.BaseAdapter @@ -179,9 +179,9 @@ class HostStubGen(val options: HostStubGenOptions) { // Next, "text based" filter, which allows to override polices without touching // the target code. if (options.policyOverrideFiles.isNotEmpty()) { - val parser = TextFileFilterPolicyParser(allClasses, filter) - options.policyOverrideFiles.forEach(parser::parse) - filter = parser.createOutputFilter() + val builder = TextFileFilterPolicyBuilder(allClasses, filter) + options.policyOverrideFiles.forEach(builder::parse) + filter = builder.createOutputFilter() } // Apply the implicit filter. diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt index 7462a8ce12c5..be1b6ca93869 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt @@ -23,10 +23,12 @@ import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.log import com.android.hoststubgen.normalizeTextLine import com.android.hoststubgen.whitespaceRegex -import java.io.File +import org.objectweb.asm.tree.ClassNode +import java.io.BufferedReader +import java.io.FileReader import java.io.PrintWriter +import java.io.Reader import java.util.regex.Pattern -import org.objectweb.asm.tree.ClassNode /** * Print a class node as a "keep" policy. @@ -48,7 +50,7 @@ fun printAsTextPolicy(pw: PrintWriter, cn: ClassNode) { private const val FILTER_REASON = "file-override" -private enum class SpecialClass { +enum class SpecialClass { NotSpecial, Aidl, FeatureFlags, @@ -56,10 +58,58 @@ private enum class SpecialClass { RFile, } -class TextFileFilterPolicyParser( +/** + * This receives [TextFileFilterPolicyBuilder] parsing result. + */ +interface PolicyFileProcessor { + /** "package" directive. */ + fun onPackage(name: String, policy: FilterPolicyWithReason) + + /** "rename" directive. */ + fun onRename(pattern: Pattern, prefix: String) + + /** "class" directive. */ + fun onSimpleClassStart(className: String) + fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason) + fun onSimpleClassEnd(className: String) + + fun onSubClassPolicy(superClassName: String, policy: FilterPolicyWithReason) + fun onRedirectionClass(fromClassName: String, toClassName: String) + fun onClassLoadHook(className: String, callback: String) + fun onSpecialClassPolicy(type: SpecialClass, policy: FilterPolicyWithReason) + + /** "field" directive. */ + fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason) + + /** "method" directive. */ + fun onSimpleMethodPolicy( + className: String, + methodName: String, + methodDesc: String, + policy: FilterPolicyWithReason, + ) + fun onMethodInClassReplace( + className: String, + methodName: String, + methodDesc: String, + targetName: String, + policy: FilterPolicyWithReason, + ) + fun onMethodOutClassReplace( + className: String, + methodName: String, + methodDesc: String, + replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + policy: FilterPolicyWithReason, + ) +} + +class TextFileFilterPolicyBuilder( private val classes: ClassNodes, fallback: OutputFilter ) { + private val parser = TextFileFilterPolicyParser() + private val subclassFilter = SubclassFilter(classes, fallback) private val packageFilter = PackageFilter(subclassFilter) private val imf = InMemoryOutputFilter(classes, packageFilter) @@ -71,30 +121,19 @@ class TextFileFilterPolicyParser( private val methodReplaceSpec = mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>() - private lateinit var currentClassName: String - /** - * Read a given "policy" file and return as an [OutputFilter] + * Parse a given policy file. This method can be called multiple times to read from + * multiple files. To get the resulting filter, use [createOutputFilter] */ fun parse(file: String) { - log.i("Loading offloaded annotations from $file ...") - log.withIndent { - var lineNo = 0 - try { - File(file).forEachLine { - lineNo++ - val line = normalizeTextLine(it) - if (line.isEmpty()) { - return@forEachLine // skip empty lines. - } - parseLine(line) - } - } catch (e: ParseException) { - throw e.withSourceInfo(file, lineNo) - } - } + // We may parse multiple files, but we reuse the same parser, because the parser + // will make sure there'll be no dupplicating "special class" policies. + parser.parse(FileReader(file), file, Processor()) } + /** + * Generate the resulting [OutputFilter]. + */ fun createOutputFilter(): OutputFilter { var ret: OutputFilter = imf if (typeRenameSpec.isNotEmpty()) { @@ -112,14 +151,200 @@ class TextFileFilterPolicyParser( return ret } + private inner class Processor : PolicyFileProcessor { + override fun onPackage(name: String, policy: FilterPolicyWithReason) { + packageFilter.addPolicy(name, policy) + } + + override fun onRename(pattern: Pattern, prefix: String) { + typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec( + pattern, prefix + ) + } + + override fun onSimpleClassStart(className: String) { + } + + override fun onSimpleClassEnd(className: String) { + } + + override fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason) { + imf.setPolicyForClass(className, policy) + } + + override fun onSubClassPolicy( + superClassName: String, + policy: FilterPolicyWithReason, + ) { + log.i("class extends $superClassName") + subclassFilter.addPolicy( superClassName, policy) + } + + override fun onRedirectionClass(fromClassName: String, toClassName: String) { + imf.setRedirectionClass(fromClassName, toClassName) + } + + override fun onClassLoadHook(className: String, callback: String) { + imf.setClassLoadHook(className, callback) + } + + override fun onSpecialClassPolicy( + type: SpecialClass, + policy: FilterPolicyWithReason, + ) { + log.i("class special $type $policy") + when (type) { + SpecialClass.NotSpecial -> {} // Shouldn't happen + + SpecialClass.Aidl -> { + aidlPolicy = policy + } + + SpecialClass.FeatureFlags -> { + featureFlagsPolicy = policy + } + + SpecialClass.Sysprops -> { + syspropsPolicy = policy + } + + SpecialClass.RFile -> { + rFilePolicy = policy + } + } + } + + override fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason) { + imf.setPolicyForField(className, fieldName, policy) + } + + override fun onSimpleMethodPolicy( + className: String, + methodName: String, + methodDesc: String, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + } + + override fun onMethodInClassReplace( + className: String, + methodName: String, + methodDesc: String, + targetName: String, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + + // Make sure to keep the target method. + imf.setPolicyForMethod( + className, + targetName, + methodDesc, + FilterPolicy.Keep.withReason(FILTER_REASON) + ) + // Set up the rename. + imf.setRenameTo(className, targetName, methodDesc, methodName) + } + + override fun onMethodOutClassReplace( + className: String, + methodName: String, + methodDesc: String, + replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + methodReplaceSpec.add(replaceSpec) + } + } +} + +/** + * Parses a filer policy text file. + */ +class TextFileFilterPolicyParser { + private lateinit var processor: PolicyFileProcessor + private var currentClassName: String? = null + + private var aidlPolicy: FilterPolicyWithReason? = null + private var featureFlagsPolicy: FilterPolicyWithReason? = null + private var syspropsPolicy: FilterPolicyWithReason? = null + private var rFilePolicy: FilterPolicyWithReason? = null + + /** Name of the file that's currently being processed. */ + var filename: String? = null + private set + + /** 1-based line number in the current file */ + var lineNumber = -1 + private set + + /** + * Parse a given "policy" file. + */ + fun parse(reader: Reader, inputName: String, processor: PolicyFileProcessor) { + filename = inputName + + log.i("Parsing text policy file $inputName ...") + this.processor = processor + BufferedReader(reader).use { rd -> + lineNumber = 0 + try { + while (true) { + var line = rd.readLine() + if (line == null) { + break + } + lineNumber++ + line = normalizeTextLine(line) // Remove comment and trim. + if (line.isEmpty()) { + continue + } + parseLine(line) + } + finishLastClass() + } catch (e: ParseException) { + throw e.withSourceInfo(inputName, lineNumber) + } + } + } + + private fun finishLastClass() { + currentClassName?.let { className -> + processor.onSimpleClassEnd(className) + currentClassName = null + } + } + + private fun ensureInClass(directive: String): String { + return currentClassName ?: + throw ParseException("Directive '$directive' must follow a 'class' directive") + } + private fun parseLine(line: String) { val fields = line.split(whitespaceRegex).toTypedArray() when (fields[0].lowercase()) { - "p", "package" -> parsePackage(fields) - "c", "class" -> parseClass(fields) - "f", "field" -> parseField(fields) - "m", "method" -> parseMethod(fields) - "r", "rename" -> parseRename(fields) + "p", "package" -> { + finishLastClass() + parsePackage(fields) + } + "c", "class" -> { + finishLastClass() + parseClass(fields) + } + "f", "field" -> { + ensureInClass("field") + parseField(fields) + } + "m", "method" -> { + ensureInClass("method") + parseMethod(fields) + } + "r", "rename" -> { + finishLastClass() + parseRename(fields) + } else -> throw ParseException("Unknown directive \"${fields[0]}\"") } } @@ -184,20 +409,20 @@ class TextFileFilterPolicyParser( if (!policy.isUsableWithClasses) { throw ParseException("Package can't have policy '$policy'") } - packageFilter.addPolicy(name, policy.withReason(FILTER_REASON)) + processor.onPackage(name, policy.withReason(FILTER_REASON)) } private fun parseClass(fields: Array<String>) { if (fields.size < 3) { throw ParseException("Class ('c') expects 2 fields.") } - currentClassName = fields[1] + val className = fields[1] // superClass is set when the class name starts with a "*". - val superClass = resolveExtendingClass(currentClassName) + val superClass = resolveExtendingClass(className) // :aidl, etc? - val classType = resolveSpecialClass(currentClassName) + val classType = resolveSpecialClass(className) if (fields[2].startsWith("!")) { if (classType != SpecialClass.NotSpecial) { @@ -208,7 +433,8 @@ class TextFileFilterPolicyParser( } // It's a redirection class. val toClass = fields[2].substring(1) - imf.setRedirectionClass(currentClassName, toClass) + + processor.onRedirectionClass(className, toClass) } else if (fields[2].startsWith("~")) { if (classType != SpecialClass.NotSpecial) { // We could support it, but not needed at least for now. @@ -218,7 +444,8 @@ class TextFileFilterPolicyParser( } // It's a class-load hook val callback = fields[2].substring(1) - imf.setClassLoadHook(currentClassName, callback) + + processor.onClassLoadHook(className, callback) } else { val policy = parsePolicy(fields[2]) if (!policy.isUsableWithClasses) { @@ -229,26 +456,27 @@ class TextFileFilterPolicyParser( SpecialClass.NotSpecial -> { // TODO: Duplicate check, etc if (superClass == null) { - imf.setPolicyForClass( - currentClassName, policy.withReason(FILTER_REASON) - ) + currentClassName = className + processor.onSimpleClassStart(className) + processor.onSimpleClassPolicy(className, policy.withReason(FILTER_REASON)) } else { - subclassFilter.addPolicy( + processor.onSubClassPolicy( superClass, - policy.withReason("extends $superClass") + policy.withReason("extends $superClass"), ) } } - SpecialClass.Aidl -> { if (aidlPolicy != null) { throw ParseException( "Policy for AIDL classes already defined" ) } - aidlPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class AIDL)" ) + processor.onSpecialClassPolicy(classType, p) + aidlPolicy = p } SpecialClass.FeatureFlags -> { @@ -257,9 +485,11 @@ class TextFileFilterPolicyParser( "Policy for feature flags already defined" ) } - featureFlagsPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class feature flags)" ) + processor.onSpecialClassPolicy(classType, p) + featureFlagsPolicy = p } SpecialClass.Sysprops -> { @@ -268,9 +498,11 @@ class TextFileFilterPolicyParser( "Policy for sysprops already defined" ) } - syspropsPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class sysprops)" ) + processor.onSpecialClassPolicy(classType, p) + syspropsPolicy = p } SpecialClass.RFile -> { @@ -279,9 +511,11 @@ class TextFileFilterPolicyParser( "Policy for R file already defined" ) } - rFilePolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class R file)" ) + processor.onSpecialClassPolicy(classType, p) + rFilePolicy = p } } } @@ -296,17 +530,16 @@ class TextFileFilterPolicyParser( if (!policy.isUsableWithFields) { throw ParseException("Field can't have policy '$policy'") } - require(this::currentClassName.isInitialized) // TODO: Duplicate check, etc - imf.setPolicyForField(currentClassName, name, policy.withReason(FILTER_REASON)) + processor.onField(currentClassName!!, name, policy.withReason(FILTER_REASON)) } private fun parseMethod(fields: Array<String>) { if (fields.size < 3 || fields.size > 4) { throw ParseException("Method ('m') expects 3 or 4 fields.") } - val name = fields[1] + val methodName = fields[1] val signature: String val policyStr: String if (fields.size <= 3) { @@ -323,44 +556,48 @@ class TextFileFilterPolicyParser( throw ParseException("Method can't have policy '$policy'") } - require(this::currentClassName.isInitialized) + val className = currentClassName!! - imf.setPolicyForMethod( - currentClassName, name, signature, - policy.withReason(FILTER_REASON) - ) - if (policy == FilterPolicy.Substitute) { - val fromName = policyStr.substring(1) + val policyWithReason = policy.withReason(FILTER_REASON) + if (policy != FilterPolicy.Substitute) { + processor.onSimpleMethodPolicy(className, methodName, signature, policyWithReason) + } else { + val targetName = policyStr.substring(1) - if (fromName == name) { + if (targetName == methodName) { throw ParseException( "Substitution must have a different name" ) } - // Set the policy for the "from" method. - imf.setPolicyForMethod( - currentClassName, fromName, signature, - FilterPolicy.Keep.withReason(FILTER_REASON) - ) - - val classAndMethod = splitWithLastPeriod(fromName) + val classAndMethod = splitWithLastPeriod(targetName) if (classAndMethod != null) { // If the substitution target contains a ".", then // it's a method call redirect. - methodReplaceSpec.add( - TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec( - currentClassName.toJvmClassName(), - name, + val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec( + currentClassName!!.toJvmClassName(), + methodName, signature, classAndMethod.first.toJvmClassName(), classAndMethod.second, ) + processor.onMethodOutClassReplace( + className, + methodName, + signature, + spec, + policyWithReason, ) } else { // It's an in-class replace. // ("@RavenwoodReplace" equivalent) - imf.setRenameTo(currentClassName, fromName, signature, name) + processor.onMethodInClassReplace( + className, + methodName, + signature, + targetName, + policyWithReason, + ) } } } @@ -378,7 +615,7 @@ class TextFileFilterPolicyParser( // applied. (Which is needed for services.jar) val prefix = fields[2].trimStart('/') - typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec( + processor.onRename( pattern, prefix ) } diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh index b389a67a8e4c..8408a18142eb 100755 --- a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh +++ b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh @@ -34,10 +34,11 @@ source "${0%/*}"/../common.sh SCRIPT_NAME="${0##*/}" -GOLDEN_DIR=golden-output +GOLDEN_DIR=${GOLDEN_DIR:-golden-output} mkdir -p $GOLDEN_DIR -DIFF_CMD=${DIFF:-diff -u --ignore-blank-lines --ignore-space-change} +# TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines. +DIFF_CMD=${DIFF_CMD:-diff -u --ignore-blank-lines --ignore-space-change --ignore-matching-lines='^\(Constant.pool:\|{\)$'} update=0 three_way=0 @@ -62,7 +63,7 @@ done shift $(($OPTIND - 1)) # Build the dump files, which are the input of this test. -run m dump-jar tiny-framework-dump-test +run ${BUILD_CMD:=m} dump-jar tiny-framework-dump-test # Get the path to the generate text files. (not the golden files.) diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py index 88fa492addb8..c35d6d106c8d 100755 --- a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py +++ b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py @@ -28,8 +28,11 @@ GOLDEN_DIRS = [ # Run diff. def run_diff(file1, file2): + # TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines. command = ['diff', '-u', '--ignore-blank-lines', - '--ignore-space-change', file1, file2] + '--ignore-space-change', + '--ignore-matching-lines=^\(Constant.pool:\|{\)$', + file1, file2] print(' '.join(command)) result = subprocess.run(command, stderr=sys.stdout) diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 37d045bf6422..8e037c3ba90c 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -413,6 +413,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private SparseArray<SurfaceControl> mA11yOverlayLayers = new SparseArray<>(); private final FlashNotificationsController mFlashNotificationsController; + private final HearingDevicePhoneCallNotificationController mHearingDeviceNotificationController; private final UserManagerInternal mUmi; private AccessibilityUserState getCurrentUserStateLocked() { @@ -541,7 +542,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub MagnificationController magnificationController, @Nullable AccessibilityInputFilter inputFilter, ProxyManager proxyManager, - PermissionEnforcer permissionEnforcer) { + PermissionEnforcer permissionEnforcer, + HearingDevicePhoneCallNotificationController hearingDeviceNotificationController) { super(permissionEnforcer); mContext = context; mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); @@ -569,6 +571,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub // TODO(b/255426725): not used on tests mVisibleBgUserIds = null; mInputManager = context.getSystemService(InputManager.class); + if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { + mHearingDeviceNotificationController = hearingDeviceNotificationController; + } else { + mHearingDeviceNotificationController = null; + } init(); } @@ -618,6 +625,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } else { mVisibleBgUserIds = null; } + if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { + mHearingDeviceNotificationController = new HearingDevicePhoneCallNotificationController( + context); + } else { + mHearingDeviceNotificationController = null; + } init(); } @@ -630,6 +643,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (enableTalkbackAndMagnifierKeyGestures()) { mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler); } + if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { + if (mHearingDeviceNotificationController != null) { + mHearingDeviceNotificationController.startListenForCallState(); + } + } disableAccessibilityMenuToMigrateIfNeeded(); } diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java index e3d7062ddb4e..b94fa2f59162 100644 --- a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java @@ -22,6 +22,7 @@ import static com.android.server.accessibility.AutoclickIndicatorView.SHOW_INDIC import android.accessibilityservice.AccessibilityTrace; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -69,7 +70,7 @@ public class AutoclickController extends BaseEventStreamTransformation { // Lazily created on the first mouse motion event. private ClickScheduler mClickScheduler; - private ClickDelayObserver mClickDelayObserver; + private AutoclickSettingsObserver mAutoclickSettingsObserver; private AutoclickIndicatorScheduler mAutoclickIndicatorScheduler; private AutoclickIndicatorView mAutoclickIndicatorView; private WindowManager mWindowManager; @@ -89,14 +90,17 @@ public class AutoclickController extends BaseEventStreamTransformation { if (event.isFromSource(InputDevice.SOURCE_MOUSE)) { if (mClickScheduler == null) { Handler handler = new Handler(mContext.getMainLooper()); - mClickScheduler = - new ClickScheduler(handler, AccessibilityManager.AUTOCLICK_DELAY_DEFAULT); - mClickDelayObserver = new ClickDelayObserver(mUserId, handler); - mClickDelayObserver.start(mContext.getContentResolver(), mClickScheduler); - if (Flags.enableAutoclickIndicator()) { initiateAutoclickIndicator(handler); } + + mClickScheduler = + new ClickScheduler(handler, AccessibilityManager.AUTOCLICK_DELAY_DEFAULT); + mAutoclickSettingsObserver = new AutoclickSettingsObserver(mUserId, handler); + mAutoclickSettingsObserver.start( + mContext.getContentResolver(), + mClickScheduler, + mAutoclickIndicatorScheduler); } handleMouseMotion(event, policyFlags); @@ -156,9 +160,9 @@ public class AutoclickController extends BaseEventStreamTransformation { @Override public void onDestroy() { - if (mClickDelayObserver != null) { - mClickDelayObserver.stop(); - mClickDelayObserver = null; + if (mAutoclickSettingsObserver != null) { + mAutoclickSettingsObserver.stop(); + mAutoclickSettingsObserver = null; } if (mClickScheduler != null) { mClickScheduler.cancel(); @@ -191,19 +195,24 @@ public class AutoclickController extends BaseEventStreamTransformation { } /** - * Observes setting value for autoclick delay, and updates ClickScheduler delay whenever the - * setting value changes. + * Observes autoclick setting values, and updates ClickScheduler delay and indicator size + * whenever the setting value changes. */ - final private static class ClickDelayObserver extends ContentObserver { + final private static class AutoclickSettingsObserver extends ContentObserver { /** URI used to identify the autoclick delay setting with content resolver. */ private final Uri mAutoclickDelaySettingUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY); + /** URI used to identify the autoclick cursor area size setting with content resolver. */ + private final Uri mAutoclickCursorAreaSizeSettingUri = + Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE); + private ContentResolver mContentResolver; private ClickScheduler mClickScheduler; + private AutoclickIndicatorScheduler mAutoclickIndicatorScheduler; private final int mUserId; - public ClickDelayObserver(int userId, Handler handler) { + public AutoclickSettingsObserver(int userId, Handler handler) { super(handler); mUserId = userId; } @@ -216,11 +225,13 @@ public class AutoclickController extends BaseEventStreamTransformation { * changes. * @param clickScheduler ClickScheduler that should be updated when click delay changes. * @throws IllegalStateException If internal state is already setup when the method is - * called. + * called. * @throws NullPointerException If any of the arguments is a null pointer. */ - public void start(@NonNull ContentResolver contentResolver, - @NonNull ClickScheduler clickScheduler) { + public void start( + @NonNull ContentResolver contentResolver, + @NonNull ClickScheduler clickScheduler, + @Nullable AutoclickIndicatorScheduler autoclickIndicatorScheduler) { if (mContentResolver != null || mClickScheduler != null) { throw new IllegalStateException("Observer already started."); } @@ -233,11 +244,20 @@ public class AutoclickController extends BaseEventStreamTransformation { mContentResolver = contentResolver; mClickScheduler = clickScheduler; + mAutoclickIndicatorScheduler = autoclickIndicatorScheduler; mContentResolver.registerContentObserver(mAutoclickDelaySettingUri, false, this, mUserId); // Initialize mClickScheduler's initial delay value. onChange(true, mAutoclickDelaySettingUri); + + if (Flags.enableAutoclickIndicator()) { + // Register observer to listen to cursor area size setting change. + mContentResolver.registerContentObserver( + mAutoclickCursorAreaSizeSettingUri, false, this, mUserId); + // Initialize mAutoclickIndicatorView's initial size. + onChange(true, mAutoclickCursorAreaSizeSettingUri); + } } /** @@ -248,7 +268,7 @@ public class AutoclickController extends BaseEventStreamTransformation { */ public void stop() { if (mContentResolver == null || mClickScheduler == null) { - throw new IllegalStateException("ClickDelayObserver not started."); + throw new IllegalStateException("AutoclickSettingsObserver not started."); } mContentResolver.unregisterContentObserver(this); @@ -262,6 +282,18 @@ public class AutoclickController extends BaseEventStreamTransformation { AccessibilityManager.AUTOCLICK_DELAY_DEFAULT, mUserId); mClickScheduler.updateDelay(delay); } + if (Flags.enableAutoclickIndicator() + && mAutoclickCursorAreaSizeSettingUri.equals(uri)) { + int size = + Settings.Secure.getIntForUser( + mContentResolver, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE, + AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT, + mUserId); + if (mAutoclickIndicatorScheduler != null) { + mAutoclickIndicatorScheduler.updateCursorAreaSize(size); + } + } } } @@ -317,6 +349,10 @@ public class AutoclickController extends BaseEventStreamTransformation { mScheduledShowIndicatorTime = -1; mHandler.removeCallbacks(this); } + + public void updateCursorAreaSize(int size) { + mAutoclickIndicatorView.setRadius(size); + } } /** diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java index 816d8e456a9a..bf5015176f8c 100644 --- a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java +++ b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java @@ -16,6 +16,8 @@ package com.android.server.accessibility; +import static android.view.accessibility.AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT; + import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; @@ -35,8 +37,7 @@ public class AutoclickIndicatorView extends View { static final int MINIMAL_ANIMATION_DURATION = 50; - // TODO(b/383901288): allow users to customize the indicator area. - static final float RADIUS = 50; + private float mRadius = AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT; private final Paint mPaint; @@ -84,10 +85,10 @@ public class AutoclickIndicatorView extends View { if (showIndicator) { mRingRect.set( - /* left= */ mX - RADIUS, - /* top= */ mY - RADIUS, - /* right= */ mX + RADIUS, - /* bottom= */ mY + RADIUS); + /* left= */ mX - mRadius, + /* top= */ mY - mRadius, + /* right= */ mX + mRadius, + /* bottom= */ mY + mRadius); canvas.drawArc(mRingRect, /* startAngle= */ -90, mSweepAngle, false, mPaint); } } @@ -107,6 +108,10 @@ public class AutoclickIndicatorView extends View { mY = y; } + public void setRadius(int radius) { + mRadius = radius; + } + public void redrawIndicator() { showIndicator = true; invalidate(); diff --git a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java new file mode 100644 index 000000000000..d06daf5db127 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java @@ -0,0 +1,356 @@ +/* + * 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.accessibility; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothUuid; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.MediaRecorder; +import android.os.Bundle; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.R; +import com.android.internal.messages.nano.SystemMessageProto; +import com.android.internal.notification.SystemNotificationChannels; +import com.android.internal.util.ArrayUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +/** + * A controller class to handle notification for hearing device during phone calls. + */ +public class HearingDevicePhoneCallNotificationController { + + private final TelephonyManager mTelephonyManager; + private final TelephonyCallback mTelephonyListener; + private final Executor mCallbackExecutor; + + public HearingDevicePhoneCallNotificationController(@NonNull Context context) { + mTelephonyListener = new CallStateListener(context); + mTelephonyManager = context.getSystemService(TelephonyManager.class); + mCallbackExecutor = Executors.newSingleThreadExecutor(); + } + + @VisibleForTesting + HearingDevicePhoneCallNotificationController(@NonNull Context context, + TelephonyCallback telephonyCallback) { + mTelephonyListener = telephonyCallback; + mTelephonyManager = context.getSystemService(TelephonyManager.class); + mCallbackExecutor = context.getMainExecutor(); + } + + /** + * Registers a telephony callback to listen for call state changed to handle notification for + * hearing device during phone calls. + */ + public void startListenForCallState() { + mTelephonyManager.registerTelephonyCallback(mCallbackExecutor, mTelephonyListener); + } + + /** + * A telephony callback listener to listen to call state changes and show/dismiss notification + */ + @VisibleForTesting + static class CallStateListener extends TelephonyCallback implements + TelephonyCallback.CallStateListener { + + private static final String TAG = + "HearingDevice_CallStateListener"; + private static final String ACTION_SWITCH_TO_BUILTIN_MIC = + "com.android.server.accessibility.hearingdevice.action.SWITCH_TO_BUILTIN_MIC"; + private static final String ACTION_SWITCH_TO_HEARING_MIC = + "com.android.server.accessibility.hearingdevice.action.SWITCH_TO_HEARING_MIC"; + private static final String ACTION_BLUETOOTH_DEVICE_DETAILS = + "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"; + private static final String KEY_BLUETOOTH_ADDRESS = "device_address"; + private static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"; + private static final int MICROPHONE_SOURCE_VOICE_COMMUNICATION = + MediaRecorder.AudioSource.VOICE_COMMUNICATION; + private static final AudioDeviceAttributes BUILTIN_MIC = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_BUILTIN_MIC, ""); + + private final Context mContext; + private NotificationManager mNotificationManager; + private AudioManager mAudioManager; + private BroadcastReceiver mHearingDeviceActionReceiver; + private BluetoothDevice mHearingDevice; + private boolean mIsNotificationShown = false; + + CallStateListener(@NonNull Context context) { + mContext = context; + } + + @Override + @SuppressLint("AndroidFrameworkRequiresPermission") + public void onCallStateChanged(int state) { + // NotificationManagerService and AudioService are all initialized after + // AccessibilityManagerService. + // Can not get them in constructor. Need to get these services until callback is + // triggered. + mNotificationManager = mContext.getSystemService(NotificationManager.class); + mAudioManager = mContext.getSystemService(AudioManager.class); + if (mNotificationManager == null || mAudioManager == null) { + Log.w(TAG, "NotificationManager or AudioManager is not prepare yet."); + return; + } + + if (state == TelephonyManager.CALL_STATE_IDLE) { + dismissNotificationIfNeeded(); + + if (mHearingDevice != null) { + // reset to its original status + setMicrophonePreferredForCalls(mHearingDevice.isMicrophonePreferredForCalls()); + } + mHearingDevice = null; + } + if (state == TelephonyManager.CALL_STATE_OFFHOOK) { + mHearingDevice = getSupportedInputHearingDeviceInfo( + mAudioManager.getAvailableCommunicationDevices()); + if (mHearingDevice != null) { + showNotificationIfNeeded(); + } + } + } + + private void showNotificationIfNeeded() { + if (mIsNotificationShown) { + return; + } + + showNotification(mHearingDevice.isMicrophonePreferredForCalls()); + mIsNotificationShown = true; + } + + private void dismissNotificationIfNeeded() { + if (!mIsNotificationShown) { + return; + } + + dismissNotification(); + mIsNotificationShown = false; + } + + private void showNotification(boolean useRemoteMicrophone) { + mNotificationManager.notify( + SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH, + createSwitchInputNotification(useRemoteMicrophone)); + registerReceiverIfNeeded(); + } + + private void dismissNotification() { + unregisterReceiverIfNeeded(); + mNotificationManager.cancel( + SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH); + } + + private BluetoothDevice getSupportedInputHearingDeviceInfo(List<AudioDeviceInfo> infoList) { + final BluetoothAdapter bluetoothAdapter = mContext.getSystemService( + BluetoothManager.class).getAdapter(); + if (bluetoothAdapter == null) { + return null; + } + if (!isHapClientSupported()) { + return null; + } + + final Set<String> inputDeviceAddress = Arrays.stream( + mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).map( + AudioDeviceInfo::getAddress).collect(Collectors.toSet()); + + //TODO: b/370812132 - Need to update if TYPE_LEA_HEARING_AID is added + final AudioDeviceInfo hearingDeviceInfo = infoList.stream() + .filter(info -> info.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) + .filter(info -> inputDeviceAddress.contains(info.getAddress())) + .filter(info -> isHapClientDevice(bluetoothAdapter, info)) + .findAny() + .orElse(null); + + return (hearingDeviceInfo != null) ? bluetoothAdapter.getRemoteDevice( + hearingDeviceInfo.getAddress()) : null; + } + + @VisibleForTesting + boolean isHapClientDevice(BluetoothAdapter bluetoothAdapter, AudioDeviceInfo info) { + BluetoothDevice device = bluetoothAdapter.getRemoteDevice(info.getAddress()); + return ArrayUtils.contains(device.getUuids(), BluetoothUuid.HAS); + } + + @VisibleForTesting + boolean isHapClientSupported() { + return BluetoothAdapter.getDefaultAdapter().getSupportedProfiles().contains( + BluetoothProfile.HAP_CLIENT); + } + + private Notification createSwitchInputNotification(boolean useRemoteMicrophone) { + return new Notification.Builder(mContext, + SystemNotificationChannels.ACCESSIBILITY_HEARING_DEVICE) + .setContentTitle(getSwitchInputTitle(useRemoteMicrophone)) + .setContentText(getSwitchInputMessage(useRemoteMicrophone)) + .setSmallIcon(R.drawable.ic_settings_24dp) + .setColor(mContext.getResources().getColor( + com.android.internal.R.color.system_notification_accent_color)) + .setLocalOnly(true) + .setCategory(Notification.CATEGORY_SYSTEM) + .setContentIntent(createPendingIntent(ACTION_BLUETOOTH_DEVICE_DETAILS)) + .setActions(buildSwitchInputAction(useRemoteMicrophone), + buildOpenSettingsAction()) + .build(); + } + + private Notification.Action buildSwitchInputAction(boolean useRemoteMicrophone) { + return useRemoteMicrophone + ? new Notification.Action.Builder(null, + mContext.getString(R.string.hearing_device_notification_switch_button), + createPendingIntent(ACTION_SWITCH_TO_BUILTIN_MIC)).build() + : new Notification.Action.Builder(null, + mContext.getString(R.string.hearing_device_notification_switch_button), + createPendingIntent(ACTION_SWITCH_TO_HEARING_MIC)).build(); + } + + private Notification.Action buildOpenSettingsAction() { + return new Notification.Action.Builder(null, + mContext.getString(R.string.hearing_device_notification_settings_button), + createPendingIntent(ACTION_BLUETOOTH_DEVICE_DETAILS)).build(); + } + + private PendingIntent createPendingIntent(String action) { + final Intent intent = new Intent(action); + + switch (action) { + case ACTION_SWITCH_TO_BUILTIN_MIC, ACTION_SWITCH_TO_HEARING_MIC -> { + intent.setPackage(mContext.getPackageName()); + return PendingIntent.getBroadcast(mContext, /* requestCode = */ 0, intent, + PendingIntent.FLAG_IMMUTABLE); + } + case ACTION_BLUETOOTH_DEVICE_DETAILS -> { + Bundle bundle = new Bundle(); + bundle.putString(KEY_BLUETOOTH_ADDRESS, mHearingDevice.getAddress()); + intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle); + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + return PendingIntent.getActivity(mContext, /* requestCode = */ 0, intent, + PendingIntent.FLAG_IMMUTABLE); + } + } + return null; + } + + private void setMicrophonePreferredForCalls(boolean useRemoteMicrophone) { + if (useRemoteMicrophone) { + switchToHearingMic(); + } else { + switchToBuiltinMic(); + } + } + + @SuppressLint("AndroidFrameworkRequiresPermission") + private void switchToBuiltinMic() { + mAudioManager.clearPreferredDevicesForCapturePreset( + MICROPHONE_SOURCE_VOICE_COMMUNICATION); + mAudioManager.setPreferredDeviceForCapturePreset(MICROPHONE_SOURCE_VOICE_COMMUNICATION, + BUILTIN_MIC); + } + + @SuppressLint("AndroidFrameworkRequiresPermission") + private void switchToHearingMic() { + // clear config to let audio manager to determine next priority device. We can assume + // user connects to hearing device here, so next priority device should be hearing + // device. + mAudioManager.clearPreferredDevicesForCapturePreset( + MICROPHONE_SOURCE_VOICE_COMMUNICATION); + } + + private void registerReceiverIfNeeded() { + if (mHearingDeviceActionReceiver != null) { + return; + } + mHearingDeviceActionReceiver = new HearingDeviceActionReceiver(); + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_SWITCH_TO_BUILTIN_MIC); + intentFilter.addAction(ACTION_SWITCH_TO_HEARING_MIC); + mContext.registerReceiver(mHearingDeviceActionReceiver, intentFilter, + Manifest.permission.MANAGE_ACCESSIBILITY, null, Context.RECEIVER_NOT_EXPORTED); + } + + private void unregisterReceiverIfNeeded() { + if (mHearingDeviceActionReceiver == null) { + return; + } + mContext.unregisterReceiver(mHearingDeviceActionReceiver); + mHearingDeviceActionReceiver = null; + } + + private CharSequence getSwitchInputTitle(boolean useRemoteMicrophone) { + return useRemoteMicrophone + ? mContext.getString( + R.string.hearing_device_switch_phone_mic_notification_title) + : mContext.getString( + R.string.hearing_device_switch_hearing_mic_notification_title); + } + + private CharSequence getSwitchInputMessage(boolean useRemoteMicrophone) { + return useRemoteMicrophone + ? mContext.getString( + R.string.hearing_device_switch_phone_mic_notification_text) + : mContext.getString( + R.string.hearing_device_switch_hearing_mic_notification_text); + } + + private class HearingDeviceActionReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (TextUtils.isEmpty(action)) { + return; + } + + if (ACTION_SWITCH_TO_BUILTIN_MIC.equals(action)) { + switchToBuiltinMic(); + showNotification(/* useRemoteMicrophone= */ false); + } else if (ACTION_SWITCH_TO_HEARING_MIC.equals(action)) { + switchToHearingMic(); + showNotification(/* useRemoteMicrophone= */ true); + } + } + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java index f15b8eec3f6b..cd46b38272c2 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java @@ -38,7 +38,7 @@ public class TouchState { // Pointer-related constants // This constant captures the current implementation detail that // pointer IDs are between 0 and 31 inclusive (subject to change). - // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) + // (See MAX_POINTER_ID in frameworks/native/include/input/Input.h) public static final int MAX_POINTER_COUNT = 32; // Constant referring to the ids bits of all pointers. public static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF; diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index 57d33f1a051e..43764442e2cf 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -50,7 +50,10 @@ import android.app.appsearch.observer.ObserverSpec; import android.app.appsearch.observer.SchemaChangeInfo; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; +import android.content.pm.SigningInfo; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; @@ -292,7 +295,8 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { safeExecuteAppFunctionCallback, /* bindFlags= */ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, - callerBinder); + callerBinder, + callingUid); }) .exceptionally( ex -> { @@ -444,7 +448,8 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { @NonNull ICancellationSignal cancellationSignalTransport, @NonNull SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback, int bindFlags, - @NonNull IBinder callerBinder) { + @NonNull IBinder callerBinder, + int callingUid) { CancellationSignal cancellationSignal = CancellationSignal.fromTransport(cancellationSignalTransport); ICancellationCallback cancellationCallback = @@ -465,7 +470,11 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { new RunAppFunctionServiceCallback( requestInternal, cancellationCallback, - safeExecuteAppFunctionCallback), + safeExecuteAppFunctionCallback, + getPackageSigningInfo( + targetUser, + requestInternal.getCallingPackage(), + callingUid)), callerBinder); if (!bindServiceResult) { @@ -477,6 +486,23 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { } } + @NonNull + private SigningInfo getPackageSigningInfo( + @NonNull UserHandle targetUser, @NonNull String packageName, int uid) { + Objects.requireNonNull(packageName); + Objects.requireNonNull(targetUser); + + PackageInfo packageInfo; + packageInfo = + Objects.requireNonNull( + mPackageManagerInternal.getPackageInfo( + packageName, + PackageManager.GET_SIGNING_CERTIFICATES, + uid, + targetUser.getIdentifier())); + return Objects.requireNonNull(packageInfo.signingInfo); + } + private AppSearchManager getAppSearchManagerAsUser(@NonNull UserHandle userHandle) { return mContext.createContextAsUser(userHandle, /* flags= */ 0) .getSystemService(AppSearchManager.class); diff --git a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java index 4cba8ecb2092..0cec09dcde8b 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java +++ b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java @@ -23,6 +23,7 @@ import android.app.appfunctions.IAppFunctionService; import android.app.appfunctions.ICancellationCallback; import android.app.appfunctions.IExecuteAppFunctionCallback; import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback; +import android.content.pm.SigningInfo; import android.os.SystemClock; import android.util.Slog; @@ -38,14 +39,17 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp private final ExecuteAppFunctionAidlRequest mRequestInternal; private final SafeOneTimeExecuteAppFunctionCallback mSafeExecuteAppFunctionCallback; private final ICancellationCallback mCancellationCallback; + private final SigningInfo mCallerSigningInfo; public RunAppFunctionServiceCallback( ExecuteAppFunctionAidlRequest requestInternal, ICancellationCallback cancellationCallback, - SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) { - this.mRequestInternal = requestInternal; - this.mSafeExecuteAppFunctionCallback = safeExecuteAppFunctionCallback; - this.mCancellationCallback = cancellationCallback; + SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback, + SigningInfo callerSigningInfo) { + mRequestInternal = requestInternal; + mSafeExecuteAppFunctionCallback = safeExecuteAppFunctionCallback; + mCancellationCallback = cancellationCallback; + mCallerSigningInfo = callerSigningInfo; } @Override @@ -58,6 +62,7 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp service.executeAppFunction( mRequestInternal.getClientRequest(), mRequestInternal.getCallingPackage(), + mCallerSigningInfo, mCancellationCallback, new IExecuteAppFunctionCallback.Stub() { @Override diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java index 3f6ede95eaf9..8804faf2d312 100644 --- a/services/backup/java/com/android/server/backup/BackupManagerService.java +++ b/services/backup/java/com/android/server/backup/BackupManagerService.java @@ -22,9 +22,9 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; import android.app.backup.BackupManager; +import android.app.backup.BackupManagerInternal; import android.app.backup.BackupRestoreEventLogger.DataTypeResult; import android.app.backup.IBackupManager; import android.app.backup.IBackupManagerMonitor; @@ -60,6 +60,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; +import com.android.server.LocalServices; import com.android.server.SystemConfig; import com.android.server.SystemService; import com.android.server.backup.utils.RandomAccessFileUtils; @@ -91,7 +92,7 @@ import java.util.Set; * privileged callers (currently {@link DevicePolicyManager}). If called on {@link * UserHandle#USER_SYSTEM}, backup is disabled for all users. */ -public class BackupManagerService extends IBackupManager.Stub { +public class BackupManagerService extends IBackupManager.Stub implements BackupManagerInternal { public static final String TAG = "BackupManagerService"; public static final boolean DEBUG = true; public static final boolean MORE_DEBUG = false; @@ -191,7 +192,6 @@ public class BackupManagerService extends IBackupManager.Stub { } } - // TODO: Remove this when we implement DI by injecting in the construtor. @VisibleForTesting Handler getBackupHandler() { return mHandler; @@ -637,51 +637,28 @@ public class BackupManagerService extends IBackupManager.Stub { } @Override - public void agentConnectedForUser(int userId, String packageName, IBinder agent) - throws RemoteException { - if (isUserReadyForBackup(userId)) { - agentConnected(userId, packageName, agent); + public void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent) { + if (!isUserReadyForBackup(userId)) { + return; } - } - @Override - public void agentConnected(String packageName, IBinder agent) throws RemoteException { - agentConnectedForUser(binderGetCallingUserId(), packageName, agent); - } - - /** - * Callback: a requested backup agent has been instantiated. This should only be called from the - * {@link ActivityManager}. - */ - public void agentConnected(@UserIdInt int userId, String packageName, IBinder agentBinder) { - UserBackupManagerService userBackupManagerService = - getServiceForUserIfCallerHasPermission(userId, "agentConnected()"); + UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission( + userId, "agentConnected()"); if (userBackupManagerService != null) { userBackupManagerService.getBackupAgentConnectionManager().agentConnected(packageName, - agentBinder); + agent); } } @Override - public void agentDisconnectedForUser(int userId, String packageName) throws RemoteException { - if (isUserReadyForBackup(userId)) { - agentDisconnected(userId, packageName); + public void agentDisconnectedForUser(String packageName, @UserIdInt int userId) { + if (!isUserReadyForBackup(userId)) { + return; } - } - @Override - public void agentDisconnected(String packageName) throws RemoteException { - agentDisconnectedForUser(binderGetCallingUserId(), packageName); - } - - /** - * Callback: a backup agent has failed to come up, or has unexpectedly quit. This should only be - * called from the {@link ActivityManager}. - */ - public void agentDisconnected(@UserIdInt int userId, String packageName) { - UserBackupManagerService userBackupManagerService = - getServiceForUserIfCallerHasPermission(userId, "agentDisconnected()"); + UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission( + userId, "agentDisconnected()"); if (userBackupManagerService != null) { userBackupManagerService.getBackupAgentConnectionManager().agentDisconnected( @@ -1688,7 +1665,7 @@ public class BackupManagerService extends IBackupManager.Stub { * @param userId User id on which the backup operation is being requested. * @param message A message to include in the exception if it is thrown. */ - void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) { + private void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) { if (binderGetCallingUserId() != userId) { mContext.enforceCallingOrSelfPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, message); @@ -1697,6 +1674,8 @@ public class BackupManagerService extends IBackupManager.Stub { /** Implementation to receive lifecycle event callbacks for system services. */ public static class Lifecycle extends SystemService { + private final BackupManagerService mBackupManagerService; + public Lifecycle(Context context) { this(context, new BackupManagerService(context)); } @@ -1704,12 +1683,14 @@ public class BackupManagerService extends IBackupManager.Stub { @VisibleForTesting Lifecycle(Context context, BackupManagerService backupManagerService) { super(context); + mBackupManagerService = backupManagerService; sInstance = backupManagerService; + LocalServices.addService(BackupManagerInternal.class, mBackupManagerService); } @Override public void onStart() { - publishService(Context.BACKUP_SERVICE, BackupManagerService.sInstance); + publishService(Context.BACKUP_SERVICE, mBackupManagerService); } @Override @@ -1717,17 +1698,17 @@ public class BackupManagerService extends IBackupManager.Stub { // Starts the backup service for this user if backup is active for this user. Offloads // work onto the handler thread {@link #mHandlerThread} to keep unlock time low since // backup is not essential for device functioning. - sInstance.postToHandler( + mBackupManagerService.postToHandler( () -> { - sInstance.updateDefaultBackupUserIdIfNeeded(); - sInstance.startServiceForUser(user.getUserIdentifier()); - sInstance.mHasFirstUserUnlockedSinceBoot = true; + mBackupManagerService.updateDefaultBackupUserIdIfNeeded(); + mBackupManagerService.startServiceForUser(user.getUserIdentifier()); + mBackupManagerService.mHasFirstUserUnlockedSinceBoot = true; }); } @Override public void onUserStopping(@NonNull TargetUser user) { - sInstance.onStopUser(user.getUserIdentifier()); + mBackupManagerService.onStopUser(user.getUserIdentifier()); } @VisibleForTesting diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 418f3a18688b..0e2e50589217 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -109,6 +109,8 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Collection; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; @SuppressLint("LongLogTag") public class CompanionDeviceManagerService extends SystemService { @@ -226,7 +228,8 @@ public class CompanionDeviceManagerService extends SystemService { if (associations.isEmpty()) return; mCompanionExemptionProcessor.updateAtm(userId, associations); - mCompanionExemptionProcessor.updateAutoRevokeExemptions(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(mCompanionExemptionProcessor::updateAutoRevokeExemptions); } @Override 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 d3e808fbd3d1..7456c5099698 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -265,8 +265,8 @@ class InputController { mInputManagerInternal.setPointerIconVisible(visible, displayId); } - void setMousePointerAccelerationEnabled(boolean enabled, int displayId) { - mInputManagerInternal.setMousePointerAccelerationEnabled(enabled, displayId); + void setMouseScalingEnabled(boolean enabled, int displayId) { + mInputManagerInternal.setMouseScalingEnabled(enabled, displayId); } void setDisplayEligibilityForPointerCapture(boolean isEligible, int displayId) { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 6bf60bf1ddf1..260ea75a1f4c 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -1518,7 +1518,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub final long token = Binder.clearCallingIdentity(); try { - mInputController.setMousePointerAccelerationEnabled(false, displayId); + mInputController.setMouseScalingEnabled(false, displayId); mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false, displayId); if (isTrustedDisplay) { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 1f3b31692289..40726b4331e2 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -176,7 +176,7 @@ public class VirtualDeviceManagerService extends SystemService { public VirtualDeviceManagerService(Context context) { super(context); mImpl = new VirtualDeviceManagerImpl(); - mNativeImpl = Flags.enableNativeVdm() ? new VirtualDeviceManagerNativeImpl() : null; + mNativeImpl = new VirtualDeviceManagerNativeImpl(); mLocalService = new LocalService(); } @@ -208,9 +208,7 @@ public class VirtualDeviceManagerService extends SystemService { @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) public void onStart() { publishBinderService(Context.VIRTUAL_DEVICE_SERVICE, mImpl); - if (Flags.enableNativeVdm()) { - publishBinderService(VIRTUAL_DEVICE_NATIVE_SERVICE, mNativeImpl); - } + publishBinderService(VIRTUAL_DEVICE_NATIVE_SERVICE, mNativeImpl); publishLocalService(VirtualDeviceManagerInternal.class, mLocalService); ActivityTaskManagerInternal activityTaskManagerInternal = getLocalService( ActivityTaskManagerInternal.class); @@ -769,7 +767,7 @@ public class VirtualDeviceManagerService extends SystemService { params, /* activityListener= */ null, /* soundEffectListener= */ null); - return new VirtualDeviceManager.VirtualDevice(mImpl, getContext(), virtualDevice); + return new VirtualDeviceManager.VirtualDevice(getContext(), virtualDevice); } @Override diff --git a/services/core/Android.bp b/services/core/Android.bp index dc830642dcc5..d6bffcb7d21d 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -132,6 +132,7 @@ java_library_static { srcs: [ ":android.hardware.tv.hdmi.connection-V1-java-source", ":android.hardware.tv.hdmi.earc-V1-java-source", + ":android.hardware.tv.mediaquality-V1-java-source", ":statslog-art-java-gen", ":statslog-contexthub-java-gen", ":services.core-aidl-sources", diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 778c6864282d..1d914c89c570 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -729,8 +729,10 @@ public class BinaryTransparencyService extends SystemService { private void printModuleDetails(ModuleInfo moduleInfo, final PrintWriter pw) { pw.println("--- Module Details ---"); pw.println("Module name: " + moduleInfo.getName()); - pw.println("Module visibility: " - + (moduleInfo.isHidden() ? "hidden" : "visible")); + if (!android.content.pm.Flags.removeHiddenModuleUsage()) { + pw.println("Module visibility: " + + (moduleInfo.isHidden() ? "hidden" : "visible")); + } } private void printAppDetails(PackageInfo packageInfo, @@ -1708,7 +1710,7 @@ public class BinaryTransparencyService extends SystemService { private class PackageUpdatedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if (!intent.getAction().equals(Intent.ACTION_PACKAGE_ADDED)) { + if (!Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) { return; } diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java index dce9760b3971..6459016eec75 100644 --- a/services/core/java/com/android/server/GestureLauncherService.java +++ b/services/core/java/com/android/server/GestureLauncherService.java @@ -66,8 +66,7 @@ import com.android.server.wm.WindowManagerInternal; /** * The service that listens for gestures detected in sensor firmware and starts the intent * accordingly. - * <p>For now, only camera launch gesture is supported, and in the future, more gestures can be - * added.</p> + * * @hide */ public class GestureLauncherService extends SystemService { @@ -109,10 +108,22 @@ public class GestureLauncherService extends SystemService { @VisibleForTesting static final int EMERGENCY_GESTURE_POWER_BUTTON_COOLDOWN_PERIOD_MS_MAX = 5000; - /** Indicates camera should be launched on power double tap. */ + /** Configuration value indicating double tap power gesture is disabled. */ + @VisibleForTesting static final int DOUBLE_TAP_POWER_DISABLED_MODE = 0; + + /** Configuration value indicating double tap power gesture should launch camera. */ + @VisibleForTesting static final int DOUBLE_TAP_POWER_LAUNCH_CAMERA_MODE = 1; + + /** + * Configuration value indicating double tap power gesture should launch one of many target + * actions. + */ + @VisibleForTesting static final int DOUBLE_TAP_POWER_MULTI_TARGET_MODE = 2; + + /** Indicates camera launch is selected as target action for multi target double tap power. */ @VisibleForTesting static final int LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER = 0; - /** Indicates wallet should be launched on power double tap. */ + /** Indicates wallet launch is selected as target action for multi target double tap power. */ @VisibleForTesting static final int LAUNCH_WALLET_ON_DOUBLE_TAP_POWER = 1; /** Number of taps required to launch the double tap shortcut (either camera or wallet). */ @@ -228,6 +239,7 @@ public class GestureLauncherService extends SystemService { return mId; } } + public GestureLauncherService(Context context) { this(context, new MetricsLogger(), QuickAccessWalletClient.create(context), new UiEventLoggerImpl()); @@ -289,16 +301,15 @@ public class GestureLauncherService extends SystemService { Settings.Secure.getUriFor( Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE), false, mSettingObserver, mUserId); - } else { - mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.CAMERA_GESTURE_DISABLED), - false, mSettingObserver, mUserId); - mContext.getContentResolver().registerContentObserver( - Settings.Secure.getUriFor( - Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED), - false, mSettingObserver, mUserId); } mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.CAMERA_GESTURE_DISABLED), + false, mSettingObserver, mUserId); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED), + false, mSettingObserver, mUserId); + mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.CAMERA_LIFT_TRIGGER_ENABLED), false, mSettingObserver, mUserId); mContext.getContentResolver().registerContentObserver( @@ -468,23 +479,27 @@ public class GestureLauncherService extends SystemService { Settings.Secure.CAMERA_GESTURE_DISABLED, 0, userId) == 0); } - /** Checks if camera should be launched on double press of the power button. */ public static boolean isCameraDoubleTapPowerSettingEnabled(Context context, int userId) { - boolean res; - - if (launchWalletOptionOnPowerDoubleTap()) { - res = isDoubleTapPowerGestureSettingEnabled(context, userId) - && getDoubleTapPowerGestureAction(context, userId) - == LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER; - } else { - // These are legacy settings that will be deprecated once the option to launch both - // wallet and camera has been created. - res = isCameraDoubleTapPowerEnabled(context.getResources()) + if (!launchWalletOptionOnPowerDoubleTap()) { + return isCameraDoubleTapPowerEnabled(context.getResources()) && (Settings.Secure.getIntForUser(context.getContentResolver(), Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0); } - return res; + + final int doubleTapPowerGestureSettingMode = getDoubleTapPowerGestureMode( + context.getResources()); + + return switch (doubleTapPowerGestureSettingMode) { + case DOUBLE_TAP_POWER_LAUNCH_CAMERA_MODE -> Settings.Secure.getIntForUser( + context.getContentResolver(), + Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0; + case DOUBLE_TAP_POWER_MULTI_TARGET_MODE -> + isMultiTargetDoubleTapPowerGestureSettingEnabled(context, userId) + && getDoubleTapPowerGestureAction(context, userId) + == LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER; + default -> false; + }; } /** Checks if wallet should be launched on double tap of the power button. */ @@ -493,7 +508,9 @@ public class GestureLauncherService extends SystemService { return false; } - return isDoubleTapPowerGestureSettingEnabled(context, userId) + return getDoubleTapPowerGestureMode(context.getResources()) + == DOUBLE_TAP_POWER_MULTI_TARGET_MODE + && isMultiTargetDoubleTapPowerGestureSettingEnabled(context, userId) && getDoubleTapPowerGestureAction(context, userId) == LAUNCH_WALLET_ON_DOUBLE_TAP_POWER; } @@ -515,26 +532,40 @@ public class GestureLauncherService extends SystemService { isDefaultEmergencyGestureEnabled(context.getResources()) ? 1 : 0, userId) != 0; } - private static int getDoubleTapPowerGestureAction(Context context, int userId) { - return Settings.Secure.getIntForUser( - context.getContentResolver(), - Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE, - LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER, - userId); + /** Gets the double tap power gesture mode. */ + private static int getDoubleTapPowerGestureMode(Resources resources) { + return resources.getInteger(R.integer.config_doubleTapPowerGestureMode); } - /** Whether the shortcut to launch app on power double press is enabled. */ - private static boolean isDoubleTapPowerGestureSettingEnabled(Context context, int userId) { + /** + * Whether the setting for multi target double tap power gesture is enabled. + * + * <p>Multi target double tap power gesture allows the user to choose one of many target actions + * when double tapping the power button. + * </p> + */ + private static boolean isMultiTargetDoubleTapPowerGestureSettingEnabled(Context context, + int userId) { return Settings.Secure.getIntForUser( context.getContentResolver(), Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED, - isDoubleTapConfigEnabled(context.getResources()) ? 1 : 0, + getDoubleTapPowerGestureMode(context.getResources()) + == DOUBLE_TAP_POWER_MULTI_TARGET_MODE ? 1 : 0, userId) == 1; } - private static boolean isDoubleTapConfigEnabled(Resources resources) { - return resources.getBoolean(R.bool.config_doubleTapPowerGestureEnabled); + /** Gets the selected target action for the multi target double tap power gesture. + * + * <p>The target actions are defined in {@link Settings.Secure#DOUBLE_TAP_POWER_BUTTON_GESTURE}. + * </p> + */ + private static int getDoubleTapPowerGestureAction(Context context, int userId) { + return Settings.Secure.getIntForUser( + context.getContentResolver(), + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE, + LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER, + userId); } /** @@ -595,7 +626,7 @@ public class GestureLauncherService extends SystemService { || isCameraLiftTriggerEnabled(resources) || isEmergencyGestureEnabled(resources); if (launchWalletOptionOnPowerDoubleTap()) { - res |= isDoubleTapConfigEnabled(resources); + res |= getDoubleTapPowerGestureMode(resources) != DOUBLE_TAP_POWER_DISABLED_MODE; } else { res |= isCameraDoubleTapPowerEnabled(resources); } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b536dc524a80..0603c4506cd1 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -256,7 +256,7 @@ import android.app.ServiceStartNotAllowedException; import android.app.WaitResult; import android.app.assist.ActivityId; import android.app.backup.BackupAnnotations.BackupDestination; -import android.app.backup.IBackupManager; +import android.app.backup.BackupManagerInternal; import android.app.compat.CompatChanges; import android.app.job.JobParameters; import android.app.usage.UsageEvents; @@ -4490,11 +4490,8 @@ public class ActivityManagerService extends IActivityManager.Stub final int userId = app.userId; final String packageName = app.info.packageName; mHandler.post(() -> { - try { - getBackupManager().agentDisconnectedForUser(userId, packageName); - } catch (RemoteException e) { - // Can't happen; the backup manager is local - } + LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser( + packageName, userId); }); } } else { @@ -12864,6 +12861,28 @@ public class ActivityManagerService extends IActivityManager.Stub } } + final long kernelCmaUsage = Debug.getKernelCmaUsageKb(); + if (kernelCmaUsage >= 0) { + pw.print(" Kernel CMA: "); + pw.println(stringifyKBSize(kernelCmaUsage)); + // CMA memory can be in one of the following four states: + // + // 1. Free, in which case it is accounted for as part of MemFree, which + // is already considered in the lostRAM calculation below. + // + // 2. Allocated as part of a userspace allocated, in which case it is + // already accounted for in the total PSS value that was computed. + // + // 3. Allocated for storing compressed memory (ZRAM) on Android kernels. + // This is accounted for by calculating the amount of memory ZRAM + // consumes and including it in the lostRAM calculuation. + // + // 4. Allocated by a kernel driver, in which case, it is currently not + // attributed to any term that has been derived thus far. Since the + // allocations come from a kernel driver, add it to kernelUsed. + kernelUsed += kernelCmaUsage; + } + // Note: ION/DMA-BUF heap pools are reclaimable and hence, they are included as part of // memInfo.getCachedSizeKb(). final long lostRAM = memInfo.getTotalSizeKb() @@ -13381,12 +13400,32 @@ public class ActivityManagerService extends IActivityManager.Stub proto.write(MemInfoDumpProto.CACHED_KERNEL_KB, memInfo.getCachedSizeKb()); proto.write(MemInfoDumpProto.FREE_KB, memInfo.getFreeSizeKb()); } + // CMA memory can be in one of the following four states: + // + // 1. Free, in which case it is accounted for as part of MemFree, which + // is already considered in the lostRAM calculation below. + // + // 2. Allocated as part of a userspace allocated, in which case it is + // already accounted for in the total PSS value that was computed. + // + // 3. Allocated for storing compressed memory (ZRAM) on Android Kernels. + // This is accounted for by calculating hte amount of memory ZRAM + // consumes and including it in the lostRAM calculation. + // + // 4. Allocated by a kernel driver, in which case, it is currently not + // attributed to any term that has been derived thus far, so subtract + // it from lostRAM. + long kernelCmaUsage = Debug.getKernelCmaUsageKb(); + if (kernelCmaUsage < 0) { + kernelCmaUsage = 0; + } long lostRAM = memInfo.getTotalSizeKb() - (ss[INDEX_TOTAL_PSS] - ss[INDEX_TOTAL_SWAP_PSS]) - memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb() // NR_SHMEM is subtracted twice (getCachedSizeKb() and getKernelUsedSizeKb()) + memInfo.getShmemSizeKb() - - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb(); + - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb() + - kernelCmaUsage; proto.write(MemInfoDumpProto.USED_PSS_KB, ss[INDEX_TOTAL_PSS] - cachedPss); proto.write(MemInfoDumpProto.USED_KERNEL_KB, memInfo.getKernelUsedSizeKb()); proto.write(MemInfoDumpProto.LOST_RAM_KB, lostRAM); @@ -13505,11 +13544,8 @@ public class ActivityManagerService extends IActivityManager.Stub if (DEBUG_BACKUP || DEBUG_CLEANUP) Slog.d(TAG_CLEANUP, "App " + backupTarget.appInfo + " died during backup"); mHandler.post(() -> { - try { - getBackupManager().agentDisconnectedForUser(app.userId, app.info.packageName); - } catch (RemoteException e) { - // can't happen; backup manager is local - } + LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser( + app.info.packageName, app.userId); }); } @@ -14223,9 +14259,8 @@ public class ActivityManagerService extends IActivityManager.Stub final long oldIdent = Binder.clearCallingIdentity(); try { - getBackupManager().agentConnectedForUser(userId, agentPackageName, agent); - } catch (RemoteException e) { - // can't happen; the backup manager service is local + LocalServices.getService(BackupManagerInternal.class).agentConnectedForUser( + agentPackageName, userId, agent); } catch (Exception e) { Slog.w(TAG, "Exception trying to deliver BackupAgent binding: "); e.printStackTrace(); @@ -14565,7 +14600,7 @@ public class ActivityManagerService extends IActivityManager.Stub app.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION); } - app.setActiveInstrumentation(activeInstr); + mProcessStateController.setActiveInstrumentation(app, activeInstr); activeInstr.mFinished = false; activeInstr.mSourceUid = callingUid; activeInstr.mRunningProcesses.add(app); @@ -14711,7 +14746,7 @@ public class ActivityManagerService extends IActivityManager.Stub abiOverride, ZYGOTE_POLICY_FLAG_EMPTY); - app.setActiveInstrumentation(activeInstr); + mProcessStateController.setActiveInstrumentation(app, activeInstr); activeInstr.mFinished = false; activeInstr.mSourceUid = callingUid; activeInstr.mRunningProcesses.add(app); @@ -14848,7 +14883,7 @@ public class ActivityManagerService extends IActivityManager.Stub } instr.removeProcess(app); - app.setActiveInstrumentation(null); + mProcessStateController.setActiveInstrumentation(app, null); } app.mProfile.clearHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION); @@ -16617,7 +16652,7 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void onUserRemoved(@UserIdInt int userId) { + public void onUserRemoving(@UserIdInt int userId) { // Clean up any ActivityTaskManager state (by telling it the user is stopped) mAtmInternal.onUserStopped(userId); // Clean up various services by removing the user @@ -16631,6 +16666,12 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override + public void onUserRemoved(int userId) { + // Clean up UserController state + mUserController.onUserRemoved(userId); + } + + @Override public boolean startUserInBackground(final int userId) { return ActivityManagerService.this.startUserInBackground(userId); } @@ -19374,7 +19415,7 @@ public class ActivityManagerService extends IActivityManager.Stub } if (preventIntentRedirectCollectNestedKeysOnServerIfNotCollected()) { // this flag will be ramped to public. - intent.collectExtraIntentKeys(); + intent.collectExtraIntentKeys(true); } } @@ -19440,8 +19481,4 @@ public class ActivityManagerService extends IActivityManager.Stub } return token; } - - private IBackupManager getBackupManager() { - return IBackupManager.Stub.asInterface(ServiceManager.getService(Context.BACKUP_SERVICE)); - } } diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java index 6b24df4a1fa8..225c7ca2ca9e 100644 --- a/services/core/java/com/android/server/am/AppProfiler.java +++ b/services/core/java/com/android/server/am/AppProfiler.java @@ -2477,13 +2477,15 @@ public class AppProfiler { // This is the wildcard mode, where every process brought up for // the target instrumentation should be included. if (aInstr.mTargetInfo.packageName.equals(app.info.packageName)) { - app.setActiveInstrumentation(aInstr); + mService.mProcessStateController.setActiveInstrumentation(app, + aInstr); aInstr.mRunningProcesses.add(app); } } else { for (String proc : aInstr.mTargetProcesses) { if (proc.equals(app.processName)) { - app.setActiveInstrumentation(aInstr); + mService.mProcessStateController.setActiveInstrumentation(app, + aInstr); aInstr.mRunningProcesses.add(app); break; } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 9c569db99797..3abcd4e7a143 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -156,7 +156,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; -import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; @@ -3401,13 +3400,10 @@ public class OomAdjuster { } private static int getCpuCapability(ProcessRecord app, long nowUptime) { + // Note: persistent processes get all capabilities, including CPU_TIME. final UidRecord uidRec = app.getUidRecord(); if (uidRec != null && uidRec.isCurAllowListed()) { - // Process has user visible activities. - return PROCESS_CAPABILITY_CPU_TIME; - } - if (UserHandle.isCore(app.uid)) { - // Make sure all system components are not frozen. + // Process is in the power allowlist. return PROCESS_CAPABILITY_CPU_TIME; } if (app.mState.getCachedHasVisibleActivities()) { @@ -3418,6 +3414,12 @@ public class OomAdjuster { // It running a short fgs, just give it cpu time. return PROCESS_CAPABILITY_CPU_TIME; } + if (app.mReceivers.numberOfCurReceivers() > 0) { + return PROCESS_CAPABILITY_CPU_TIME; + } + if (app.hasActiveInstrumentation()) { + return PROCESS_CAPABILITY_CPU_TIME; + } // TODO(b/370817323): Populate this method with all of the reasons to keep a process // unfrozen. return 0; diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 3817ba1a28b9..0b7890167c08 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -305,6 +305,10 @@ public final class PendingIntentRecord extends IIntentSender.Stub { this.stringName = null; } + @VisibleForTesting TempAllowListDuration getAllowlistDurationLocked(IBinder allowlistToken) { + return mAllowlistDuration.get(allowlistToken); + } + void setAllowBgActivityStarts(IBinder token, int flags) { if (token == null) return; if ((flags & FLAG_ACTIVITY_SENDER) != 0) { @@ -323,6 +327,12 @@ public final class PendingIntentRecord extends IIntentSender.Stub { mAllowBgActivityStartsForActivitySender.remove(token); mAllowBgActivityStartsForBroadcastSender.remove(token); mAllowBgActivityStartsForServiceSender.remove(token); + if (mAllowlistDuration != null) { + mAllowlistDuration.remove(token); + if (mAllowlistDuration.isEmpty()) { + mAllowlistDuration = null; + } + } } public void registerCancelListenerLocked(IResultReceiver receiver) { @@ -703,7 +713,7 @@ public final class PendingIntentRecord extends IIntentSender.Stub { return res; } - private BackgroundStartPrivileges getBackgroundStartPrivilegesForActivitySender( + @VisibleForTesting BackgroundStartPrivileges getBackgroundStartPrivilegesForActivitySender( IBinder allowlistToken) { return mAllowBgActivityStartsForActivitySender.contains(allowlistToken) ? BackgroundStartPrivileges.allowBackgroundActivityStarts(allowlistToken) diff --git a/services/core/java/com/android/server/am/ProcessStateController.java b/services/core/java/com/android/server/am/ProcessStateController.java index 57899228e6ad..f44fb06727cf 100644 --- a/services/core/java/com/android/server/am/ProcessStateController.java +++ b/services/core/java/com/android/server/am/ProcessStateController.java @@ -246,12 +246,11 @@ public class ProcessStateController { } /** - * Set what sched group to grant a process due to running a broadcast. - * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast. + * Sets an active instrumentation running within the given process. */ - public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) { - // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model - throw new UnsupportedOperationException("Not implemented yet"); + public void setActiveInstrumentation(@NonNull ProcessRecord proc, + ActiveInstrumentation activeInstrumentation) { + proc.setActiveInstrumentation(activeInstrumentation); } /********************* Process Visibility State Events *********************/ @@ -587,6 +586,34 @@ public class ProcessStateController { psr.updateHasTopStartedAlmostPerceptibleServices(); } + /************************ Broadcast Receiver State Events **************************/ + /** + * Set what sched group to grant a process due to running a broadcast. + * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast. + */ + public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) { + // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Note that the process has started processing a broadcast receiver. + */ + public boolean incrementCurReceivers(@NonNull ProcessRecord app) { + // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model + // maybe used ActivityStateFlags instead. + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Note that the process has finished processing a broadcast receiver. + */ + public boolean decrementCurReceivers(@NonNull ProcessRecord app) { + // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model + // maybe used ActivityStateFlags instead. + throw new UnsupportedOperationException("Not implemented yet"); + } + /** * Builder for ProcessStateController. */ diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index ec74f60539a2..d76c04ac7f31 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -125,7 +125,6 @@ import android.view.Display; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.policy.IKeyguardDismissCallback; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.ObjectUtils; @@ -161,7 +160,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; -import java.util.function.Consumer; /** * Helper class for {@link ActivityManagerService} responsible for multi-user functionality. @@ -227,14 +225,6 @@ class UserController implements Handler.Callback { private static final int USER_SWITCH_CALLBACKS_TIMEOUT_MS = 5 * 1000; /** - * Amount of time waited for {@link WindowManagerService#dismissKeyguard} callbacks to be - * called after dismissing the keyguard. - * Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog()} - * and report user switch is complete {@link #REPORT_USER_SWITCH_COMPLETE_MSG}. - */ - private static final int DISMISS_KEYGUARD_TIMEOUT_MS = 2 * 1000; - - /** * Time after last scheduleOnUserCompletedEvent() call at which USER_COMPLETED_EVENT_MSG will be * scheduled (although it may fire sooner instead). * When it fires, {@link #reportOnUserCompletedEvent} will be processed. @@ -455,11 +445,6 @@ class UserController implements Handler.Callback { public void onUserCreated(UserInfo user, Object token) { onUserAdded(user); } - - @Override - public void onUserRemoved(UserInfo user) { - UserController.this.onUserRemoved(user.id); - } }; UserController(ActivityManagerService service) { @@ -2010,7 +1995,7 @@ class UserController implements Handler.Callback { mInjector.getWindowManager().setSwitchingUser(true); // Only lock if the user has a secure keyguard PIN/Pattern/Pwd if (mInjector.getKeyguardManager().isDeviceSecure(userId)) { - // Make sure the device is locked before moving on with the user switch + Slogf.d(TAG, "Locking the device before moving on with the user switch"); mInjector.lockDeviceNowAndWaitForKeyguardShown(); } } @@ -2640,7 +2625,7 @@ class UserController implements Handler.Callback { EventLog.writeEvent(EventLogTags.UC_CONTINUE_USER_SWITCH, oldUserId, newUserId); - // Do the keyguard dismiss and dismiss the user switching dialog later + // Dismiss the user switching dialog and complete the user switch mHandler.removeMessages(COMPLETE_USER_SWITCH_MSG); mHandler.sendMessage(mHandler.obtainMessage( COMPLETE_USER_SWITCH_MSG, oldUserId, newUserId)); @@ -2655,31 +2640,17 @@ class UserController implements Handler.Callback { @VisibleForTesting void completeUserSwitch(int oldUserId, int newUserId) { - final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled(); - // serialize each conditional step - await( - // STEP 1 - If there is no challenge set, dismiss the keyguard right away - isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId), - mInjector::dismissKeyguard, - () -> await( - // STEP 2 - If user switch ui was enabled, dismiss user switch dialog - isUserSwitchUiEnabled, - this::dismissUserSwitchDialog, - () -> { - // STEP 3 - Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast - // ACTION_USER_SWITCHED & call UserSwitchObservers.onUserSwitchComplete - mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG); - mHandler.sendMessage(mHandler.obtainMessage( - REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId)); - } - )); - } - - private void await(boolean condition, Consumer<Runnable> conditionalStep, Runnable nextStep) { - if (condition) { - conditionalStep.accept(nextStep); + final Runnable runnable = () -> { + // Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast ACTION_USER_SWITCHED and call + // onUserSwitchComplete on UserSwitchObservers. + mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG); + mHandler.sendMessage(mHandler.obtainMessage( + REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId)); + }; + if (isUserSwitchUiEnabled()) { + dismissUserSwitchDialog(runnable); } else { - nextStep.run(); + runnable.run(); } } @@ -3381,10 +3352,12 @@ class UserController implements Handler.Callback { if (mUserProfileGroupIds.keyAt(i) == userId || mUserProfileGroupIds.valueAt(i) == userId) { mUserProfileGroupIds.removeAt(i); - } } mCurrentProfileIds = ArrayUtils.removeInt(mCurrentProfileIds, userId); + mUserLru.remove((Integer) userId); + mStartedUsers.remove(userId); + updateStartedUserArrayLU(); } } @@ -4127,33 +4100,6 @@ class UserController implements Handler.Callback { return IStorageManager.Stub.asInterface(ServiceManager.getService("mount")); } - protected void dismissKeyguard(Runnable runnable) { - final AtomicBoolean isFirst = new AtomicBoolean(true); - final Runnable runOnce = () -> { - if (isFirst.getAndSet(false)) { - runnable.run(); - } - }; - - mHandler.postDelayed(runOnce, DISMISS_KEYGUARD_TIMEOUT_MS); - getWindowManager().dismissKeyguard(new IKeyguardDismissCallback.Stub() { - @Override - public void onDismissError() throws RemoteException { - mHandler.post(runOnce); - } - - @Override - public void onDismissSucceeded() throws RemoteException { - mHandler.post(runOnce); - } - - @Override - public void onDismissCancelled() throws RemoteException { - mHandler.post(runOnce); - } - }, /* message= */ null); - } - boolean isHeadlessSystemUserMode() { return UserManager.isHeadlessSystemUserMode(); } @@ -4178,6 +4124,7 @@ class UserController implements Handler.Callback { void lockDeviceNowAndWaitForKeyguardShown() { if (getWindowManager().isKeyguardLocked()) { + Slogf.w(TAG, "Not locking the device since the keyguard is already locked"); return; } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 295e0443371d..8a63f9a24ea3 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -359,7 +359,7 @@ public class AppOpsService extends IAppOpsService.Stub { private static final Duration RATE_LIMITER_WINDOW = Duration.ofMillis(10); private final RateLimiter mRateLimiter = new RateLimiter(RATE_LIMITER_WINDOW); - volatile @NonNull HistoricalRegistry mHistoricalRegistry = new HistoricalRegistry(this); + volatile @NonNull HistoricalRegistry mHistoricalRegistry; /* * These are app op restrictions imposed per user from various parties. @@ -1039,6 +1039,8 @@ public class AppOpsService extends IAppOpsService.Stub { // will not exist and the nonce will be UNSET. AppOpsManager.invalidateAppOpModeCache(); AppOpsManager.disableAppOpModeCache(); + + mHistoricalRegistry = new HistoricalRegistry(this, context); } public void publish() { diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java index 4d114b4ad4ac..9dd09cef88f9 100644 --- a/services/core/java/com/android/server/appop/AttributedOp.java +++ b/services/core/java/com/android/server/appop/AttributedOp.java @@ -113,7 +113,7 @@ final class AttributedOp { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, uidState, flags, accessTime, AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE, - DiscreteRegistry.ACCESS_TYPE_NOTE_OP, accessCount); + DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP, accessCount); } /** @@ -257,7 +257,8 @@ final class AttributedOp { if (isStarted) { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, uidState, flags, startTime, - attributionFlags, attributionChainId, DiscreteRegistry.ACCESS_TYPE_START_OP, 1); + attributionFlags, attributionChainId, + DiscreteOpsRegistry.ACCESS_TYPE_START_OP, 1); } } @@ -344,8 +345,8 @@ final class AttributedOp { parent.packageName, persistentDeviceId, tag, event.getUidState(), event.getFlags(), finishedEvent.getNoteTime(), finishedEvent.getDuration(), event.getAttributionFlags(), event.getAttributionChainId(), - isPausing ? DiscreteRegistry.ACCESS_TYPE_PAUSE_OP - : DiscreteRegistry.ACCESS_TYPE_FINISH_OP); + isPausing ? DiscreteOpsRegistry.ACCESS_TYPE_PAUSE_OP + : DiscreteOpsRegistry.ACCESS_TYPE_FINISH_OP); if (!isPausing) { mAppOpsService.mInProgressStartOpEventPool.release(event); @@ -453,7 +454,7 @@ final class AttributedOp { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, event.getUidState(), event.getFlags(), startTime, event.getAttributionFlags(), - event.getAttributionChainId(), DiscreteRegistry.ACCESS_TYPE_RESUME_OP, 1); + event.getAttributionChainId(), DiscreteOpsRegistry.ACCESS_TYPE_RESUME_OP, 1); if (shouldSendActive) { mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, parent.packageName, tag, event.getVirtualDeviceId(), true, diff --git a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java new file mode 100644 index 000000000000..e4c36cc214e8 --- /dev/null +++ b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java @@ -0,0 +1,387 @@ +/* + * 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.appop; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AppOpsManager; +import android.content.Context; +import android.database.DatabaseErrorHandler; +import android.database.DefaultDatabaseErrorHandler; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteRawStatement; +import android.os.Environment; +import android.util.IntArray; +import android.util.Slog; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +class DiscreteOpsDbHelper extends SQLiteOpenHelper { + private static final String LOG_TAG = "DiscreteOpsDbHelper"; + static final String DATABASE_NAME = "app_op_history.db"; + private static final int DATABASE_VERSION = 1; + private static final boolean DEBUG = false; + + DiscreteOpsDbHelper(@NonNull Context context, @NonNull File databaseFile) { + super(context, databaseFile.getAbsolutePath(), null, DATABASE_VERSION, + new DiscreteOpsDatabaseErrorHandler()); + setOpenParams(getDatabaseOpenParams()); + } + + private static SQLiteDatabase.OpenParams getDatabaseOpenParams() { + return new SQLiteDatabase.OpenParams.Builder() + .addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) + .build(); + } + + @NonNull + static File getDatabaseFile() { + return new File(new File(Environment.getDataSystemDirectory(), "appops"), DATABASE_NAME); + } + + @Override + public void onConfigure(SQLiteDatabase db) { + db.execSQL("PRAGMA synchronous = NORMAL"); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(DiscreteOpsTable.CREATE_TABLE_SQL); + db.execSQL(DiscreteOpsTable.CREATE_INDEX_SQL); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + } + + void insertDiscreteOps(@NonNull List<DiscreteOpsSqlRegistry.DiscreteOp> opEvents) { + if (opEvents.isEmpty()) { + return; + } + + SQLiteDatabase db = getWritableDatabase(); + // TODO (b/383157289) what if database is busy and can't start a transaction? will read + // more about it and can be done in a follow up cl. + db.beginTransaction(); + try (SQLiteRawStatement statement = db.createRawStatement( + DiscreteOpsTable.INSERT_TABLE_SQL)) { + for (DiscreteOpsSqlRegistry.DiscreteOp event : opEvents) { + try { + statement.bindInt(DiscreteOpsTable.UID_INDEX, event.getUid()); + bindTextOrNull(statement, DiscreteOpsTable.PACKAGE_NAME_INDEX, + event.getPackageName()); + bindTextOrNull(statement, DiscreteOpsTable.DEVICE_ID_INDEX, + event.getDeviceId()); + statement.bindInt(DiscreteOpsTable.OP_CODE_INDEX, event.getOpCode()); + bindTextOrNull(statement, DiscreteOpsTable.ATTRIBUTION_TAG_INDEX, + event.getAttributionTag()); + statement.bindLong(DiscreteOpsTable.ACCESS_TIME_INDEX, event.getAccessTime()); + statement.bindLong( + DiscreteOpsTable.ACCESS_DURATION_INDEX, event.getDuration()); + statement.bindInt(DiscreteOpsTable.UID_STATE_INDEX, event.getUidState()); + statement.bindInt(DiscreteOpsTable.OP_FLAGS_INDEX, event.getOpFlags()); + statement.bindInt(DiscreteOpsTable.ATTRIBUTION_FLAGS_INDEX, + event.getAttributionFlags()); + statement.bindLong(DiscreteOpsTable.CHAIN_ID_INDEX, event.getChainId()); + statement.step(); + } catch (Exception exception) { + Slog.e(LOG_TAG, "Error inserting the discrete op: " + event, exception); + } finally { + statement.reset(); + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + private void bindTextOrNull(SQLiteRawStatement statement, int index, @Nullable String text) { + if (text == null) { + statement.bindNull(index); + } else { + statement.bindText(index, text); + } + } + + /** + * This will be used as an offset for inserting new chain id in discrete ops table. + */ + long getLargestAttributionChainId() { + long chainId = 0; + try { + SQLiteDatabase db = getReadableDatabase(); + db.beginTransactionReadOnly(); + try (SQLiteRawStatement statement = + db.createRawStatement(DiscreteOpsTable.SELECT_MAX_ATTRIBUTION_CHAIN_ID)) { + if (statement.step()) { + chainId = statement.getColumnLong(0); + if (chainId < 0) { + chainId = 0; + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } catch (SQLiteException exception) { + Slog.e(LOG_TAG, "Error reading attribution chain id", exception); + } + return chainId; + } + + void execSQL(@NonNull String sql) { + execSQL(sql, null); + } + + void execSQL(@NonNull String sql, Object[] bindArgs) { + if (DEBUG) { + Slog.i(LOG_TAG, "DB execSQL, sql: " + sql); + } + SQLiteDatabase db = getWritableDatabase(); + if (bindArgs == null) { + db.execSQL(sql); + } else { + db.execSQL(sql, bindArgs); + } + } + + /** + * Returns a list of {@link DiscreteOpsSqlRegistry.DiscreteOp} based on the given filters. + */ + List<DiscreteOpsSqlRegistry.DiscreteOp> getDiscreteOps( + @AppOpsManager.HistoricalOpsRequestFilter int requestFilters, + int uidFilter, @Nullable String packageNameFilter, + @Nullable String attributionTagFilter, IntArray opCodesFilter, int opFlagsFilter, + long beginTime, long endTime, int limit, String orderByColumn) { + List<SQLCondition> conditions = prepareConditions(beginTime, endTime, requestFilters, + uidFilter, packageNameFilter, + attributionTagFilter, opCodesFilter, opFlagsFilter); + String sql = buildSql(conditions, orderByColumn, limit); + + SQLiteDatabase db = getReadableDatabase(); + List<DiscreteOpsSqlRegistry.DiscreteOp> results = new ArrayList<>(); + db.beginTransactionReadOnly(); + try (SQLiteRawStatement statement = db.createRawStatement(sql)) { + int size = conditions.size(); + for (int i = 0; i < size; i++) { + SQLCondition condition = conditions.get(i); + if (DEBUG) { + Slog.i(LOG_TAG, condition + ", binding value = " + condition.mFilterValue); + } + switch (condition.mColumnFilter) { + case PACKAGE_NAME, ATTR_TAG -> statement.bindText(i + 1, + condition.mFilterValue.toString()); + case UID, OP_CODE_EQUAL, OP_FLAGS -> statement.bindInt(i + 1, + Integer.parseInt(condition.mFilterValue.toString())); + case BEGIN_TIME, END_TIME -> statement.bindLong(i + 1, + Long.parseLong(condition.mFilterValue.toString())); + case OP_CODE_IN -> Slog.d(LOG_TAG, "No binding for In operator"); + default -> Slog.w(LOG_TAG, "unknown sql condition " + condition); + } + } + + while (statement.step()) { + int uid = statement.getColumnInt(0); + String packageName = statement.getColumnText(1); + String deviceId = statement.getColumnText(2); + int opCode = statement.getColumnInt(3); + String attributionTag = statement.getColumnText(4); + long accessTime = statement.getColumnLong(5); + long duration = statement.getColumnLong(6); + int uidState = statement.getColumnInt(7); + int opFlags = statement.getColumnInt(8); + int attributionFlags = statement.getColumnInt(9); + long chainId = statement.getColumnLong(10); + DiscreteOpsSqlRegistry.DiscreteOp event = new DiscreteOpsSqlRegistry.DiscreteOp(uid, + packageName, attributionTag, deviceId, opCode, + opFlags, attributionFlags, uidState, chainId, accessTime, duration); + results.add(event); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + return results; + } + + private String buildSql(List<SQLCondition> conditions, String orderByColumn, int limit) { + StringBuilder sql = new StringBuilder(DiscreteOpsTable.SELECT_TABLE_DATA); + if (!conditions.isEmpty()) { + sql.append(" WHERE "); + int size = conditions.size(); + for (int i = 0; i < size; i++) { + sql.append(conditions.get(i).toString()); + if (i < size - 1) { + sql.append(" AND "); + } + } + } + + if (orderByColumn != null) { + sql.append(" ORDER BY ").append(orderByColumn); + } + if (limit > 0) { + sql.append(" LIMIT ").append(limit); + } + if (DEBUG) { + Slog.i(LOG_TAG, "Sql query " + sql); + } + return sql.toString(); + } + + /** + * Creates where conditions for package, uid, attribution tag and app op codes, + * app op codes condition does not support argument binding. + */ + private List<SQLCondition> prepareConditions(long beginTime, long endTime, int requestFilters, + int uid, @Nullable String packageName, @Nullable String attributionTag, + IntArray opCodes, int opFlags) { + final List<SQLCondition> conditions = new ArrayList<>(); + + if (beginTime != -1) { + conditions.add(new SQLCondition(ColumnFilter.BEGIN_TIME, beginTime)); + } + if (endTime != -1) { + conditions.add(new SQLCondition(ColumnFilter.END_TIME, endTime)); + } + if (opFlags != 0) { + conditions.add(new SQLCondition(ColumnFilter.OP_FLAGS, opFlags)); + } + + if (requestFilters != 0) { + if ((requestFilters & AppOpsManager.FILTER_BY_PACKAGE_NAME) != 0) { + conditions.add(new SQLCondition(ColumnFilter.PACKAGE_NAME, packageName)); + } + if ((requestFilters & AppOpsManager.FILTER_BY_UID) != 0) { + conditions.add(new SQLCondition(ColumnFilter.UID, uid)); + + } + if ((requestFilters & AppOpsManager.FILTER_BY_ATTRIBUTION_TAG) != 0) { + conditions.add(new SQLCondition(ColumnFilter.ATTR_TAG, attributionTag)); + } + // filter op codes + if (opCodes != null && opCodes.size() == 1) { + conditions.add(new SQLCondition(ColumnFilter.OP_CODE_EQUAL, opCodes.get(0))); + } else if (opCodes != null && opCodes.size() > 1) { + StringBuilder b = new StringBuilder(); + int size = opCodes.size(); + for (int i = 0; i < size; i++) { + b.append(opCodes.get(i)); + if (i < size - 1) { + b.append(", "); + } + } + conditions.add(new SQLCondition(ColumnFilter.OP_CODE_IN, b.toString())); + } + } + return conditions; + } + + /** + * This class prepares a where clause condition for discrete ops table column. + */ + static final class SQLCondition { + private final ColumnFilter mColumnFilter; + private final Object mFilterValue; + + SQLCondition(ColumnFilter columnFilter, Object filterValue) { + mColumnFilter = columnFilter; + mFilterValue = filterValue; + } + + @Override + public String toString() { + if (mColumnFilter == ColumnFilter.OP_CODE_IN) { + return mColumnFilter + " ( " + mFilterValue + " )"; + } + return mColumnFilter.toString(); + } + } + + /** + * This enum describes the where clause conditions for different columns in discrete ops + * table. + */ + private enum ColumnFilter { + PACKAGE_NAME(DiscreteOpsTable.Columns.PACKAGE_NAME + " = ? "), + UID(DiscreteOpsTable.Columns.UID + " = ? "), + ATTR_TAG(DiscreteOpsTable.Columns.ATTRIBUTION_TAG + " = ? "), + END_TIME(DiscreteOpsTable.Columns.ACCESS_TIME + " < ? "), + OP_CODE_EQUAL(DiscreteOpsTable.Columns.OP_CODE + " = ? "), + BEGIN_TIME(DiscreteOpsTable.Columns.ACCESS_TIME + " + " + + DiscreteOpsTable.Columns.ACCESS_DURATION + " > ? "), + OP_FLAGS("(" + DiscreteOpsTable.Columns.OP_FLAGS + " & ? ) != 0"), + OP_CODE_IN(DiscreteOpsTable.Columns.OP_CODE + " IN "); + + final String mCondition; + + ColumnFilter(String condition) { + mCondition = condition; + } + + @Override + public String toString() { + return mCondition; + } + } + + static final class DiscreteOpsDatabaseErrorHandler implements DatabaseErrorHandler { + private final DefaultDatabaseErrorHandler mDefaultDatabaseErrorHandler = + new DefaultDatabaseErrorHandler(); + + @Override + public void onCorruption(SQLiteDatabase dbObj) { + Slog.e(LOG_TAG, "discrete ops database got corrupted."); + mDefaultDatabaseErrorHandler.onCorruption(dbObj); + } + } + + // USED for testing only + List<DiscreteOpsSqlRegistry.DiscreteOp> getAllDiscreteOps(@NonNull String sql) { + SQLiteDatabase db = getReadableDatabase(); + List<DiscreteOpsSqlRegistry.DiscreteOp> results = new ArrayList<>(); + db.beginTransactionReadOnly(); + try (SQLiteRawStatement statement = db.createRawStatement(sql)) { + while (statement.step()) { + int uid = statement.getColumnInt(0); + String packageName = statement.getColumnText(1); + String deviceId = statement.getColumnText(2); + int opCode = statement.getColumnInt(3); + String attributionTag = statement.getColumnText(4); + long accessTime = statement.getColumnLong(5); + long duration = statement.getColumnLong(6); + int uidState = statement.getColumnInt(7); + int opFlags = statement.getColumnInt(8); + int attributionFlags = statement.getColumnInt(9); + long chainId = statement.getColumnLong(10); + DiscreteOpsSqlRegistry.DiscreteOp event = new DiscreteOpsSqlRegistry.DiscreteOp(uid, + packageName, attributionTag, deviceId, opCode, + opFlags, attributionFlags, uidState, chainId, accessTime, duration); + results.add(event); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + return results; + } +} diff --git a/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java new file mode 100644 index 000000000000..c38ee55b4f42 --- /dev/null +++ b/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.appop; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for migrating discrete ops from xml to sqlite + */ +public class DiscreteOpsMigrationHelper { + /** + * migrate discrete ops from xml to sqlite. + */ + static void migrateDiscreteOpsToSqlite(DiscreteOpsXmlRegistry xmlRegistry, + DiscreteOpsSqlRegistry sqlRegistry) { + DiscreteOpsXmlRegistry.DiscreteOps xmlOps = xmlRegistry.getAllDiscreteOps(); + List<DiscreteOpsSqlRegistry.DiscreteOp> discreteOps = getSqlDiscreteOps(xmlOps); + sqlRegistry.migrateXmlData(discreteOps, xmlOps.mChainIdOffset); + xmlRegistry.deleteDiscreteOpsDir(); + } + + /** + * rollback discrete ops from sqlite to xml. + */ + static void migrateDiscreteOpsToXml(DiscreteOpsSqlRegistry sqlRegistry, + DiscreteOpsXmlRegistry xmlRegistry) { + List<DiscreteOpsSqlRegistry.DiscreteOp> sqlOps = sqlRegistry.getAllDiscreteOps(); + DiscreteOpsXmlRegistry.DiscreteOps xmlOps = getXmlDiscreteOps(sqlOps); + xmlRegistry.migrateSqliteData(xmlOps); + sqlRegistry.deleteDatabase(); + } + + /** + * Convert sqlite flat rows to hierarchical data. + */ + private static DiscreteOpsXmlRegistry.DiscreteOps getXmlDiscreteOps( + List<DiscreteOpsSqlRegistry.DiscreteOp> discreteOps) { + DiscreteOpsXmlRegistry.DiscreteOps xmlOps = + new DiscreteOpsXmlRegistry.DiscreteOps(0); + if (discreteOps.isEmpty()) { + return xmlOps; + } + + for (DiscreteOpsSqlRegistry.DiscreteOp discreteOp : discreteOps) { + xmlOps.addDiscreteAccess(discreteOp.getOpCode(), discreteOp.getUid(), + discreteOp.getPackageName(), discreteOp.getDeviceId(), + discreteOp.getAttributionTag(), discreteOp.getOpFlags(), + discreteOp.getUidState(), + discreteOp.getAccessTime(), discreteOp.getDuration(), + discreteOp.getAttributionFlags(), (int) discreteOp.getChainId()); + } + return xmlOps; + } + + /** + * Convert xml (hierarchical) data to flat row based data. + */ + private static List<DiscreteOpsSqlRegistry.DiscreteOp> getSqlDiscreteOps( + DiscreteOpsXmlRegistry.DiscreteOps discreteOps) { + List<DiscreteOpsSqlRegistry.DiscreteOp> opEvents = new ArrayList<>(); + + if (discreteOps.isEmpty()) { + return opEvents; + } + + discreteOps.mUids.forEach((uid, discreteUidOps) -> { + discreteUidOps.mPackages.forEach((packageName, packageOps) -> { + packageOps.mPackageOps.forEach((opcode, ops) -> { + ops.mDeviceAttributedOps.forEach((deviceId, deviceOps) -> { + deviceOps.mAttributedOps.forEach((tag, attributedOps) -> { + for (DiscreteOpsXmlRegistry.DiscreteOpEvent attributedOp : + attributedOps) { + DiscreteOpsSqlRegistry.DiscreteOp + opModel = new DiscreteOpsSqlRegistry.DiscreteOp(uid, + packageName, tag, + deviceId, opcode, attributedOp.mOpFlag, + attributedOp.mAttributionFlags, + attributedOp.mUidState, attributedOp.mAttributionChainId, + attributedOp.mNoteTime, + attributedOp.mNoteDuration); + opEvents.add(opModel); + } + }); + }); + }); + }); + }); + + return opEvents; + } +} diff --git a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java new file mode 100644 index 000000000000..88b3f6dce4c2 --- /dev/null +++ b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java @@ -0,0 +1,298 @@ +/* + * 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.appop; + +import static android.app.AppOpsManager.OP_CAMERA; +import static android.app.AppOpsManager.OP_COARSE_LOCATION; +import static android.app.AppOpsManager.OP_EMERGENCY_LOCATION; +import static android.app.AppOpsManager.OP_FINE_LOCATION; +import static android.app.AppOpsManager.OP_FLAG_SELF; +import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED; +import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY; +import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION; +import static android.app.AppOpsManager.OP_MONITOR_LOCATION; +import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA; +import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE; +import static android.app.AppOpsManager.OP_PROCESS_OUTGOING_CALLS; +import static android.app.AppOpsManager.OP_READ_ICC_SMS; +import static android.app.AppOpsManager.OP_READ_SMS; +import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; +import static android.app.AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO; +import static android.app.AppOpsManager.OP_RECORD_AUDIO; +import static android.app.AppOpsManager.OP_RESERVED_FOR_TESTING; +import static android.app.AppOpsManager.OP_SEND_SMS; +import static android.app.AppOpsManager.OP_SMS_FINANCIAL_TRANSACTIONS; +import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW; +import static android.app.AppOpsManager.OP_WRITE_ICC_SMS; +import static android.app.AppOpsManager.OP_WRITE_SMS; + +import static java.lang.Long.min; +import static java.lang.Math.max; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AppOpsManager; +import android.os.AsyncTask; +import android.os.Build; +import android.permission.flags.Flags; +import android.provider.DeviceConfig; +import android.util.Slog; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.FrameworkStatsLog; + +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.util.Date; +import java.util.Set; + +/** + * This class provides interface for xml and sqlite implementation. Implementation manages + * information about recent accesses to ops for permission usage timeline. + * <p> + * The discrete history is kept for limited time (initial default is 24 hours, set in + * {@link DiscreteOpsRegistry#sDiscreteHistoryCutoff} and discarded after that. + * <p> + * Discrete history is quantized to reduce resources footprint. By default, quantization is set to + * one minute in {@link DiscreteOpsRegistry#sDiscreteHistoryQuantization}. All access times are + * aligned to the closest quantized time. All durations (except -1, meaning no duration) are + * rounded up to the closest quantized interval. + * <p> + * When data is queried through API, events are deduplicated and for every time quant there can + * be only one {@link AppOpsManager.AttributedOpEntry}. Each entry contains information about + * different accesses which happened in specified time quant - across dimensions of + * {@link AppOpsManager.UidState} and {@link AppOpsManager.OpFlags}. For each dimension + * it is only possible to know if at least one access happened in the time quant. + * <p> + * INITIALIZATION: We can initialize persistence only after the system is ready + * as we need to check the optional configuration override from the settings + * database which is not initialized at the time the app ops service is created. This class + * relies on {@link HistoricalRegistry} for controlling that no calls are allowed until then. All + * outside calls are going through {@link HistoricalRegistry}. + * + */ +abstract class DiscreteOpsRegistry { + private static final String TAG = DiscreteOpsRegistry.class.getSimpleName(); + + static final boolean DEBUG_LOG = false; + static final String PROPERTY_DISCRETE_HISTORY_CUTOFF = "discrete_history_cutoff_millis"; + static final String PROPERTY_DISCRETE_HISTORY_QUANTIZATION = + "discrete_history_quantization_millis"; + static final String PROPERTY_DISCRETE_FLAGS = "discrete_history_op_flags"; + static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist"; + static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION + + "," + OP_EMERGENCY_LOCATION + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + "," + + OP_PHONE_CALL_MICROPHONE + "," + OP_PHONE_CALL_CAMERA + "," + + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO + "," + OP_RECEIVE_SANDBOX_TRIGGER_AUDIO + + "," + OP_RESERVED_FOR_TESTING; + static final int[] sDiscreteOpsToLog = + new int[]{OP_FINE_LOCATION, OP_COARSE_LOCATION, OP_EMERGENCY_LOCATION, OP_CAMERA, + OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE, OP_PHONE_CALL_CAMERA, + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_READ_SMS, + OP_WRITE_SMS, OP_SEND_SMS, OP_READ_ICC_SMS, OP_WRITE_ICC_SMS, + OP_SMS_FINANCIAL_TRANSACTIONS, OP_SYSTEM_ALERT_WINDOW, OP_MONITOR_LOCATION, + OP_MONITOR_HIGH_POWER_LOCATION, OP_PROCESS_OUTGOING_CALLS, + }; + + static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis(); + static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis(); + // The duration for which the data is kept, default is 7 days and max 30 days enforced. + static long sDiscreteHistoryCutoff; + + static final long DEFAULT_DISCRETE_HISTORY_QUANTIZATION = Duration.ofMinutes(1).toMillis(); + // discrete ops are rounded up to quantization time, meaning we record one op per time bucket + // in case of duplicate op events. + static long sDiscreteHistoryQuantization; + + static int[] sDiscreteOps; + static int sDiscreteFlags; + + static final int OP_FLAGS_DISCRETE = OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED + | OP_FLAG_TRUSTED_PROXY; + + boolean mDebugMode = false; + + static final int ACCESS_TYPE_NOTE_OP = + FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__NOTE_OP; + static final int ACCESS_TYPE_START_OP = + FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__START_OP; + static final int ACCESS_TYPE_FINISH_OP = + FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__FINISH_OP; + static final int ACCESS_TYPE_PAUSE_OP = + FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__PAUSE_OP; + static final int ACCESS_TYPE_RESUME_OP = + FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__RESUME_OP; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"ACCESS_TYPE_"}, value = { + ACCESS_TYPE_NOTE_OP, + ACCESS_TYPE_START_OP, + ACCESS_TYPE_FINISH_OP, + ACCESS_TYPE_PAUSE_OP, + ACCESS_TYPE_RESUME_OP + }) + @interface AccessType {} + + void systemReady() { + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY, + AsyncTask.THREAD_POOL_EXECUTOR, (DeviceConfig.Properties p) -> { + setDiscreteHistoryParameters(p); + }); + setDiscreteHistoryParameters(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_PRIVACY)); + } + + abstract void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, + int op, @Nullable String attributionTag, @AppOpsManager.OpFlags int flags, + @AppOpsManager.UidState int uidState, long accessTime, long accessDuration, + @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, + @DiscreteOpsRegistry.AccessType int accessType); + + /** + * A periodic callback from {@link AppOpsService} to flush the in memory events to disk. + * The shutdown callback is also plugged into it. + * <p> + * This method flushes in memory records to disk, and also clears old records from disk. + */ + abstract void writeAndClearOldAccessHistory(); + + /** Remove all discrete op events. */ + abstract void clearHistory(); + + /** Remove all discrete op events for given UID and package. */ + abstract void clearHistory(int uid, String packageName); + + /** + * Offset access time by given timestamp, new access time would be accessTime - offsetMillis. + */ + abstract void offsetHistory(long offset); + + abstract void addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result, + long beginTimeMillis, long endTimeMillis, + @AppOpsManager.HistoricalOpsRequestFilter int filter, int uidFilter, + @Nullable String packageNameFilter, @Nullable String[] opNamesFilter, + @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter, + Set<String> attributionExemptPkgs); + + abstract void dump(@NonNull PrintWriter pw, int uidFilter, @Nullable String packageNameFilter, + @Nullable String attributionTagFilter, + @AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp, + @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix, + int nDiscreteOps); + + void setDebugMode(boolean debugMode) { + this.mDebugMode = debugMode; + } + + static long discretizeTimeStamp(long timeStamp) { + return timeStamp / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization; + + } + + static long discretizeDuration(long duration) { + return duration == -1 ? -1 : (duration + sDiscreteHistoryQuantization - 1) + / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization; + } + + static boolean isDiscreteOp(int op, @AppOpsManager.OpFlags int flags) { + if (!ArrayUtils.contains(sDiscreteOps, op)) { + return false; + } + if ((flags & (sDiscreteFlags)) == 0) { + return false; + } + return true; + } + + // could this be impl detail of discrete registry, just one test is using the method + // abstract DiscreteRegistry.DiscreteOps getAllDiscreteOps(); + + private void setDiscreteHistoryParameters(DeviceConfig.Properties p) { + if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_CUTOFF)) { + sDiscreteHistoryCutoff = p.getLong(PROPERTY_DISCRETE_HISTORY_CUTOFF, + DEFAULT_DISCRETE_HISTORY_CUTOFF); + if (!Build.IS_DEBUGGABLE && !mDebugMode) { + sDiscreteHistoryCutoff = min(MAXIMUM_DISCRETE_HISTORY_CUTOFF, + sDiscreteHistoryCutoff); + } + } else { + sDiscreteHistoryCutoff = DEFAULT_DISCRETE_HISTORY_CUTOFF; + } + if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_QUANTIZATION)) { + sDiscreteHistoryQuantization = p.getLong(PROPERTY_DISCRETE_HISTORY_QUANTIZATION, + DEFAULT_DISCRETE_HISTORY_QUANTIZATION); + if (!Build.IS_DEBUGGABLE && !mDebugMode) { + sDiscreteHistoryQuantization = max(DEFAULT_DISCRETE_HISTORY_QUANTIZATION, + sDiscreteHistoryQuantization); + } + } else { + sDiscreteHistoryQuantization = DEFAULT_DISCRETE_HISTORY_QUANTIZATION; + } + sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS) ? sDiscreteFlags = + p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE; + sDiscreteOps = p.getKeyset().contains(PROPERTY_DISCRETE_OPS_LIST) ? parseOpsList( + p.getString(PROPERTY_DISCRETE_OPS_LIST, DEFAULT_DISCRETE_OPS)) : parseOpsList( + DEFAULT_DISCRETE_OPS); + } + + private static int[] parseOpsList(String opsList) { + String[] strArr; + if (opsList.isEmpty()) { + strArr = new String[0]; + } else { + strArr = opsList.split(","); + } + int nOps = strArr.length; + int[] result = new int[nOps]; + try { + for (int i = 0; i < nOps; i++) { + result[i] = Integer.parseInt(strArr[i]); + } + } catch (NumberFormatException e) { + Slog.e(TAG, "Failed to parse Discrete ops list: " + e.getMessage()); + return parseOpsList(DEFAULT_DISCRETE_OPS); + } + return result; + } + + /** + * Whether app op access tacking is enabled and a metric event should be logged. + */ + static boolean shouldLogAccess(int op) { + return Flags.appopAccessTrackingLoggingEnabled() + && ArrayUtils.contains(sDiscreteOpsToLog, op); + } + + String getAttributionTag(String attributionTag, String packageName) { + if (attributionTag == null || packageName == null) { + return attributionTag; + } + int firstChar = 0; + if (attributionTag.startsWith(packageName)) { + firstChar = packageName.length(); + if (firstChar < attributionTag.length() && attributionTag.charAt(firstChar) + == '.') { + firstChar++; + } + } + return attributionTag.substring(firstChar); + } + +} diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java new file mode 100644 index 000000000000..4b3981cd4bc0 --- /dev/null +++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java @@ -0,0 +1,689 @@ +/* + * 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.appop; + +import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE; +import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR; +import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER; +import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED; +import static android.app.AppOpsManager.flagsToString; +import static android.app.AppOpsManager.getUidStateName; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AppOpsManager; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.util.ArraySet; +import android.util.IntArray; +import android.util.LongSparseArray; +import android.util.Slog; + +import com.android.internal.util.FrameworkStatsLog; +import com.android.server.ServiceThread; + +import java.io.File; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * This class handles sqlite persistence layer for discrete ops. + */ +public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { + private static final String TAG = "DiscreteOpsSqlRegistry"; + + private final Context mContext; + private final DiscreteOpsDbHelper mDiscreteOpsDbHelper; + private final SqliteWriteHandler mSqliteWriteHandler; + private final DiscreteOpCache mDiscreteOpCache = new DiscreteOpCache(512); + private static final long THREE_HOURS = Duration.ofHours(3).toMillis(); + private static final int WRITE_CACHE_EVICTED_OP_EVENTS = 1; + private static final int DELETE_OLD_OP_EVENTS = 2; + // Attribution chain id is used to identify an attribution source chain, This is + // set for startOp only. PermissionManagerService resets this ID on device restart, so + // we use previously persisted chain id as offset, and add it to chain id received from + // permission manager service. + private long mChainIdOffset; + private final File mDatabaseFile; + + DiscreteOpsSqlRegistry(Context context) { + this(context, DiscreteOpsDbHelper.getDatabaseFile()); + } + + DiscreteOpsSqlRegistry(Context context, File databaseFile) { + ServiceThread thread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, true); + thread.start(); + mContext = context; + mDatabaseFile = databaseFile; + mSqliteWriteHandler = new SqliteWriteHandler(thread.getLooper()); + mDiscreteOpsDbHelper = new DiscreteOpsDbHelper(context, databaseFile); + mChainIdOffset = mDiscreteOpsDbHelper.getLargestAttributionChainId(); + } + + @Override + void recordDiscreteAccess(int uid, String packageName, + @NonNull String deviceId, int op, + @Nullable String attributionTag, int flags, int uidState, + long accessTime, long accessDuration, int attributionFlags, int attributionChainId, + int accessType) { + if (shouldLogAccess(op)) { + FrameworkStatsLog.write(FrameworkStatsLog.APP_OP_ACCESS_TRACKED, uid, op, accessType, + uidState, flags, attributionFlags, + getAttributionTag(attributionTag, packageName), + attributionChainId); + } + + if (!isDiscreteOp(op, flags)) { + return; + } + + long offsetChainId = attributionChainId; + if (attributionChainId != ATTRIBUTION_CHAIN_ID_NONE) { + offsetChainId = attributionChainId + mChainIdOffset; + // PermissionManagerService chain id reached the max value, + // reset offset, it's going to be very rare. + if (attributionChainId == Integer.MAX_VALUE) { + mChainIdOffset = offsetChainId; + } + } + DiscreteOp discreteOpEvent = new DiscreteOp(uid, packageName, attributionTag, deviceId, op, + flags, attributionFlags, uidState, offsetChainId, accessTime, accessDuration); + mDiscreteOpCache.add(discreteOpEvent); + } + + @Override + void writeAndClearOldAccessHistory() { + // Let the sql impl also follow the same disk write frequencies as xml, + // controlled by AppOpsService. + mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear()); + if (!mSqliteWriteHandler.hasMessages(DELETE_OLD_OP_EVENTS)) { + if (mSqliteWriteHandler.sendEmptyMessageDelayed(DELETE_OLD_OP_EVENTS, THREE_HOURS)) { + Slog.w(TAG, "DELETE_OLD_OP_EVENTS is not queued"); + } + } + } + + @Override + void clearHistory() { + mDiscreteOpCache.clear(); + mDiscreteOpsDbHelper.execSQL(DiscreteOpsTable.DELETE_TABLE_DATA); + } + + @Override + void clearHistory(int uid, String packageName) { + mDiscreteOpCache.clear(uid, packageName); + mDiscreteOpsDbHelper.execSQL(DiscreteOpsTable.DELETE_DATA_FOR_UID_PACKAGE, + new Object[]{uid, packageName}); + } + + @Override + void offsetHistory(long offset) { + mDiscreteOpCache.offsetTimestamp(offset); + mDiscreteOpsDbHelper.execSQL(DiscreteOpsTable.OFFSET_ACCESS_TIME, + new Object[]{offset}); + } + + private IntArray getAppOpCodes(@AppOpsManager.HistoricalOpsRequestFilter int filter, + @Nullable String[] opNamesFilter) { + if ((filter & AppOpsManager.FILTER_BY_OP_NAMES) != 0) { + IntArray opCodes = new IntArray(opNamesFilter.length); + for (int i = 0; i < opNamesFilter.length; i++) { + int op; + try { + op = AppOpsManager.strOpToOp(opNamesFilter[i]); + } catch (IllegalArgumentException ex) { + Slog.w(TAG, "Appop `" + opNamesFilter[i] + "` is not recognized."); + continue; + } + opCodes.add(op); + } + return opCodes; + } + return null; + } + + @Override + void addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result, + long beginTimeMillis, long endTimeMillis, int filter, int uidFilter, + @Nullable String packageNameFilter, + @Nullable String[] opNamesFilter, + @Nullable String attributionTagFilter, int opFlagsFilter, + Set<String> attributionExemptPkgs) { + // flush the cache into database before read. + writeAndClearOldAccessHistory(); + boolean assembleChains = attributionExemptPkgs != null; + IntArray opCodes = getAppOpCodes(filter, opNamesFilter); + List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter, + packageNameFilter, attributionTagFilter, opCodes, opFlagsFilter, beginTimeMillis, + endTimeMillis, -1, null); + + LongSparseArray<AttributionChain> attributionChains = null; + if (assembleChains) { + attributionChains = createAttributionChains(discreteOps, attributionExemptPkgs); + } + + int nEvents = discreteOps.size(); + for (int j = 0; j < nEvents; j++) { + DiscreteOp event = discreteOps.get(j); + AppOpsManager.OpEventProxyInfo proxy = null; + if (assembleChains && event.mChainId != ATTRIBUTION_CHAIN_ID_NONE) { + AttributionChain chain = attributionChains.get(event.mChainId); + if (chain != null && chain.isComplete() + && chain.isStart(event) + && chain.mLastVisibleEvent != null) { + DiscreteOp proxyEvent = chain.mLastVisibleEvent; + proxy = new AppOpsManager.OpEventProxyInfo(proxyEvent.mUid, + proxyEvent.mPackageName, proxyEvent.mAttributionTag); + } + } + result.addDiscreteAccess(event.mOpCode, event.mUid, event.mPackageName, + event.mAttributionTag, event.mUidState, event.mOpFlags, + event.mDiscretizedAccessTime, event.mDiscretizedDuration, proxy); + } + } + + @Override + void dump(@NonNull PrintWriter pw, int uidFilter, @Nullable String packageNameFilter, + @Nullable String attributionTagFilter, + @AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp, + @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix, + int nDiscreteOps) { + writeAndClearOldAccessHistory(); + IntArray opCodes = new IntArray(); + if (dumpOp != AppOpsManager.OP_NONE) { + opCodes.add(dumpOp); + } + List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter, + packageNameFilter, attributionTagFilter, opCodes, 0, -1, + -1, nDiscreteOps, DiscreteOpsTable.Columns.ACCESS_TIME); + + pw.print(prefix); + pw.print("Largest chain id: "); + pw.print(mDiscreteOpsDbHelper.getLargestAttributionChainId()); + pw.println(); + pw.println("UID|PACKAGE_NAME|DEVICE_ID|OP_NAME|ATTRIBUTION_TAG|UID_STATE|OP_FLAGS|" + + "ATTR_FLAGS|CHAIN_ID|ACCESS_TIME|DURATION"); + int discreteOpsCount = discreteOps.size(); + for (int i = 0; i < discreteOpsCount; i++) { + DiscreteOp event = discreteOps.get(i); + date.setTime(event.mAccessTime); + pw.println(event.mUid + "|" + event.mPackageName + "|" + event.mDeviceId + "|" + + AppOpsManager.opToName(event.mOpCode) + "|" + event.mAttributionTag + "|" + + getUidStateName(event.mUidState) + "|" + + flagsToString(event.mOpFlags) + "|" + event.mAttributionFlags + "|" + + event.mChainId + "|" + + sdf.format(date) + "|" + event.mDuration); + } + pw.println(); + } + + void migrateXmlData(List<DiscreteOp> opEvents, int chainIdOffset) { + mChainIdOffset = chainIdOffset; + mDiscreteOpsDbHelper.insertDiscreteOps(opEvents); + } + + LongSparseArray<AttributionChain> createAttributionChains( + List<DiscreteOp> discreteOps, Set<String> attributionExemptPkgs) { + LongSparseArray<AttributionChain> chains = new LongSparseArray<>(); + final int count = discreteOps.size(); + + for (int i = 0; i < count; i++) { + DiscreteOp opEvent = discreteOps.get(i); + if (opEvent.mChainId == ATTRIBUTION_CHAIN_ID_NONE + || (opEvent.mAttributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) { + continue; + } + AttributionChain chain = chains.get(opEvent.mChainId); + if (chain == null) { + chain = new AttributionChain(attributionExemptPkgs); + chains.put(opEvent.mChainId, chain); + } + chain.addEvent(opEvent); + } + return chains; + } + + static class AttributionChain { + List<DiscreteOp> mChain = new ArrayList<>(); + Set<String> mExemptPkgs; + DiscreteOp mStartEvent = null; + DiscreteOp mLastVisibleEvent = null; + + AttributionChain(Set<String> exemptPkgs) { + mExemptPkgs = exemptPkgs; + } + + boolean isComplete() { + return !mChain.isEmpty() && getStart() != null && isEnd(mChain.get(mChain.size() - 1)); + } + + DiscreteOp getStart() { + return mChain.isEmpty() || !isStart(mChain.get(0)) ? null : mChain.get(0); + } + + private boolean isEnd(DiscreteOp event) { + return event != null + && (event.mAttributionFlags & ATTRIBUTION_FLAG_ACCESSOR) != 0; + } + + private boolean isStart(DiscreteOp event) { + return event != null + && (event.mAttributionFlags & ATTRIBUTION_FLAG_RECEIVER) != 0; + } + + DiscreteOp getLastVisible() { + // Search all nodes but the first one, which is the start node + for (int i = mChain.size() - 1; i > 0; i--) { + DiscreteOp event = mChain.get(i); + if (!mExemptPkgs.contains(event.mPackageName)) { + return event; + } + } + return null; + } + + void addEvent(DiscreteOp opEvent) { + // check if we have a matching event except duration. + DiscreteOp matchingItem = null; + for (int i = 0; i < mChain.size(); i++) { + DiscreteOp item = mChain.get(i); + if (item.equalsExceptDuration(opEvent)) { + matchingItem = item; + break; + } + } + + if (matchingItem != null) { + // exact match or existing event has longer duration + if (matchingItem.mDuration == opEvent.mDuration + || matchingItem.mDuration > opEvent.mDuration) { + return; + } + mChain.remove(matchingItem); + } + + if (mChain.isEmpty() || isEnd(opEvent)) { + mChain.add(opEvent); + } else if (isStart(opEvent)) { + mChain.add(0, opEvent); + } else { + for (int i = 0; i < mChain.size(); i++) { + DiscreteOp currEvent = mChain.get(i); + if ((!isStart(currEvent) + && currEvent.mAccessTime > opEvent.mAccessTime) + || (i == mChain.size() - 1 && isEnd(currEvent))) { + mChain.add(i, opEvent); + break; + } else if (i == mChain.size() - 1) { + mChain.add(opEvent); + break; + } + } + } + mStartEvent = isComplete() ? getStart() : null; + mLastVisibleEvent = isComplete() ? getLastVisible() : null; + } + } + + /** + * Handler to write asynchronously to sqlite database. + */ + class SqliteWriteHandler extends Handler { + SqliteWriteHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case WRITE_CACHE_EVICTED_OP_EVENTS: + List<DiscreteOp> opEvents = (List<DiscreteOp>) msg.obj; + mDiscreteOpsDbHelper.insertDiscreteOps(opEvents); + break; + case DELETE_OLD_OP_EVENTS: + long cutOffTimeStamp = System.currentTimeMillis() - sDiscreteHistoryCutoff; + mDiscreteOpsDbHelper.execSQL( + DiscreteOpsTable.DELETE_TABLE_DATA_BEFORE_ACCESS_TIME, + new Object[]{cutOffTimeStamp}); + break; + default: + throw new IllegalStateException("Unexpected value: " + msg.what); + } + } + } + + /** + * A write cache for discrete ops. The noteOp, start/finishOp discrete op events are written to + * the cache first. + * <p> + * These events are persisted into sqlite database + * 1) Periodic interval, controlled by {@link AppOpsService} + * 2) When total events in the cache exceeds cache limit. + * 3) During read call we flush the whole cache to sqlite. + * 4) During shutdown. + */ + class DiscreteOpCache { + private final int mCapacity; + private final ArraySet<DiscreteOp> mCache; + + DiscreteOpCache(int capacity) { + mCapacity = capacity; + mCache = new ArraySet<>(); + } + + public void add(DiscreteOp opEvent) { + synchronized (this) { + if (mCache.contains(opEvent)) { + return; + } + mCache.add(opEvent); + if (mCache.size() >= mCapacity) { + if (DEBUG_LOG) { + Slog.i(TAG, "Current discrete ops cache size: " + mCache.size()); + } + List<DiscreteOp> evictedEvents = evict(); + if (DEBUG_LOG) { + Slog.i(TAG, "Evicted discrete ops size: " + evictedEvents.size()); + } + // if nothing to evict, just write the whole cache to disk + if (evictedEvents.isEmpty()) { + Slog.w(TAG, "No discrete ops event is evicted, write cache to db."); + evictedEvents.addAll(mCache); + mCache.clear(); + } + mSqliteWriteHandler.obtainMessage(WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents); + } + } + } + + /** + * Evict entries older than {@link DiscreteOpsRegistry#sDiscreteHistoryQuantization}. + */ + private List<DiscreteOp> evict() { + synchronized (this) { + List<DiscreteOp> evictedEvents = new ArrayList<>(); + Set<DiscreteOp> snapshot = new ArraySet<>(mCache); + long evictionTimestamp = System.currentTimeMillis() - sDiscreteHistoryQuantization; + evictionTimestamp = discretizeTimeStamp(evictionTimestamp); + for (DiscreteOp opEvent : snapshot) { + if (opEvent.mDiscretizedAccessTime <= evictionTimestamp) { + evictedEvents.add(opEvent); + mCache.remove(opEvent); + } + } + return evictedEvents; + } + } + + /** + * Remove all the entries from cache. + * + * @return return all removed entries. + */ + public List<DiscreteOp> getAllEventsAndClear() { + synchronized (this) { + List<DiscreteOp> cachedOps = new ArrayList<>(mCache.size()); + if (mCache.isEmpty()) { + return cachedOps; + } + cachedOps.addAll(mCache); + mCache.clear(); + return cachedOps; + } + } + + /** + * Remove all entries from the cache. + */ + public void clear() { + synchronized (this) { + mCache.clear(); + } + } + + /** + * Offset access time by given offset milliseconds. + */ + public void offsetTimestamp(long offsetMillis) { + synchronized (this) { + List<DiscreteOp> cachedOps = new ArrayList<>(mCache); + mCache.clear(); + for (DiscreteOp discreteOp : cachedOps) { + add(new DiscreteOp(discreteOp.getUid(), discreteOp.mPackageName, + discreteOp.getAttributionTag(), discreteOp.getDeviceId(), + discreteOp.mOpCode, discreteOp.mOpFlags, + discreteOp.getAttributionFlags(), discreteOp.getUidState(), + discreteOp.getChainId(), discreteOp.mAccessTime - offsetMillis, + discreteOp.getDuration()) + ); + } + } + } + + /** Remove cached events for given UID and package. */ + public void clear(int uid, String packageName) { + synchronized (this) { + Set<DiscreteOp> snapshot = new ArraySet<>(mCache); + for (DiscreteOp currentEvent : snapshot) { + if (Objects.equals(packageName, currentEvent.mPackageName) + && uid == currentEvent.getUid()) { + mCache.remove(currentEvent); + } + } + } + } + } + + /** Immutable discrete op object. */ + static class DiscreteOp { + private final int mUid; + private final String mPackageName; + private final String mAttributionTag; + private final String mDeviceId; + private final int mOpCode; + private final int mOpFlags; + private final int mAttributionFlags; + private final int mUidState; + private final long mChainId; + private final long mAccessTime; + private final long mDuration; + // store discretized timestamp to avoid repeated calculations. + private final long mDiscretizedAccessTime; + private final long mDiscretizedDuration; + + DiscreteOp(int uid, String packageName, String attributionTag, String deviceId, + int opCode, + int mOpFlags, int mAttributionFlags, int uidState, long chainId, long accessTime, + long duration) { + this.mUid = uid; + this.mPackageName = packageName.intern(); + this.mAttributionTag = attributionTag; + this.mDeviceId = deviceId; + this.mOpCode = opCode; + this.mOpFlags = mOpFlags; + this.mAttributionFlags = mAttributionFlags; + this.mUidState = uidState; + this.mChainId = chainId; + this.mAccessTime = accessTime; + this.mDiscretizedAccessTime = discretizeTimeStamp(accessTime); + this.mDuration = duration; + this.mDiscretizedDuration = discretizeDuration(duration); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DiscreteOp that)) return false; + + if (mUid != that.mUid) return false; + if (mOpCode != that.mOpCode) return false; + if (mOpFlags != that.mOpFlags) return false; + if (mAttributionFlags != that.mAttributionFlags) return false; + if (mUidState != that.mUidState) return false; + if (mChainId != that.mChainId) return false; + if (!Objects.equals(mPackageName, that.mPackageName)) { + return false; + } + if (!Objects.equals(mAttributionTag, that.mAttributionTag)) { + return false; + } + if (!Objects.equals(mDeviceId, that.mDeviceId)) { + return false; + } + if (mDiscretizedAccessTime != that.mDiscretizedAccessTime) { + return false; + } + return mDiscretizedDuration == that.mDiscretizedDuration; + } + + @Override + public int hashCode() { + int result = mUid; + result = 31 * result + (mPackageName != null ? mPackageName.hashCode() : 0); + result = 31 * result + (mAttributionTag != null ? mAttributionTag.hashCode() : 0); + result = 31 * result + (mDeviceId != null ? mDeviceId.hashCode() : 0); + result = 31 * result + mOpCode; + result = 31 * result + mOpFlags; + result = 31 * result + mAttributionFlags; + result = 31 * result + mUidState; + result = 31 * result + Objects.hash(mChainId); + result = 31 * result + Objects.hash(mDiscretizedAccessTime); + result = 31 * result + Objects.hash(mDiscretizedDuration); + return result; + } + + public boolean equalsExceptDuration(DiscreteOp that) { + if (mUid != that.mUid) return false; + if (mOpCode != that.mOpCode) return false; + if (mOpFlags != that.mOpFlags) return false; + if (mAttributionFlags != that.mAttributionFlags) return false; + if (mUidState != that.mUidState) return false; + if (mChainId != that.mChainId) return false; + if (!Objects.equals(mPackageName, that.mPackageName)) { + return false; + } + if (!Objects.equals(mAttributionTag, that.mAttributionTag)) { + return false; + } + if (!Objects.equals(mDeviceId, that.mDeviceId)) { + return false; + } + return mAccessTime == that.mAccessTime; + } + + @Override + public String toString() { + return "DiscreteOp{" + + "uid=" + mUid + + ", packageName='" + mPackageName + '\'' + + ", attributionTag='" + mAttributionTag + '\'' + + ", deviceId='" + mDeviceId + '\'' + + ", opCode=" + AppOpsManager.opToName(mOpCode) + + ", opFlag=" + flagsToString(mOpFlags) + + ", attributionFlag=" + mAttributionFlags + + ", uidState=" + getUidStateName(mUidState) + + ", chainId=" + mChainId + + ", accessTime=" + mAccessTime + + ", duration=" + mDuration + '}'; + } + + public int getUid() { + return mUid; + } + + public String getPackageName() { + return mPackageName; + } + + public String getAttributionTag() { + return mAttributionTag; + } + + public String getDeviceId() { + return mDeviceId; + } + + public int getOpCode() { + return mOpCode; + } + + @AppOpsManager.OpFlags + public int getOpFlags() { + return mOpFlags; + } + + + @AppOpsManager.AttributionFlags + public int getAttributionFlags() { + return mAttributionFlags; + } + + @AppOpsManager.UidState + public int getUidState() { + return mUidState; + } + + public long getChainId() { + return mChainId; + } + + public long getAccessTime() { + return mAccessTime; + } + + public long getDuration() { + return mDuration; + } + } + + // API for tests only, can be removed or changed. + void recordDiscreteAccess(DiscreteOp discreteOpEvent) { + mDiscreteOpCache.add(discreteOpEvent); + } + + // API for tests only, can be removed or changed. + List<DiscreteOp> getCachedDiscreteOps() { + return new ArrayList<>(mDiscreteOpCache.mCache); + } + + // API for tests only, can be removed or changed. + List<DiscreteOp> getAllDiscreteOps() { + List<DiscreteOp> ops = new ArrayList<>(mDiscreteOpCache.mCache); + ops.addAll(mDiscreteOpsDbHelper.getAllDiscreteOps(DiscreteOpsTable.SELECT_TABLE_DATA)); + return ops; + } + + // API for testing and migration + long getLargestAttributionChainId() { + return mDiscreteOpsDbHelper.getLargestAttributionChainId(); + } + + // API for testing and migration + void deleteDatabase() { + mDiscreteOpsDbHelper.close(); + mContext.deleteDatabase(mDatabaseFile.getName()); + } +} diff --git a/services/core/java/com/android/server/appop/DiscreteOpsTable.java b/services/core/java/com/android/server/appop/DiscreteOpsTable.java new file mode 100644 index 000000000000..9cb19aa30a15 --- /dev/null +++ b/services/core/java/com/android/server/appop/DiscreteOpsTable.java @@ -0,0 +1,128 @@ +/* + * 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.appop; + + +/** + * SQLite table for storing app op accesses. + */ +final class DiscreteOpsTable { + private static final String TABLE_NAME = "app_op_accesses"; + private static final String INDEX_APP_OP = "app_op_access_index"; + + static final class Columns { + /** Auto increment primary key. */ + static final String ID = "id"; + /** UID of the package accessing private data. */ + static final String UID = "uid"; + /** Package accessing private data. */ + static final String PACKAGE_NAME = "package_name"; + /** The device from which the private data is accessed. */ + static final String DEVICE_ID = "device_id"; + /** Op code representing private data i.e. location, mic etc. */ + static final String OP_CODE = "op_code"; + /** Attribution tag provided when accessing the private data. */ + static final String ATTRIBUTION_TAG = "attribution_tag"; + /** Timestamp when private data is accessed, number of milliseconds that have passed + * since Unix epoch */ + static final String ACCESS_TIME = "access_time"; + /** For how long the private data is accessed. */ + static final String ACCESS_DURATION = "access_duration"; + /** App process state, whether the app is in foreground, background or cached etc. */ + static final String UID_STATE = "uid_state"; + /** App op flags */ + static final String OP_FLAGS = "op_flags"; + /** Attribution flags */ + static final String ATTRIBUTION_FLAGS = "attribution_flags"; + /** Chain id */ + static final String CHAIN_ID = "chain_id"; + } + + static final int UID_INDEX = 1; + static final int PACKAGE_NAME_INDEX = 2; + static final int DEVICE_ID_INDEX = 3; + static final int OP_CODE_INDEX = 4; + static final int ATTRIBUTION_TAG_INDEX = 5; + static final int ACCESS_TIME_INDEX = 6; + static final int ACCESS_DURATION_INDEX = 7; + static final int UID_STATE_INDEX = 8; + static final int OP_FLAGS_INDEX = 9; + static final int ATTRIBUTION_FLAGS_INDEX = 10; + static final int CHAIN_ID_INDEX = 11; + + static final String CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS " + + TABLE_NAME + "(" + + Columns.ID + " INTEGER PRIMARY KEY," + + Columns.UID + " INTEGER," + + Columns.PACKAGE_NAME + " TEXT," + + Columns.DEVICE_ID + " TEXT NOT NULL," + + Columns.OP_CODE + " INTEGER," + + Columns.ATTRIBUTION_TAG + " TEXT," + + Columns.ACCESS_TIME + " INTEGER," + + Columns.ACCESS_DURATION + " INTEGER," + + Columns.UID_STATE + " INTEGER," + + Columns.OP_FLAGS + " INTEGER," + + Columns.ATTRIBUTION_FLAGS + " INTEGER," + + Columns.CHAIN_ID + " INTEGER" + + ")"; + + static final String INSERT_TABLE_SQL = "INSERT INTO " + TABLE_NAME + "(" + + Columns.UID + ", " + + Columns.PACKAGE_NAME + ", " + + Columns.DEVICE_ID + ", " + + Columns.OP_CODE + ", " + + Columns.ATTRIBUTION_TAG + ", " + + Columns.ACCESS_TIME + ", " + + Columns.ACCESS_DURATION + ", " + + Columns.UID_STATE + ", " + + Columns.OP_FLAGS + ", " + + Columns.ATTRIBUTION_FLAGS + ", " + + Columns.CHAIN_ID + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + static final String SELECT_MAX_ATTRIBUTION_CHAIN_ID = "SELECT MAX(" + Columns.CHAIN_ID + ")" + + " FROM " + TABLE_NAME; + + static final String SELECT_TABLE_DATA = "SELECT DISTINCT " + + Columns.UID + "," + + Columns.PACKAGE_NAME + "," + + Columns.DEVICE_ID + "," + + Columns.OP_CODE + "," + + Columns.ATTRIBUTION_TAG + "," + + Columns.ACCESS_TIME + "," + + Columns.ACCESS_DURATION + "," + + Columns.UID_STATE + "," + + Columns.OP_FLAGS + "," + + Columns.ATTRIBUTION_FLAGS + "," + + Columns.CHAIN_ID + + " FROM " + TABLE_NAME; + + static final String DELETE_TABLE_DATA = "DELETE FROM " + TABLE_NAME; + + static final String DELETE_TABLE_DATA_BEFORE_ACCESS_TIME = "DELETE FROM " + TABLE_NAME + + " WHERE " + Columns.ACCESS_TIME + " < ?"; + + static final String DELETE_DATA_FOR_UID_PACKAGE = "DELETE FROM " + DiscreteOpsTable.TABLE_NAME + + " WHERE " + Columns.UID + " = ? AND " + Columns.PACKAGE_NAME + " = ?"; + + static final String OFFSET_ACCESS_TIME = "UPDATE " + DiscreteOpsTable.TABLE_NAME + + " SET " + Columns.ACCESS_TIME + " = ACCESS_TIME - ?"; + + // Index on access time, uid and op code + static final String CREATE_INDEX_SQL = "CREATE INDEX IF NOT EXISTS " + + INDEX_APP_OP + " ON " + TABLE_NAME + + " (" + Columns.ACCESS_TIME + ", " + Columns.UID + ", " + Columns.OP_CODE + ")"; +} diff --git a/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java b/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java new file mode 100644 index 000000000000..1523cca86607 --- /dev/null +++ b/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java @@ -0,0 +1,220 @@ +/* + * 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.appop; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AppOpsManager; +import android.os.SystemClock; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; + +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Objects; +import java.util.Set; + +/** + * A testing class, which supports both xml and sqlite persistence for discrete ops, the class + * logs warning if there is a mismatch in the behavior. + */ +class DiscreteOpsTestingShim extends DiscreteOpsRegistry { + private static final String LOG_TAG = "DiscreteOpsTestingShim"; + private final DiscreteOpsRegistry mXmlRegistry; + private final DiscreteOpsRegistry mSqlRegistry; + + DiscreteOpsTestingShim(DiscreteOpsRegistry xmlRegistry, + DiscreteOpsRegistry sqlRegistry) { + mXmlRegistry = xmlRegistry; + mSqlRegistry = sqlRegistry; + } + + @Override + void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, + @Nullable String attributionTag, int flags, int uidState, long accessTime, + long accessDuration, int attributionFlags, int attributionChainId, int accessType) { + long start = SystemClock.uptimeMillis(); + mXmlRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags, + uidState, accessTime, accessDuration, attributionFlags, attributionChainId, + accessType); + long start2 = SystemClock.uptimeMillis(); + mSqlRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags, + uidState, accessTime, accessDuration, attributionFlags, attributionChainId, + accessType); + long end = SystemClock.uptimeMillis(); + long xmlTimeTaken = start2 - start; + long sqlTimeTaken = end - start2; + Log.i(LOG_TAG, + "recordDiscreteAccess: XML time taken : " + xmlTimeTaken + ", SQL time taken : " + + sqlTimeTaken + ", diff (sql - xml): " + (sqlTimeTaken - xmlTimeTaken)); + } + + + @Override + void writeAndClearOldAccessHistory() { + mXmlRegistry.writeAndClearOldAccessHistory(); + mSqlRegistry.writeAndClearOldAccessHistory(); + } + + @Override + void clearHistory() { + mXmlRegistry.clearHistory(); + mSqlRegistry.clearHistory(); + } + + @Override + void clearHistory(int uid, String packageName) { + mXmlRegistry.clearHistory(uid, packageName); + mSqlRegistry.clearHistory(uid, packageName); + } + + @Override + void offsetHistory(long offset) { + mXmlRegistry.offsetHistory(offset); + mSqlRegistry.offsetHistory(offset); + } + + @Override + void addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result, + long beginTimeMillis, long endTimeMillis, int filter, int uidFilter, + @Nullable String packageNameFilter, @Nullable String[] opNamesFilter, + @Nullable String attributionTagFilter, int flagsFilter, + Set<String> attributionExemptPkgs) { + AppOpsManager.HistoricalOps result2 = + new AppOpsManager.HistoricalOps(beginTimeMillis, endTimeMillis); + + long start = System.currentTimeMillis(); + mXmlRegistry.addFilteredDiscreteOpsToHistoricalOps(result2, beginTimeMillis, endTimeMillis, + filter, uidFilter, packageNameFilter, opNamesFilter, attributionTagFilter, + flagsFilter, attributionExemptPkgs); + long start2 = System.currentTimeMillis(); + mSqlRegistry.addFilteredDiscreteOpsToHistoricalOps(result, beginTimeMillis, endTimeMillis, + filter, uidFilter, packageNameFilter, opNamesFilter, attributionTagFilter, + flagsFilter, attributionExemptPkgs); + long end = System.currentTimeMillis(); + long xmlTimeTaken = start2 - start; + long sqlTimeTaken = end - start2; + try { + assertHistoricalOpsAreEquals(result, result2); + } catch (Exception ex) { + Slog.e(LOG_TAG, "different output when reading discrete ops", ex); + } + Log.i(LOG_TAG, "Read: XML time taken : " + xmlTimeTaken + ", SQL time taken : " + + sqlTimeTaken + ", diff (sql - xml): " + (sqlTimeTaken - xmlTimeTaken)); + } + + void assertHistoricalOpsAreEquals(AppOpsManager.HistoricalOps sqlResult, + AppOpsManager.HistoricalOps xmlResult) { + assertEquals(sqlResult.getUidCount(), xmlResult.getUidCount()); + int uidCount = sqlResult.getUidCount(); + + for (int i = 0; i < uidCount; i++) { + AppOpsManager.HistoricalUidOps sqlUidOps = sqlResult.getUidOpsAt(i); + AppOpsManager.HistoricalUidOps xmlUidOps = xmlResult.getUidOpsAt(i); + Slog.i(LOG_TAG, "sql uid: " + sqlUidOps.getUid() + ", xml uid: " + xmlUidOps.getUid()); + assertEquals(sqlUidOps.getUid(), xmlUidOps.getUid()); + assertEquals(sqlUidOps.getPackageCount(), xmlUidOps.getPackageCount()); + + int packageCount = sqlUidOps.getPackageCount(); + for (int p = 0; p < packageCount; p++) { + AppOpsManager.HistoricalPackageOps sqlPackageOps = sqlUidOps.getPackageOpsAt(p); + AppOpsManager.HistoricalPackageOps xmlPackageOps = xmlUidOps.getPackageOpsAt(p); + Slog.i(LOG_TAG, "sql package: " + sqlPackageOps.getPackageName() + ", xml package: " + + xmlPackageOps.getPackageName()); + assertEquals(sqlPackageOps.getPackageName(), xmlPackageOps.getPackageName()); + assertEquals(sqlPackageOps.getAttributedOpsCount(), + xmlPackageOps.getAttributedOpsCount()); + + int attrCount = sqlPackageOps.getAttributedOpsCount(); + for (int a = 0; a < attrCount; a++) { + AppOpsManager.AttributedHistoricalOps sqlAttrOps = + sqlPackageOps.getAttributedOpsAt(a); + AppOpsManager.AttributedHistoricalOps xmlAttrOps = + xmlPackageOps.getAttributedOpsAt(a); + Slog.i(LOG_TAG, "sql tag: " + sqlAttrOps.getTag() + ", xml tag: " + + xmlAttrOps.getTag()); + assertEquals(sqlAttrOps.getTag(), xmlAttrOps.getTag()); + assertEquals(sqlAttrOps.getOpCount(), xmlAttrOps.getOpCount()); + + int opCount = sqlAttrOps.getOpCount(); + for (int o = 0; o < opCount; o++) { + AppOpsManager.HistoricalOp sqlHistoricalOp = sqlAttrOps.getOpAt(o); + AppOpsManager.HistoricalOp xmlHistoricalOp = xmlAttrOps.getOpAt(o); + Slog.i(LOG_TAG, "sql op: " + sqlHistoricalOp.getOpName() + ", xml op: " + + xmlHistoricalOp.getOpName()); + assertEquals(sqlHistoricalOp.getOpName(), xmlHistoricalOp.getOpName()); + assertEquals(sqlHistoricalOp.getDiscreteAccessCount(), + xmlHistoricalOp.getDiscreteAccessCount()); + + int accessCount = sqlHistoricalOp.getDiscreteAccessCount(); + for (int x = 0; x < accessCount; x++) { + AppOpsManager.AttributedOpEntry sqlOpEntry = + sqlHistoricalOp.getDiscreteAccessAt(x); + AppOpsManager.AttributedOpEntry xmlOpEntry = + xmlHistoricalOp.getDiscreteAccessAt(x); + Slog.i(LOG_TAG, "sql keys: " + sqlOpEntry.collectKeys() + ", xml keys: " + + xmlOpEntry.collectKeys()); + assertEquals(sqlOpEntry.collectKeys(), xmlOpEntry.collectKeys()); + assertEquals(sqlOpEntry.isRunning(), xmlOpEntry.isRunning()); + ArraySet<Long> keys = sqlOpEntry.collectKeys(); + final int keyCount = keys.size(); + for (int k = 0; k < keyCount; k++) { + final long key = keys.valueAt(k); + final int flags = extractFlagsFromKey(key); + assertEquals(sqlOpEntry.getLastDuration(flags), + xmlOpEntry.getLastDuration(flags)); + assertEquals(sqlOpEntry.getLastProxyInfo(flags), + xmlOpEntry.getLastProxyInfo(flags)); + assertEquals(sqlOpEntry.getLastAccessTime(flags), + xmlOpEntry.getLastAccessTime(flags)); + } + } + } + } + } + } + } + + // code duplicated for assertions + private static final int FLAGS_MASK = 0xFFFFFFFF; + + public static int extractFlagsFromKey(@AppOpsManager.DataBucketKey long key) { + return (int) (key & FLAGS_MASK); + } + + private void assertEquals(Object actual, Object expected) { + if (!Objects.equals(actual, expected)) { + throw new IllegalStateException("Actual (" + actual + ") is not equal to expected (" + + expected + ")"); + } + } + + @Override + void dump(@NonNull PrintWriter pw, int uidFilter, @Nullable String packageNameFilter, + @Nullable String attributionTagFilter, int filter, int dumpOp, + @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix, + int nDiscreteOps) { + mXmlRegistry.dump(pw, uidFilter, packageNameFilter, attributionTagFilter, filter, dumpOp, + sdf, date, prefix, nDiscreteOps); + pw.println("--------------------------------------------------------"); + pw.println("--------------------------------------------------------"); + mSqlRegistry.dump(pw, uidFilter, packageNameFilter, attributionTagFilter, filter, dumpOp, + sdf, date, prefix, nDiscreteOps); + } +} diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java index 7f161f618618..a6e3fc7cc66a 100644 --- a/services/core/java/com/android/server/appop/DiscreteRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java @@ -24,48 +24,20 @@ import static android.app.AppOpsManager.FILTER_BY_ATTRIBUTION_TAG; import static android.app.AppOpsManager.FILTER_BY_OP_NAMES; import static android.app.AppOpsManager.FILTER_BY_PACKAGE_NAME; import static android.app.AppOpsManager.FILTER_BY_UID; -import static android.app.AppOpsManager.OP_CAMERA; -import static android.app.AppOpsManager.OP_COARSE_LOCATION; -import static android.app.AppOpsManager.OP_EMERGENCY_LOCATION; -import static android.app.AppOpsManager.OP_FINE_LOCATION; import static android.app.AppOpsManager.OP_FLAGS_ALL; -import static android.app.AppOpsManager.OP_FLAG_SELF; -import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED; -import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY; -import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION; -import static android.app.AppOpsManager.OP_MONITOR_LOCATION; import static android.app.AppOpsManager.OP_NONE; -import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA; -import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE; -import static android.app.AppOpsManager.OP_PROCESS_OUTGOING_CALLS; -import static android.app.AppOpsManager.OP_READ_ICC_SMS; -import static android.app.AppOpsManager.OP_READ_SMS; -import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; -import static android.app.AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO; -import static android.app.AppOpsManager.OP_RECORD_AUDIO; -import static android.app.AppOpsManager.OP_RESERVED_FOR_TESTING; -import static android.app.AppOpsManager.OP_SEND_SMS; -import static android.app.AppOpsManager.OP_SMS_FINANCIAL_TRANSACTIONS; -import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW; -import static android.app.AppOpsManager.OP_WRITE_ICC_SMS; -import static android.app.AppOpsManager.OP_WRITE_SMS; import static android.app.AppOpsManager.flagsToString; import static android.app.AppOpsManager.getUidStateName; import static android.companion.virtual.VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT; -import static java.lang.Long.min; import static java.lang.Math.max; -import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppOpsManager; -import android.os.AsyncTask; -import android.os.Build; import android.os.Environment; import android.os.FileUtils; import android.permission.flags.Flags; -import android.provider.DeviceConfig; import android.util.ArrayMap; import android.util.AtomicFile; import android.util.Slog; @@ -84,10 +56,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.text.SimpleDateFormat; -import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -99,100 +68,30 @@ import java.util.Objects; import java.util.Set; /** - * This class manages information about recent accesses to ops for permission usage timeline. - * - * The discrete history is kept for limited time (initial default is 24 hours, set in - * {@link DiscreteRegistry#sDiscreteHistoryCutoff) and discarded after that. - * - * Discrete history is quantized to reduce resources footprint. By default quantization is set to - * one minute in {@link DiscreteRegistry#sDiscreteHistoryQuantization}. All access times are aligned - * to the closest quantized time. All durations (except -1, meaning no duration) are rounded up to - * the closest quantized interval. - * - * When data is queried through API, events are deduplicated and for every time quant there can - * be only one {@link AppOpsManager.AttributedOpEntry}. Each entry contains information about - * different accesses which happened in specified time quant - across dimensions of - * {@link AppOpsManager.UidState} and {@link AppOpsManager.OpFlags}. For each dimension - * it is only possible to know if at least one access happened in the time quant. + * Xml persistence implementation for discrete ops. * + * <p> * Every time state is saved (default is 30 minutes), memory state is dumped to a * new file and memory state is cleared. Files older than time limit are deleted * during the process. - * + * <p> * When request comes in, files are read and requested information is collected * and delivered. Information is cached in memory until the next state save (up to 30 minutes), to * avoid reading disk if more API calls come in a quick succession. - * + * <p> * THREADING AND LOCKING: - * For in-memory transactions this class relies on {@link DiscreteRegistry#mInMemoryLock}. It is - * assumed that the same lock is used for in-memory transactions in {@link AppOpsService}, - * {@link HistoricalRegistry}, and {@link DiscreteRegistry}. - * {@link DiscreteRegistry#recordDiscreteAccess(int, String, int, String, int, int, long, long)} - * must only be called while holding this lock. - * {@link DiscreteRegistry#mOnDiskLock} is used when disk transactions are performed. - * It is very important to release {@link DiscreteRegistry#mInMemoryLock} as soon as possible, as - * no AppOps related transactions across the system can be performed while it is held. + * For in-memory transactions this class relies on {@link DiscreteOpsXmlRegistry#mInMemoryLock}. + * It is assumed that the same lock is used for in-memory transactions in {@link AppOpsService}, + * {@link HistoricalRegistry}, and {@link DiscreteOpsXmlRegistry }. + * {@link DiscreteOpsRegistry#recordDiscreteAccess} must only be called while holding this lock. + * {@link DiscreteOpsXmlRegistry#mOnDiskLock} is used when disk transactions are performed. + * It is very important to release {@link DiscreteOpsXmlRegistry#mInMemoryLock} as soon as + * possible, as no AppOps related transactions across the system can be performed while it is held. * - * INITIALIZATION: We can initialize persistence only after the system is ready - * as we need to check the optional configuration override from the settings - * database which is not initialized at the time the app ops service is created. This class - * relies on {@link HistoricalRegistry} for controlling that no calls are allowed until then. All - * outside calls are going through {@link HistoricalRegistry}, where - * {@link HistoricalRegistry#isPersistenceInitializedMLocked()} check is done. */ - -final class DiscreteRegistry { +class DiscreteOpsXmlRegistry extends DiscreteOpsRegistry { static final String DISCRETE_HISTORY_FILE_SUFFIX = "tl"; - private static final String TAG = DiscreteRegistry.class.getSimpleName(); - - private static final String PROPERTY_DISCRETE_HISTORY_CUTOFF = "discrete_history_cutoff_millis"; - private static final String PROPERTY_DISCRETE_HISTORY_QUANTIZATION = - "discrete_history_quantization_millis"; - private static final String PROPERTY_DISCRETE_FLAGS = "discrete_history_op_flags"; - private static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist"; - private static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION - + "," + OP_EMERGENCY_LOCATION + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + "," - + OP_PHONE_CALL_MICROPHONE + "," + OP_PHONE_CALL_CAMERA + "," - + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO + "," + OP_RECEIVE_SANDBOX_TRIGGER_AUDIO - + "," + OP_RESERVED_FOR_TESTING; - private static final int[] sDiscreteOpsToLog = - new int[]{OP_FINE_LOCATION, OP_COARSE_LOCATION, OP_EMERGENCY_LOCATION, OP_CAMERA, - OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE, OP_PHONE_CALL_CAMERA, - OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_READ_SMS, - OP_WRITE_SMS, OP_SEND_SMS, OP_READ_ICC_SMS, OP_WRITE_ICC_SMS, - OP_SMS_FINANCIAL_TRANSACTIONS, OP_SYSTEM_ALERT_WINDOW, OP_MONITOR_LOCATION, - OP_MONITOR_HIGH_POWER_LOCATION, OP_PROCESS_OUTGOING_CALLS, - }; - private static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis(); - private static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis(); - private static final long DEFAULT_DISCRETE_HISTORY_QUANTIZATION = - Duration.ofMinutes(1).toMillis(); - - static final int ACCESS_TYPE_NOTE_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__NOTE_OP; - static final int ACCESS_TYPE_START_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__START_OP; - static final int ACCESS_TYPE_FINISH_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__FINISH_OP; - static final int ACCESS_TYPE_PAUSE_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__PAUSE_OP; - static final int ACCESS_TYPE_RESUME_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__RESUME_OP; - - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = {"ACCESS_TYPE_"}, value = { - ACCESS_TYPE_NOTE_OP, - ACCESS_TYPE_START_OP, - ACCESS_TYPE_FINISH_OP, - ACCESS_TYPE_PAUSE_OP, - ACCESS_TYPE_RESUME_OP - }) - public @interface AccessType {} - - private static long sDiscreteHistoryCutoff; - private static long sDiscreteHistoryQuantization; - private static int[] sDiscreteOps; - private static int sDiscreteFlags; + private static final String TAG = DiscreteOpsXmlRegistry.class.getSimpleName(); private static final String TAG_HISTORY = "h"; private static final String ATTR_VERSION = "v"; @@ -221,9 +120,6 @@ final class DiscreteRegistry { private static final String ATTR_ATTRIBUTION_FLAGS = "af"; private static final String ATTR_CHAIN_ID = "ci"; - private static final int OP_FLAGS_DISCRETE = OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED - | OP_FLAG_TRUSTED_PROXY; - // Lock for read/write access to on disk state private final Object mOnDiskLock = new Object(); @@ -239,14 +135,12 @@ final class DiscreteRegistry { @GuardedBy("mOnDiskLock") private DiscreteOps mCachedOps = null; - private boolean mDebugMode = false; - - DiscreteRegistry(Object inMemoryLock) { - this(inMemoryLock, new File(new File(Environment.getDataSystemDirectory(), "appops"), - "discrete")); + DiscreteOpsXmlRegistry(Object inMemoryLock) { + this(inMemoryLock, getDiscreteOpsDir()); } - DiscreteRegistry(Object inMemoryLock, File discreteAccessDir) { + // constructor for tests. + DiscreteOpsXmlRegistry(Object inMemoryLock, File discreteAccessDir) { mInMemoryLock = inMemoryLock; synchronized (mOnDiskLock) { mDiscreteAccessDir = discreteAccessDir; @@ -258,40 +152,8 @@ final class DiscreteRegistry { } } - void systemReady() { - DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY, - AsyncTask.THREAD_POOL_EXECUTOR, (DeviceConfig.Properties p) -> { - setDiscreteHistoryParameters(p); - }); - setDiscreteHistoryParameters(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_PRIVACY)); - } - - private void setDiscreteHistoryParameters(DeviceConfig.Properties p) { - if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_CUTOFF)) { - sDiscreteHistoryCutoff = p.getLong(PROPERTY_DISCRETE_HISTORY_CUTOFF, - DEFAULT_DISCRETE_HISTORY_CUTOFF); - if (!Build.IS_DEBUGGABLE && !mDebugMode) { - sDiscreteHistoryCutoff = min(MAXIMUM_DISCRETE_HISTORY_CUTOFF, - sDiscreteHistoryCutoff); - } - } else { - sDiscreteHistoryCutoff = DEFAULT_DISCRETE_HISTORY_CUTOFF; - } - if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_QUANTIZATION)) { - sDiscreteHistoryQuantization = p.getLong(PROPERTY_DISCRETE_HISTORY_QUANTIZATION, - DEFAULT_DISCRETE_HISTORY_QUANTIZATION); - if (!Build.IS_DEBUGGABLE && !mDebugMode) { - sDiscreteHistoryQuantization = max(DEFAULT_DISCRETE_HISTORY_QUANTIZATION, - sDiscreteHistoryQuantization); - } - } else { - sDiscreteHistoryQuantization = DEFAULT_DISCRETE_HISTORY_QUANTIZATION; - } - sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS) ? sDiscreteFlags = - p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE; - sDiscreteOps = p.getKeyset().contains(PROPERTY_DISCRETE_OPS_LIST) ? parseOpsList( - p.getString(PROPERTY_DISCRETE_OPS_LIST, DEFAULT_DISCRETE_OPS)) : parseOpsList( - DEFAULT_DISCRETE_OPS); + static File getDiscreteOpsDir() { + return new File(new File(Environment.getDataSystemDirectory(), "appops"), "discrete"); } void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, @@ -300,17 +162,9 @@ final class DiscreteRegistry { @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, @AccessType int accessType) { if (shouldLogAccess(op)) { - int firstChar = 0; - if (attributionTag != null && attributionTag.startsWith(packageName)) { - firstChar = packageName.length(); - if (firstChar < attributionTag.length() && attributionTag.charAt(firstChar) - == '.') { - firstChar++; - } - } FrameworkStatsLog.write(FrameworkStatsLog.APP_OP_ACCESS_TRACKED, uid, op, accessType, uidState, flags, attributionFlags, - attributionTag == null ? null : attributionTag.substring(firstChar), + getAttributionTag(attributionTag, packageName), attributionChainId); } @@ -331,7 +185,7 @@ final class DiscreteRegistry { } } - void writeAndClearAccessHistory() { + void writeAndClearOldAccessHistory() { synchronized (mOnDiskLock) { if (mDiscreteAccessDir == null) { Slog.d(TAG, "State not saved - persistence not initialized."); @@ -350,6 +204,22 @@ final class DiscreteRegistry { } } + void migrateSqliteData(DiscreteOps sqliteOps) { + synchronized (mOnDiskLock) { + if (mDiscreteAccessDir == null) { + Slog.d(TAG, "State not saved - persistence not initialized."); + return; + } + synchronized (mInMemoryLock) { + mDiscreteOps.mLargestChainId = sqliteOps.mLargestChainId; + mDiscreteOps.mChainIdOffset = sqliteOps.mChainIdOffset; + } + if (!sqliteOps.isEmpty()) { + persistDiscreteOpsLocked(sqliteOps); + } + } + } + void addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result, long beginTimeMillis, long endTimeMillis, @AppOpsManager.HistoricalOpsRequestFilter int filter, int uidFilter, @@ -369,7 +239,7 @@ final class DiscreteRegistry { discreteOps.applyToHistoricalOps(result, attributionChains); } - private int readLargestChainIdFromDiskLocked() { + int readLargestChainIdFromDiskLocked() { final File[] files = mDiscreteAccessDir.listFiles(); if (files != null && files.length > 0) { File latestFile = null; @@ -497,6 +367,13 @@ final class DiscreteRegistry { } } + void deleteDiscreteOpsDir() { + synchronized (mOnDiskLock) { + mCachedOps = null; + FileUtils.deleteContentsAndDir(mDiscreteAccessDir); + } + } + void clearHistory(int uid, String packageName) { synchronized (mOnDiskLock) { DiscreteOps discreteOps; @@ -1506,26 +1383,6 @@ final class DiscreteRegistry { } } - private static int[] parseOpsList(String opsList) { - String[] strArr; - if (opsList.isEmpty()) { - strArr = new String[0]; - } else { - strArr = opsList.split(","); - } - int nOps = strArr.length; - int[] result = new int[nOps]; - try { - for (int i = 0; i < nOps; i++) { - result[i] = Integer.parseInt(strArr[i]); - } - } catch (NumberFormatException e) { - Slog.e(TAG, "Failed to parse Discrete ops list: " + e.getMessage()); - return parseOpsList(DEFAULT_DISCRETE_OPS); - } - return result; - } - private static List<DiscreteOpEvent> stableListMerge(List<DiscreteOpEvent> a, List<DiscreteOpEvent> b) { int nA = a.size(); @@ -1570,34 +1427,4 @@ final class DiscreteRegistry { } return result; } - - private static boolean isDiscreteOp(int op, @AppOpsManager.OpFlags int flags) { - if (!ArrayUtils.contains(sDiscreteOps, op)) { - return false; - } - if ((flags & (sDiscreteFlags)) == 0) { - return false; - } - return true; - } - - private static boolean shouldLogAccess(int op) { - return Flags.appopAccessTrackingLoggingEnabled() - && ArrayUtils.contains(sDiscreteOpsToLog, op); - } - - private static long discretizeTimeStamp(long timeStamp) { - return timeStamp / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization; - - } - - private static long discretizeDuration(long duration) { - return duration == -1 ? -1 : (duration + sDiscreteHistoryQuantization - 1) - / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization; - } - - void setDebugMode(boolean debugMode) { - this.mDebugMode = debugMode; - } } - diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java index 5e67f26ba1f6..ba391d0a9995 100644 --- a/services/core/java/com/android/server/appop/HistoricalRegistry.java +++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java @@ -35,6 +35,7 @@ import android.app.AppOpsManager.OpFlags; import android.app.AppOpsManager.OpHistoryFlags; import android.app.AppOpsManager.UidState; import android.content.ContentResolver; +import android.content.Context; import android.database.ContentObserver; import android.net.Uri; import android.os.Build; @@ -45,6 +46,7 @@ import android.os.Message; import android.os.Process; import android.os.RemoteCallback; import android.os.UserHandle; +import android.permission.flags.Flags; import android.provider.Settings; import android.util.ArraySet; import android.util.LongSparseArray; @@ -135,7 +137,7 @@ final class HistoricalRegistry { private static final String PARAMETER_DELIMITER = ","; private static final String PARAMETER_ASSIGNMENT = "="; - private volatile @NonNull DiscreteRegistry mDiscreteRegistry; + private volatile @NonNull DiscreteOpsRegistry mDiscreteRegistry; @GuardedBy("mLock") private @NonNull LinkedList<HistoricalOps> mPendingWrites = new LinkedList<>(); @@ -196,13 +198,30 @@ final class HistoricalRegistry { @GuardedBy("mOnDiskLock") private Persistence mPersistence; - HistoricalRegistry(@NonNull Object lock) { + private final Context mContext; + + HistoricalRegistry(@NonNull Object lock, Context context) { mInMemoryLock = lock; - mDiscreteRegistry = new DiscreteRegistry(lock); + mContext = context; + if (Flags.enableSqliteAppopsAccesses()) { + mDiscreteRegistry = new DiscreteOpsSqlRegistry(context); + if (DiscreteOpsXmlRegistry.getDiscreteOpsDir().exists()) { + DiscreteOpsSqlRegistry sqlRegistry = (DiscreteOpsSqlRegistry) mDiscreteRegistry; + DiscreteOpsXmlRegistry xmlRegistry = new DiscreteOpsXmlRegistry(context); + DiscreteOpsMigrationHelper.migrateDiscreteOpsToSqlite(xmlRegistry, sqlRegistry); + } + } else { + mDiscreteRegistry = new DiscreteOpsXmlRegistry(context); + if (DiscreteOpsDbHelper.getDatabaseFile().exists()) { // roll-back sqlite + DiscreteOpsSqlRegistry sqlRegistry = new DiscreteOpsSqlRegistry(context); + DiscreteOpsXmlRegistry xmlRegistry = (DiscreteOpsXmlRegistry) mDiscreteRegistry; + DiscreteOpsMigrationHelper.migrateDiscreteOpsToXml(sqlRegistry, xmlRegistry); + } + } } HistoricalRegistry(@NonNull HistoricalRegistry other) { - this(other.mInMemoryLock); + this(other.mInMemoryLock, other.mContext); mMode = other.mMode; mBaseSnapshotInterval = other.mBaseSnapshotInterval; mIntervalCompressionMultiplier = other.mIntervalCompressionMultiplier; @@ -475,7 +494,7 @@ final class HistoricalRegistry { @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState, @OpFlags int flags, long accessTime, @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, - @DiscreteRegistry.AccessType int accessType, int accessCount) { + @DiscreteOpsRegistry.AccessType int accessType, int accessCount) { synchronized (mInMemoryLock) { if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) { if (!isPersistenceInitializedMLocked()) { @@ -512,7 +531,7 @@ final class HistoricalRegistry { @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState, @OpFlags int flags, long eventStartTime, long increment, @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, - @DiscreteRegistry.AccessType int accessType) { + @DiscreteOpsRegistry.AccessType int accessType) { synchronized (mInMemoryLock) { if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) { if (!isPersistenceInitializedMLocked()) { @@ -648,7 +667,7 @@ final class HistoricalRegistry { } void writeAndClearDiscreteHistory() { - mDiscreteRegistry.writeAndClearAccessHistory(); + mDiscreteRegistry.writeAndClearOldAccessHistory(); } void clearAllHistory() { @@ -743,7 +762,7 @@ final class HistoricalRegistry { } persistPendingHistory(pendingWrites); } - mDiscreteRegistry.writeAndClearAccessHistory(); + mDiscreteRegistry.writeAndClearOldAccessHistory(); } private void persistPendingHistory(@NonNull List<HistoricalOps> pendingWrites) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index bd2714211796..b9b06701a11b 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -47,6 +47,7 @@ import static android.media.AudioManager.RINGER_MODE_NORMAL; import static android.media.AudioManager.RINGER_MODE_SILENT; import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static android.media.AudioManager.STREAM_SYSTEM; +import static android.media.IAudioManagerNative.HardeningType; import static android.media.audio.Flags.autoPublicVolumeApiHardening; import static android.media.audio.Flags.automaticBtDeviceType; import static android.media.audio.Flags.concurrentAudioRecordBypassPermission; @@ -151,6 +152,7 @@ import android.media.BluetoothProfileConnectionInfo; import android.media.FadeManagerConfiguration; import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; +import android.media.IAudioManagerNative; import android.media.IAudioModeDispatcher; import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; @@ -835,6 +837,18 @@ public class AudioService extends IAudioService.Stub private final UserRestrictionsListener mUserRestrictionsListener = new AudioServiceUserRestrictionsListener(); + private final IAudioManagerNative mNativeShim = new IAudioManagerNative.Stub() { + // oneway + @Override + public void playbackHardeningEvent(int uid, byte type, boolean bypassed) { + } + + @Override + public void permissionUpdateBarrier() { + AudioService.this.permissionUpdateBarrier(); + } + }; + // List of binder death handlers for setMode() client processes. // The last process to have called setMode() is at the top of the list. // package-private so it can be accessed in AudioDeviceBroker.getSetModeDeathHandlers @@ -2811,6 +2825,11 @@ public class AudioService extends IAudioService.Stub args, callback, resultReceiver); } + @Override + public IAudioManagerNative getNativeInterface() { + return mNativeShim; + } + /** @see AudioManager#getSurroundFormats() */ @Override public Map<Integer, Boolean> getSurroundFormats() { @@ -7214,7 +7233,7 @@ public class AudioService extends IAudioService.Stub final int pid = Binder.getCallingPid(); final String eventSource = new StringBuilder("setBluetoothA2dpOn(").append(on) .append(") from u/pid:").append(uid).append("/") - .append(pid).toString(); + .append(pid).append(" src:AudioService.setBtA2dpOn").toString(); new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + MediaMetrics.SEPARATOR + "setBluetoothA2dpOn") @@ -8571,6 +8590,12 @@ public class AudioService extends IAudioService.Stub return true; } + private boolean shouldPreserveVolume(boolean userSwitch, VolumeGroupState vgs) { + // as for STREAM_MUSIC, preserve volume from one user to the next except + // Android Automotive platform + return (userSwitch && vgs.isMusic()) && !isPlatformAutomotive(); + } + private void readVolumeGroupsSettings(boolean userSwitch) { synchronized (mSettingsLock) { synchronized (VolumeStreamState.class) { @@ -8579,8 +8604,7 @@ public class AudioService extends IAudioService.Stub } for (int i = 0; i < sVolumeGroupStates.size(); i++) { VolumeGroupState vgs = sVolumeGroupStates.valueAt(i); - // as for STREAM_MUSIC, preserve volume from one user to the next. - if (!(userSwitch && vgs.isMusic())) { + if (!shouldPreserveVolume(userSwitch, vgs)) { vgs.clearIndexCache(); vgs.readSettings(); } @@ -9019,6 +9043,11 @@ public class AudioService extends IAudioService.Stub mIndexMap.clear(); } + private @UserIdInt int getVolumePersistenceUserId() { + return isMusic() && !isPlatformAutomotive() + ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT; + } + private void persistVolumeGroup(int device) { // No need to persist the index if the volume group is backed up // by a public stream type as this is redundant @@ -9036,7 +9065,7 @@ public class AudioService extends IAudioService.Stub boolean success = mSettings.putSystemIntForUser(mContentResolver, getSettingNameForDevice(device), getIndex(device), - isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT); + getVolumePersistenceUserId()); if (!success) { Log.e(TAG, "persistVolumeGroup failed for group " + mAudioVolumeGroup.name()); } @@ -9059,7 +9088,7 @@ public class AudioService extends IAudioService.Stub String name = getSettingNameForDevice(device); index = mSettings.getSystemIntForUser( mContentResolver, name, defaultIndex, - isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT); + getVolumePersistenceUserId()); if (index == -1) { continue; } diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java index 5d9db65fe2b2..d89db8d5581b 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java @@ -312,6 +312,13 @@ public final class DeviceStateManagerService extends SystemService { mProcessObserver); } + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_SYSTEM_SERVICES_READY) { + mDeviceStatePolicy.getDeviceStateProvider().onSystemReady(); + } + } + @VisibleForTesting Handler getHandler() { return mHandler; diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java index 8d07609cef30..8a8ebc2ffc21 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java @@ -91,6 +91,11 @@ public interface DeviceStateProvider extends Dumpable { @interface SupportedStatesUpdatedReason {} /** + * Called when the system boot phase advances to PHASE_SYSTEM_SERVICES_READY. + */ + default void onSystemReady() {}; + + /** * Registers a listener for changes in provider state. * <p> * It is <b>required</b> that diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index c8192e534f5c..b530da2a5f5e 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2246,10 +2246,6 @@ public final class DisplayManagerService extends SystemService { @GuardedBy("mSyncRoot") private void handleLogicalDisplayDisconnectedLocked(LogicalDisplay display) { - if (!mFlags.isConnectedDisplayManagementEnabled()) { - Slog.e(TAG, "DisplayDisconnected shouldn't be received when the flag is off"); - return; - } releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_DISCONNECTED); mExternalDisplayPolicy.handleLogicalDisplayDisconnectedLocked(display); } @@ -2315,11 +2311,6 @@ public final class DisplayManagerService extends SystemService { @SuppressLint("AndroidFrameworkRequiresPermission") private void handleLogicalDisplayConnectedLocked(LogicalDisplay display) { - if (!mFlags.isConnectedDisplayManagementEnabled()) { - Slog.e(TAG, "DisplayConnected shouldn't be received when the flag is off"); - return; - } - setupLogicalDisplay(display); if (ExternalDisplayPolicy.isExternalDisplayLocked(display)) { @@ -2346,9 +2337,6 @@ public final class DisplayManagerService extends SystemService { private void handleLogicalDisplayAddedLocked(LogicalDisplay display) { final int displayId = display.getDisplayIdLocked(); final boolean isDefault = displayId == Display.DEFAULT_DISPLAY; - if (!mFlags.isConnectedDisplayManagementEnabled()) { - setupLogicalDisplay(display); - } // Wake up waitForDefaultDisplay. if (isDefault) { @@ -2443,21 +2431,17 @@ public final class DisplayManagerService extends SystemService { } private void handleLogicalDisplayRemovedLocked(@NonNull LogicalDisplay display) { - // With display management, the display is removed when disabled, and it might still exist. + // The display is removed when disabled, and it might still exist. // Resources must only be released when the disconnected signal is received. - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (display.isValidLocked()) { - updateViewportPowerStateLocked(display); - } + if (display.isValidLocked()) { + updateViewportPowerStateLocked(display); + } - // Note: This method is only called if the display was enabled before being removed. - sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); + // Note: This method is only called if the display was enabled before being removed. + sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); - if (display.isValidLocked()) { - applyDisplayChangedLocked(display); - } - } else { - releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); + if (display.isValidLocked()) { + applyDisplayChangedLocked(display); } if (mDisplayTopologyCoordinator != null) { mDisplayTopologyCoordinator.onDisplayRemoved(display.getDisplayIdLocked()); @@ -4565,13 +4549,11 @@ public final class DisplayManagerService extends SystemService { final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); - if (mFlags.isConnectedDisplayManagementEnabled()) { - if ((internalEventFlagsMask - & DisplayManagerGlobal - .INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) { - mContext.enforceCallingOrSelfPermission(MANAGE_DISPLAYS, - "Permission required to get signals about connection events."); - } + if ((internalEventFlagsMask + & DisplayManagerGlobal + .INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) { + mContext.enforceCallingOrSelfPermission(MANAGE_DISPLAYS, + "Permission required to get signals about connection events."); } final long token = Binder.clearCallingIdentity(); diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java index e46397bc8ab7..f6b2591ea440 100644 --- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java +++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java @@ -179,12 +179,10 @@ class DisplayManagerShellCommand extends ShellCommand { pw.println(" Sets brightness to docked + idle screen brightness mode"); pw.println(" undock"); pw.println(" Sets brightness to active (normal) screen brightness mode"); - if (mFlags.isConnectedDisplayManagementEnabled()) { - pw.println(" enable-display DISPLAY_ID"); - pw.println(" Enable the DISPLAY_ID. Only possible if this is a connected display."); - pw.println(" disable-display DISPLAY_ID"); - pw.println(" Disable the DISPLAY_ID. Only possible if this is a connected display."); - } + pw.println(" enable-display DISPLAY_ID"); + pw.println(" Enable the DISPLAY_ID. Only possible if this is a connected display."); + pw.println(" disable-display DISPLAY_ID"); + pw.println(" Disable the DISPLAY_ID. Only possible if this is a connected display."); pw.println(" power-reset DISPLAY_ID"); pw.println(" Turn the DISPLAY_ID power to a state the display supposed to have."); pw.println(" power-off DISPLAY_ID"); @@ -601,11 +599,6 @@ class DisplayManagerShellCommand extends ShellCommand { } private int setDisplayEnabled(boolean enable) { - if (!mFlags.isConnectedDisplayManagementEnabled()) { - getErrPrintWriter() - .println("Error: external display management is not available on this device."); - return 1; - } final String displayIdText = getNextArg(); if (displayIdText == null) { getErrPrintWriter().println("Error: no displayId specified"); diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java index 519763a1c3db..a47853c8e555 100644 --- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java +++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java @@ -142,14 +142,6 @@ class ExternalDisplayPolicy { mDisplayIdsWaitingForBootCompletion.clear(); } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - if (DEBUG) { - Slog.d(TAG, "External display management is not enabled on your device:" - + " cannot register thermal listener."); - } - return; - } - if (!mFlags.isConnectedDisplayErrorHandlingEnabled()) { if (DEBUG) { Slog.d(TAG, "ConnectedDisplayErrorHandlingEnabled is not enabled on your device:" @@ -173,14 +165,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - if (DEBUG) { - Slog.d(TAG, "setExternalDisplayEnabledLocked: External display management is not" - + " enabled on your device, cannot enable/disable display."); - } - return; - } - if (enabled && !isExternalDisplayAllowed()) { Slog.w(TAG, "setExternalDisplayEnabledLocked: External display can not be enabled" + " because it is currently not allowed."); @@ -202,14 +186,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - if (DEBUG) { - Slog.d(TAG, "handleExternalDisplayConnectedLocked connected display management" - + " flag is off"); - } - return; - } - if (!mIsBootCompleted) { mDisplayIdsWaitingForBootCompletion.add(logicalDisplay.getDisplayIdLocked()); return; @@ -251,10 +227,6 @@ class ExternalDisplayPolicy { void handleLogicalDisplayDisconnectedLocked(@NonNull final LogicalDisplay logicalDisplay) { // Type of the display here is always UNKNOWN, so we can't verify it is an external display - if (!mFlags.isConnectedDisplayManagementEnabled()) { - return; - } - var displayId = logicalDisplay.getDisplayIdLocked(); if (mDisplayIdsWaitingForBootCompletion.remove(displayId)) { return; @@ -271,10 +243,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - return; - } - mExternalDisplayStatsService.onDisplayAdded(logicalDisplay.getDisplayIdLocked()); } @@ -289,10 +257,6 @@ class ExternalDisplayPolicy { } } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - return; - } - if (isShown) { mExternalDisplayStatsService.onPresentationWindowAdded(displayId); } else { @@ -306,12 +270,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - Slog.e(TAG, "disableExternalDisplayLocked shouldn't be called when the" - + " connected display management flag is off"); - return; - } - if (!mFlags.isConnectedDisplayErrorHandlingEnabled()) { if (DEBUG) { Slog.d(TAG, "disableExternalDisplayLocked shouldn't be called when the" diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 006921572977..ecc8896b69c6 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -823,18 +823,13 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { if (wasPreviouslyUpdated) { // The display isn't actually removed from our internal data structures until // after the notification is sent; see {@link #sendUpdatesForDisplaysLocked}. - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (mDisplaysEnabledCache.get(displayId)) { - // We still need to send LOGICAL_DISPLAY_EVENT_DISCONNECTED - reloop = true; - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED; - } else { - mUpdatedLogicalDisplays.delete(displayId); - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_DISCONNECTED; - } + if (mDisplaysEnabledCache.get(displayId)) { + // We still need to send LOGICAL_DISPLAY_EVENT_DISCONNECTED + reloop = true; + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED; } else { mUpdatedLogicalDisplays.delete(displayId); - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED; + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_DISCONNECTED; } } else { // This display never left this class, safe to remove without notification @@ -845,20 +840,15 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // The display is new. } else if (!wasPreviouslyUpdated) { - if (mFlags.isConnectedDisplayManagementEnabled()) { - // We still need to send LOGICAL_DISPLAY_EVENT_ADDED - reloop = true; - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CONNECTED; - } else { - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_ADDED; - } + // We still need to send LOGICAL_DISPLAY_EVENT_ADDED + reloop = true; + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CONNECTED; // Underlying displays device has changed to a different one. } else if (!TextUtils.equals(mTempDisplayInfo.uniqueId, newDisplayInfo.uniqueId)) { logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_SWAPPED; // Something about the display device has changed. - } else if (mFlags.isConnectedDisplayManagementEnabled() - && wasPreviouslyEnabled != isCurrentlyEnabled) { + } else if (wasPreviouslyEnabled != isCurrentlyEnabled) { int event = isCurrentlyEnabled ? LOGICAL_DISPLAY_EVENT_ADDED : LOGICAL_DISPLAY_EVENT_REMOVED; logicalDisplayEventMask |= event; @@ -936,17 +926,13 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION); sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_ADDED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REMOVED); - if (mFlags.isConnectedDisplayManagementEnabled()) { - sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DISCONNECTED); - } + sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DISCONNECTED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_BASIC_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_STATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_SWAPPED); - if (mFlags.isConnectedDisplayManagementEnabled()) { - sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED); - } + sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_ADDED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED); sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_CHANGED); @@ -996,23 +982,15 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { + "display=" + id + " with device=" + uniqueId); } - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_ADDED) { - mDisplaysEnabledCache.put(id, true); - } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) { - mDisplaysEnabledCache.delete(id); - } + if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_ADDED) { + mDisplaysEnabledCache.put(id, true); + } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) { + mDisplaysEnabledCache.delete(id); } mListener.onLogicalDisplayEventLocked(display, logicalDisplayEvent); - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_DISCONNECTED) { - mLogicalDisplays.delete(id); - } - } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) { - // We wait until we sent the EVENT_REMOVED event before actually removing the - // display. + if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_DISCONNECTED) { mLogicalDisplays.delete(id); } } diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index addfbf1833b9..4e57d6791ff6 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -42,10 +42,6 @@ public class DisplayManagerFlags { Flags.FLAG_ENABLE_PORT_IN_DISPLAY_LAYOUT, Flags::enablePortInDisplayLayout); - private final FlagState mConnectedDisplayManagementFlagState = new FlagState( - Flags.FLAG_ENABLE_CONNECTED_DISPLAY_MANAGEMENT, - Flags::enableConnectedDisplayManagement); - private final FlagState mAdaptiveToneImprovements1 = new FlagState( Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1, Flags::enableAdaptiveToneImprovements1); @@ -269,11 +265,6 @@ public class DisplayManagerFlags { return mPortInDisplayLayoutFlagState.isEnabled(); } - /** Returns whether connected display management is enabled or not. */ - public boolean isConnectedDisplayManagementEnabled() { - return mConnectedDisplayManagementFlagState.isEnabled(); - } - /** Returns whether power throttling clamper is enabled on not. */ public boolean isPowerThrottlingClamperEnabled() { return mPowerThrottlingClamperFlagState.isEnabled(); @@ -572,7 +563,6 @@ public class DisplayManagerFlags { pw.println(" " + mAdaptiveToneImprovements2); pw.println(" " + mBackUpSmoothDisplayAndForcePeakRefreshRateFlagState); pw.println(" " + mConnectedDisplayErrorHandlingFlagState); - pw.println(" " + mConnectedDisplayManagementFlagState); pw.println(" " + mDisplayOffloadFlagState); pw.println(" " + mExternalDisplayLimitModeState); pw.println(" " + mDisplayTopology); diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index eccbbb14c4ea..afae07c88f8d 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -29,14 +29,6 @@ flag { } flag { - name: "enable_connected_display_management" - namespace: "display_manager" - description: "Feature flag for Connected Display management" - bug: "280739508" - is_fixed_read_only: true -} - -flag { name: "enable_power_throttling_clamper" namespace: "display_manager" description: "Feature flag for Power Throttling Clamper" diff --git a/services/core/java/com/android/server/flags/services.aconfig b/services/core/java/com/android/server/flags/services.aconfig index eea5c982c537..4505d0e2d799 100644 --- a/services/core/java/com/android/server/flags/services.aconfig +++ b/services/core/java/com/android/server/flags/services.aconfig @@ -78,3 +78,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "datetime_notifications" + # "location" is used by the Android System Time team for feature flags. + namespace: "location" + description: "Enable the time notifications feature, a toggle to enable/disable time-related notifications in Date & Time Settings" + bug: "283267917" +} diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 9f785ac81398..93fdbc787ed0 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -19,6 +19,7 @@ package com.android.server.input; import static android.hardware.input.InputGestureData.createKeyTrigger; import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures; +import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.server.flags.Flags.newBugreportKeyboardShortcut; import static com.android.window.flags.Flags.enableMoveToNextDisplayShortcut; @@ -240,6 +241,13 @@ final class InputGestureManager { KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK)); } + if (enableVoiceAccessKeyGestures()) { + systemShortcuts.add( + createKeyGesture( + KeyEvent.KEYCODE_V, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); + } if (enableTaskResizingKeyboardShortcuts()) { systemShortcuts.add(createKeyGesture( KeyEvent.KEYCODE_LEFT_BRACKET, @@ -308,6 +316,31 @@ final class InputGestureManager { } } + @Nullable + public InputGestureData getInputGesture(int userId, InputGestureData.Trigger trigger) { + synchronized (mGestureLock) { + if (mBlockListedTriggers.contains(trigger)) { + return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build(); + } + if (trigger instanceof InputGestureData.KeyTrigger keyTrigger) { + if (KeyEvent.isModifierKey(keyTrigger.getKeycode()) || + KeyEvent.isSystemKey(keyTrigger.getKeycode())) { + return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build(); + } + } + InputGestureData gestureData = mSystemShortcuts.get(trigger); + if (gestureData != null) { + return gestureData; + } + if (!mCustomInputGestures.contains(userId)) { + return null; + } + return mCustomInputGestures.get(userId).get(trigger); + } + } + @InputManager.CustomInputGestureResult public int addCustomInputGesture(int userId, InputGestureData newGesture) { synchronized (mGestureLock) { diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index bc44fed21f2d..4e5c720f9f1c 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -104,13 +104,16 @@ public abstract class InputManagerInternal { public abstract PointF getCursorPosition(int displayId); /** - * Enables or disables pointer acceleration for mouse movements. + * Set whether all pointer scaling, including linear scaling based on the + * user's pointer speed setting, should be enabled or disabled for mice. * * Note that this only affects pointer movements from mice (that is, pointing devices which send * relative motions, including trackballs and pointing sticks), not from other pointer devices * such as touchpads and styluses. + * + * Scaling is enabled by default on new displays until it is explicitly disabled. */ - public abstract void setMousePointerAccelerationEnabled(boolean enabled, int displayId); + public abstract void setMouseScalingEnabled(boolean enabled, int displayId); /** * Sets the eligibility of windows on a given display for pointer capture. If a display is diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 559b4ae64e50..2ba35d6a70d2 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -1382,9 +1382,9 @@ public class InputManagerService extends IInputManager.Stub mNative.setPointerSpeed(speed); } - private void setMousePointerAccelerationEnabled(boolean enabled, int displayId) { + private void setMouseScalingEnabled(boolean enabled, int displayId) { updateAdditionalDisplayInputProperties(displayId, - properties -> properties.mousePointerAccelerationEnabled = enabled); + properties -> properties.mouseScalingEnabled = enabled); } private void setPointerIconVisible(boolean visible, int displayId) { @@ -2232,8 +2232,8 @@ public class InputManagerService extends IInputManager.Stub pw.println("displayId: " + mAdditionalDisplayInputProperties.keyAt(i)); final AdditionalDisplayInputProperties properties = mAdditionalDisplayInputProperties.valueAt(i); - pw.println("mousePointerAccelerationEnabled: " - + properties.mousePointerAccelerationEnabled); + pw.println("mouseScalingEnabled: " + + properties.mouseScalingEnabled); pw.println("pointerIconVisible: " + properties.pointerIconVisible); } } finally { @@ -3061,6 +3061,16 @@ public class InputManagerService extends IInputManager.Stub @Override @PermissionManuallyEnforced + public AidlInputGestureData getInputGesture(@UserIdInt int userId, + @NonNull AidlInputGestureData.Trigger trigger) { + enforceManageKeyGesturePermission(); + + Objects.requireNonNull(trigger); + return mKeyGestureController.getInputGesture(userId, trigger); + } + + @Override + @PermissionManuallyEnforced public int addCustomInputGesture(@UserIdInt int userId, @NonNull AidlInputGestureData inputGestureData) { enforceManageKeyGesturePermission(); @@ -3575,8 +3585,8 @@ public class InputManagerService extends IInputManager.Stub } @Override - public void setMousePointerAccelerationEnabled(boolean enabled, int displayId) { - InputManagerService.this.setMousePointerAccelerationEnabled(enabled, displayId); + public void setMouseScalingEnabled(boolean enabled, int displayId) { + InputManagerService.this.setMouseScalingEnabled(enabled, displayId); } @Override @@ -3716,15 +3726,15 @@ public class InputManagerService extends IInputManager.Stub private static class AdditionalDisplayInputProperties { static final boolean DEFAULT_POINTER_ICON_VISIBLE = true; - static final boolean DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED = true; + static final boolean DEFAULT_MOUSE_SCALING_ENABLED = true; /** - * Whether to enable mouse pointer acceleration on this display. Note that this only affects + * Whether to enable mouse pointer scaling on this display. Note that this only affects * pointer movements from mice (that is, pointing devices which send relative motions, * including trackballs and pointing sticks), not from other pointer devices such as * touchpads and styluses. */ - public boolean mousePointerAccelerationEnabled; + public boolean mouseScalingEnabled; // Whether the pointer icon should be visible or hidden on this display. public boolean pointerIconVisible; @@ -3734,12 +3744,12 @@ public class InputManagerService extends IInputManager.Stub } public boolean allDefaults() { - return mousePointerAccelerationEnabled == DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED + return mouseScalingEnabled == DEFAULT_MOUSE_SCALING_ENABLED && pointerIconVisible == DEFAULT_POINTER_ICON_VISIBLE; } public void reset() { - mousePointerAccelerationEnabled = DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED; + mouseScalingEnabled = DEFAULT_MOUSE_SCALING_ENABLED; pointerIconVisible = DEFAULT_POINTER_ICON_VISIBLE; } } @@ -3754,14 +3764,14 @@ public class InputManagerService extends IInputManager.Stub mAdditionalDisplayInputProperties.put(displayId, properties); } final boolean oldPointerIconVisible = properties.pointerIconVisible; - final boolean oldMouseAccelerationEnabled = properties.mousePointerAccelerationEnabled; + final boolean oldMouseScalingEnabled = properties.mouseScalingEnabled; updater.accept(properties); if (oldPointerIconVisible != properties.pointerIconVisible) { mNative.setPointerIconVisibility(displayId, properties.pointerIconVisible); } - if (oldMouseAccelerationEnabled != properties.mousePointerAccelerationEnabled) { - mNative.setMousePointerAccelerationEnabled(displayId, - properties.mousePointerAccelerationEnabled); + if (oldMouseScalingEnabled != properties.mouseScalingEnabled) { + mNative.setMouseScalingEnabled(displayId, + properties.mouseScalingEnabled); } if (properties.allDefaults()) { mAdditionalDisplayInputProperties.remove(displayId); diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java index febf24edc294..e25ea4b43827 100644 --- a/services/core/java/com/android/server/input/InputSettingsObserver.java +++ b/services/core/java/com/android/server/input/InputSettingsObserver.java @@ -74,6 +74,8 @@ class InputSettingsObserver extends ContentObserver { Map.entry(Settings.System.getUriFor( Settings.System.MOUSE_POINTER_ACCELERATION_ENABLED), (reason) -> updateMouseAccelerationEnabled()), + Map.entry(Settings.System.getUriFor(Settings.System.MOUSE_SCROLLING_SPEED), + (reason) -> updateMouseScrollingSpeed()), Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_POINTER_SPEED), (reason) -> updateTouchpadPointerSpeed()), Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_NATURAL_SCROLLING), @@ -199,6 +201,11 @@ class InputSettingsObserver extends ContentObserver { InputSettings.isMousePointerAccelerationEnabled(mContext)); } + private void updateMouseScrollingSpeed() { + mNative.setMouseScrollingSpeed( + constrainPointerSpeedValue(InputSettings.getMouseScrollingSpeed(mContext))); + } + private void updateTouchpadPointerSpeed() { mNative.setTouchpadPointerSpeed( constrainPointerSpeedValue(InputSettings.getTouchpadPointerSpeed(mContext))); diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 5f7ad2797368..fb5ce5b4e5fa 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -18,6 +18,8 @@ package com.android.server.input; import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_WATCH; +import static android.os.UserManager.isVisibleBackgroundUsersEnabled; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE; import static com.android.hardware.input.Flags.enableNew25q2Keycodes; @@ -55,7 +57,6 @@ import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.SparseArray; -import android.view.Display; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -64,6 +65,8 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.IShortcutService; +import com.android.server.LocalServices; +import com.android.server.pm.UserManagerInternal; import com.android.server.policy.KeyCombinationManager; import java.util.ArrayDeque; @@ -159,6 +162,10 @@ final class KeyGestureController { /** Currently fully consumed key codes per device */ private final SparseArray<Set<Integer>> mConsumedKeysForDevice = new SparseArray<>(); + private final UserManagerInternal mUserManagerInternal; + + private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled(); + KeyGestureController(Context context, Looper looper, InputDataStore inputDataStore) { mContext = context; mHandler = new Handler(looper, this::handleMessage); @@ -180,6 +187,7 @@ final class KeyGestureController { mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext); mInputGestureManager = new InputGestureManager(mContext); mInputDataStore = inputDataStore; + mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); initBehaviors(); initKeyCombinationRules(); } @@ -449,6 +457,9 @@ final class KeyGestureController { } public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { + if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) { + return false; + } final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0; if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures() && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { @@ -457,6 +468,24 @@ final class KeyGestureController { return false; } + private boolean shouldIgnoreKeyEventForVisibleBackgroundUser(KeyEvent event) { + final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay( + event.getDisplayId()); + final int currentUserId; + synchronized (mUserLock) { + currentUserId = mCurrentUserId; + } + if (currentUserId != displayAssignedUserId + && !KeyEvent.isVisibleBackgroundUserAllowedKey(event.getKeyCode())) { + if (DEBUG) { + Slog.w(TAG, "Ignored key event [" + event + "] for visible background user [" + + displayAssignedUserId + "]"); + } + return true; + } + return false; + } + public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event, int policyFlags) { // TODO(b/358569822): Handle shortcuts trigger logic here and pass it to appropriate @@ -895,7 +924,7 @@ final class KeyGestureController { private void handleMultiKeyGesture(int[] keycodes, @KeyGestureEvent.KeyGestureType int gestureType, int action, int flags) { handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0, - gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags, + gestureType, action, DEFAULT_DISPLAY, /* focusedToken = */null, flags, /* appLaunchData = */null); } @@ -903,7 +932,7 @@ final class KeyGestureController { @Nullable AppLaunchData appLaunchData) { handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, new int[0], /* modifierState= */0, keyGestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, - Display.DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData); + DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData); } @VisibleForTesting @@ -915,6 +944,11 @@ final class KeyGestureController { } private boolean handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) { + if (mVisibleBackgroundUsersEnabled && event.displayId != DEFAULT_DISPLAY + && shouldIgnoreGestureEventForVisibleBackgroundUser(event.gestureType, + event.displayId)) { + return false; + } synchronized (mKeyGestureHandlerRecords) { for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) { if (handler.handleKeyGesture(event, focusedToken)) { @@ -927,6 +961,24 @@ final class KeyGestureController { return false; } + private boolean shouldIgnoreGestureEventForVisibleBackgroundUser( + @KeyGestureEvent.KeyGestureType int gestureType, int displayId) { + final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay(displayId); + final int currentUserId; + synchronized (mUserLock) { + currentUserId = mCurrentUserId; + } + if (currentUserId != displayAssignedUserId + && !KeyGestureEvent.isVisibleBackgrounduserAllowedGesture(gestureType)) { + if (DEBUG) { + Slog.w(TAG, "Ignored gesture event [" + gestureType + + "] for visible background user [" + displayAssignedUserId + "]"); + } + return true; + } + return false; + } + private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) { synchronized (mKeyGestureHandlerRecords) { for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) { @@ -943,7 +995,7 @@ final class KeyGestureController { // TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally // should not rely on PWM to tell us about the gesture start and end. AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState, - gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, + gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY, /* flags = */0, /* appLaunchData = */null); mHandler.obtainMessage(MSG_NOTIFY_KEY_GESTURE_EVENT, event).sendToTarget(); } @@ -951,7 +1003,7 @@ final class KeyGestureController { public void handleKeyGesture(int deviceId, int[] keycodes, int modifierState, @KeyGestureEvent.KeyGestureType int gestureType) { AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState, - gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, + gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY, /* flags = */0, /* appLaunchData = */null); handleKeyGesture(event, null /*focusedToken*/); } @@ -1069,6 +1121,18 @@ final class KeyGestureController { } @BinderThread + @Nullable + public AidlInputGestureData getInputGesture(@UserIdInt int userId, + @NonNull AidlInputGestureData.Trigger trigger) { + InputGestureData gestureData = mInputGestureManager.getInputGesture(userId, + InputGestureData.createTriggerFromAidlTrigger(trigger)); + if (gestureData == null) { + return null; + } + return gestureData.getAidlData(); + } + + @BinderThread @InputManager.CustomInputGestureResult public int addCustomInputGesture(@UserIdInt int userId, @NonNull AidlInputGestureData inputGestureData) { diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 7dbde64a6412..4d38c8401e2d 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -130,12 +130,14 @@ interface NativeInputManagerService { void setPointerSpeed(int speed); - void setMousePointerAccelerationEnabled(int displayId, boolean enabled); + void setMouseScalingEnabled(int displayId, boolean enabled); void setMouseReverseVerticalScrollingEnabled(boolean enabled); void setMouseScrollingAccelerationEnabled(boolean enabled); + void setMouseScrollingSpeed(int speed); + void setMouseSwapPrimaryButtonEnabled(boolean enabled); void setMouseAccelerationEnabled(boolean enabled); @@ -419,7 +421,7 @@ interface NativeInputManagerService { public native void setPointerSpeed(int speed); @Override - public native void setMousePointerAccelerationEnabled(int displayId, boolean enabled); + public native void setMouseScalingEnabled(int displayId, boolean enabled); @Override public native void setMouseReverseVerticalScrollingEnabled(boolean enabled); @@ -428,6 +430,9 @@ interface NativeInputManagerService { public native void setMouseScrollingAccelerationEnabled(boolean enabled); @Override + public native void setMouseScrollingSpeed(int speed); + + @Override public native void setMouseSwapPrimaryButtonEnabled(boolean enabled); @Override diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java index b0dff22c6f03..281db0ae9518 100644 --- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -387,8 +387,9 @@ public final class ImeVisibilityStateComputer { @GuardedBy("ImfLock.class") void setWindowState(IBinder windowToken, @NonNull ImeTargetWindowState newState) { final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); - if (state != null && newState.hasEditorFocused() - && newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS) { + if (state != null && newState.hasEditorFocused() && ( + newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS + || Flags.refactorInsetsController())) { // Inherit the last requested IME visible state when the target window is still // focused with an editor. newState.setRequestedImeVisible(state.mRequestedImeVisible); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index 87d809b5e850..1e54beeb2d64 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -32,7 +32,10 @@ import android.hardware.location.ContextHubTransaction; import android.hardware.location.IContextHubTransactionCallback; import android.os.Binder; import android.os.IBinder; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; import android.os.RemoteException; +import android.os.WorkSource; import android.util.Log; import android.util.SparseArray; @@ -54,6 +57,16 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /** Message used by noteOp when this client receives a message from an endpoint. */ private static final String RECEIVE_MSG_NOTE = "ContextHubEndpointMessageDelivery"; + /** The duration of wakelocks acquired during HAL callbacks */ + private static final long WAKELOCK_TIMEOUT_MILLIS = 5 * 1000; + + /* + * Internal interface used to invoke client callbacks. + */ + interface CallbackConsumer { + void accept(IContextHubEndpointCallback callback) throws RemoteException; + } + /** The context of the service. */ private final Context mContext; @@ -134,6 +147,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub private final int mUid; + /** Wakelock held while nanoapp message are in flight to the client */ + private final WakeLock mWakeLock; + /* package */ ContextHubEndpointBroker( Context context, IEndpointCommunication hubInterface, @@ -158,6 +174,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mAppOpsManager = context.getSystemService(AppOpsManager.class); mAppOpsManager.startWatchingMode(AppOpsManager.OP_NONE, mPackageName, this); + + PowerManager powerManager = context.getSystemService(PowerManager.class); + mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.setWorkSource(new WorkSource(mUid, mPackageName)); + mWakeLock.setReferenceCounted(true); } @Override @@ -227,6 +248,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } mEndpointManager.unregisterEndpoint(mEndpointInfo.getIdentifier().getEndpoint()); + releaseWakeLockOnExit(); } @Override @@ -302,6 +324,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) + public void onCallbackFinished() { + super.onCallbackFinished_enforcePermission(); + releaseWakeLock(); + } + /** Invoked when the underlying binder of this broker has died at the client process. */ @Override public void binderDied() { @@ -357,15 +386,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true)); } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionOpenRequest( - sessionId, initiator, serviceDescriptor); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionOpenRequest", e); - cleanupSessionResources(sessionId); - return; - } + boolean success = + invokeCallback( + (consumer) -> + consumer.onSessionOpenRequest( + sessionId, initiator, serviceDescriptor)); + if (!success) { + cleanupSessionResources(sessionId); } } @@ -374,14 +401,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub Log.w(TAG, "Unknown session ID in onCloseEndpointSession: id=" + sessionId); return; } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionClosed( - sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason)); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionClosed", e); - } - } + + invokeCallback( + (consumer) -> + consumer.onSessionClosed( + sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason))); } /* package */ void onEndpointSessionOpenComplete(int sessionId) { @@ -392,16 +416,30 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } mSessionInfoMap.get(sessionId).setSessionState(SessionInfo.SessionState.ACTIVE); } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionOpenComplete(sessionId); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionClosed", e); - } - } + + invokeCallback((consumer) -> consumer.onSessionOpenComplete(sessionId)); } /* package */ void onMessageReceived(int sessionId, HubMessage message) { + byte code = onMessageReceivedInternal(sessionId, message); + if (code != ErrorCode.OK && message.isResponseRequired()) { + sendMessageDeliveryStatus( + sessionId, message.getMessageSequenceNumber(), code); + } + } + + /* package */ void onMessageDeliveryStatusReceived( + int sessionId, int sequenceNumber, byte errorCode) { + mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK); + } + + /* package */ boolean hasSessionId(int sessionId) { + synchronized (mOpenSessionLock) { + return mSessionInfoMap.contains(sessionId); + } + } + + private byte onMessageReceivedInternal(int sessionId, HubMessage message) { HubEndpointInfo remote; synchronized (mOpenSessionLock) { if (!isSessionActive(sessionId)) { @@ -411,9 +449,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + sessionId + ") with message: " + message); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMANENT_ERROR); - return; + return ErrorCode.PERMANENT_ERROR; } remote = mSessionInfoMap.get(sessionId).getRemoteEndpointInfo(); } @@ -435,31 +471,12 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + ". " + mPackageName + " doesn't have permission"); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMISSION_DENIED); - return; - } - - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onMessageReceived(sessionId, message); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onMessageReceived", e); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.TRANSIENT_ERROR); - } + return ErrorCode.PERMISSION_DENIED; } - } - - /* package */ void onMessageDeliveryStatusReceived( - int sessionId, int sequenceNumber, byte errorCode) { - mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK); - } - /* package */ boolean hasSessionId(int sessionId) { - synchronized (mOpenSessionLock) { - return mSessionInfoMap.contains(sessionId); - } + boolean success = + invokeCallback((consumer) -> consumer.onMessageReceived(sessionId, message)); + return success ? ErrorCode.OK : ErrorCode.TRANSIENT_ERROR; } /** @@ -520,4 +537,63 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub Collection<String> requiredPermissions = targetEndpointInfo.getRequiredPermissions(); return ContextHubServiceUtil.hasPermissions(mContext, mPid, mUid, requiredPermissions); } + + private void acquireWakeLock() { + Binder.withCleanCallingIdentity( + () -> { + if (mIsRegistered.get()) { + mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS); + } + }); + } + + private void releaseWakeLock() { + Binder.withCleanCallingIdentity( + () -> { + if (mWakeLock.isHeld()) { + try { + mWakeLock.release(); + } catch (RuntimeException e) { + Log.e(TAG, "Releasing the wakelock fails - ", e); + } + } + }); + } + + private void releaseWakeLockOnExit() { + Binder.withCleanCallingIdentity( + () -> { + while (mWakeLock.isHeld()) { + try { + mWakeLock.release(); + } catch (RuntimeException e) { + Log.e( + TAG, + "Releasing the wakelock for all acquisitions fails - ", + e); + break; + } + } + }); + } + + /** + * Invokes a callback and acquires a wakelock. + * + * @param consumer The callback invoke + * @return false if the callback threw a RemoteException + */ + private boolean invokeCallback(CallbackConsumer consumer) { + if (mContextHubEndpointCallback != null) { + acquireWakeLock(); + try { + consumer.accept(mContextHubEndpointCallback); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException while calling endpoint callback", e); + releaseWakeLock(); + return false; + } + } + return true; + } } diff --git a/services/core/java/com/android/server/location/fudger/LocationFudger.java b/services/core/java/com/android/server/location/fudger/LocationFudger.java index 27577641ad1d..28e21b71dcc9 100644 --- a/services/core/java/com/android/server/location/fudger/LocationFudger.java +++ b/services/core/java/com/android/server/location/fudger/LocationFudger.java @@ -302,6 +302,15 @@ public class LocationFudger { // requires latitude since longitudinal distances change with distance from equator. private static double metersToDegreesLongitude(double distance, double lat) { - return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat)); + // Needed to convert from longitude distance to longitude degree. + // X meters near the poles is more degrees than at the equator. + double cosLat = Math.cos(Math.toRadians(lat)); + // If we are right on top of the pole, the degree is always 0. + // We return a very small value instead to avoid divide by zero errors + // later on. + if (cosLat == 0.0) { + return 0.0001; + } + return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / cosLat; } } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 286238e7888c..0d0cdd83cc73 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -438,9 +438,9 @@ public class LockSettingsService extends ILockSettings.Stub { } LockscreenCredential credential = LockscreenCredential.createUnifiedProfilePassword(newPassword); - Arrays.fill(newPasswordChars, '\u0000'); - Arrays.fill(newPassword, (byte) 0); - Arrays.fill(randomLockSeed, (byte) 0); + LockPatternUtils.zeroize(newPasswordChars); + LockPatternUtils.zeroize(newPassword); + LockPatternUtils.zeroize(randomLockSeed); return credential; } @@ -1537,7 +1537,7 @@ public class LockSettingsService extends ILockSettings.Stub { + userId); } } finally { - Arrays.fill(password, (byte) 0); + LockPatternUtils.zeroize(password); } } @@ -1570,7 +1570,7 @@ public class LockSettingsService extends ILockSettings.Stub { decryptionResult = cipher.doFinal(encryptedPassword); LockscreenCredential credential = LockscreenCredential.createUnifiedProfilePassword( decryptionResult); - Arrays.fill(decryptionResult, (byte) 0); + LockPatternUtils.zeroize(decryptionResult); try { long parentSid = getGateKeeperService().getSecureUserId( mUserManager.getProfileParent(userId).id); @@ -2263,7 +2263,7 @@ public class LockSettingsService extends ILockSettings.Stub { } catch (RemoteException e) { Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId); } finally { - Arrays.fill(secret, (byte) 0); + LockPatternUtils.zeroize(secret); } } diff --git a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java index 21caf76d30d0..3d64f1890073 100644 --- a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java +++ b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java @@ -26,6 +26,7 @@ import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockscreenCredential; import java.security.GeneralSecurityException; @@ -154,7 +155,7 @@ public class UnifiedProfilePasswordCache { } LockscreenCredential result = LockscreenCredential.createUnifiedProfilePassword(credential); - Arrays.fill(credential, (byte) 0); + LockPatternUtils.zeroize(credential); return result; } } @@ -175,7 +176,7 @@ public class UnifiedProfilePasswordCache { Slog.d(TAG, "Cannot delete key", e); } if (mEncryptedPasswords.contains(userId)) { - Arrays.fill(mEncryptedPasswords.get(userId), (byte) 0); + LockPatternUtils.zeroize(mEncryptedPasswords.get(userId)); mEncryptedPasswords.remove(userId); } } diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java index bf1b3c3f0b35..85dc811a7811 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java @@ -162,7 +162,7 @@ public class KeySyncTask implements Runnable { Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e); } finally { if (mCredential != null) { - Arrays.fill(mCredential, (byte) 0); // no longer needed. + LockPatternUtils.zeroize(mCredential); // no longer needed. } } } @@ -506,7 +506,7 @@ public class KeySyncTask implements Runnable { try { byte[] hash = MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes); - Arrays.fill(bytes, (byte) 0); + LockPatternUtils.zeroize(bytes); return hash; } catch (NoSuchAlgorithmException e) { // Impossible, SHA-256 must be supported on Android. diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java index 54303c01890a..7d8300a8148a 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java @@ -1082,7 +1082,7 @@ public class RecoverableKeyStoreManager { int keyguardCredentialsType = lockPatternUtilsToKeyguardType(savedCredentialType); try (LockscreenCredential credential = createLockscreenCredential(keyguardCredentialsType, decryptedCredentials)) { - Arrays.fill(decryptedCredentials, (byte) 0); + LockPatternUtils.zeroize(decryptedCredentials); decryptedCredentials = null; VerifyCredentialResponse verifyResponse = lockSettingsService.verifyCredential(credential, userId, 0); diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java index 0e66746f4160..f1ef333d223a 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java @@ -19,8 +19,9 @@ package com.android.server.locksettings.recoverablekeystore.storage; import android.annotation.Nullable; import android.util.SparseArray; +import com.android.internal.widget.LockPatternUtils; + import java.util.ArrayList; -import java.util.Arrays; import javax.security.auth.Destroyable; @@ -187,8 +188,8 @@ public class RecoverySessionStorage implements Destroyable { */ @Override public void destroy() { - Arrays.fill(mLskfHash, (byte) 0); - Arrays.fill(mKeyClaimant, (byte) 0); + LockPatternUtils.zeroize(mLskfHash); + LockPatternUtils.zeroize(mKeyClaimant); } } } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 68e195d7f079..35bb19943a24 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -302,7 +302,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub final long token = Binder.clearCallingIdentity(); try { - mAudioService.setBluetoothA2dpOn(on); + if (!Flags.disableSetBluetoothAd2pOnCalls()) { + mAudioService.setBluetoothA2dpOn(on); + } } catch (RemoteException ex) { Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn. on=" + on); } finally { @@ -677,7 +679,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub if (DEBUG) { Slog.d(TAG, "restoreBluetoothA2dp(" + a2dpOn + ")"); } - mAudioService.setBluetoothA2dpOn(a2dpOn); + if (!Flags.disableSetBluetoothAd2pOnCalls()) { + mAudioService.setBluetoothA2dpOn(a2dpOn); + } } } catch (RemoteException e) { Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn."); diff --git a/services/core/java/com/android/server/media/TEST_MAPPING b/services/core/java/com/android/server/media/TEST_MAPPING index 43e2afd8827d..dbf9915c6e0c 100644 --- a/services/core/java/com/android/server/media/TEST_MAPPING +++ b/services/core/java/com/android/server/media/TEST_MAPPING @@ -1,7 +1,10 @@ { "presubmit": [ { - "name": "CtsMediaBetterTogetherTestCases" + "name": "CtsMediaRouterTestCases" + }, + { + "name": "CtsMediaSessionTestCases" }, { "name": "MediaRouterServiceTests" diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 34bb4155c943..d440d3ab3521 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -18,8 +18,10 @@ package com.android.server.media.quality; import android.content.ContentValues; import android.content.Context; +import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.hardware.tv.mediaquality.IMediaQuality; import android.media.quality.AmbientBacklightSettings; import android.media.quality.IAmbientBacklightCallback; import android.media.quality.IMediaQualityManager; @@ -34,20 +36,30 @@ import android.media.quality.SoundProfile; import android.media.quality.SoundProfileHandle; import android.os.Binder; import android.os.Bundle; +import android.os.IBinder; import android.os.PersistableBundle; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.ServiceManager; import android.os.UserHandle; import android.util.Log; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseArray; import com.android.server.SystemService; +import com.android.server.utils.Slogf; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; @@ -64,10 +76,14 @@ public class MediaQualityService extends SystemService { private final MediaQualityDbHelper mMediaQualityDbHelper; private final BiMap<Long, String> mPictureProfileTempIdMap; private final BiMap<Long, String> mSoundProfileTempIdMap; + private final PackageManager mPackageManager; + private final SparseArray<UserState> mUserStates = new SparseArray<>(); + private IMediaQuality mMediaQuality; public MediaQualityService(Context context) { super(context); mContext = context; + mPackageManager = mContext.getPackageManager(); mPictureProfileTempIdMap = new BiMap<>(); mSoundProfileTempIdMap = new BiMap<>(); mMediaQualityDbHelper = new MediaQualityDbHelper(mContext); @@ -77,6 +93,12 @@ public class MediaQualityService extends SystemService { @Override public void onStart() { + IBinder binder = ServiceManager.getService(IMediaQuality.DESCRIPTOR + "/default"); + if (binder != null) { + Slogf.d(TAG, "binder is not null"); + mMediaQuality = IMediaQuality.Stub.asInterface(binder); + } + publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService()); } @@ -85,12 +107,20 @@ public class MediaQualityService extends SystemService { @Override public PictureProfile createPictureProfile(PictureProfile pp, UserHandle user) { + if ((pp.getPackageName() != null && !pp.getPackageName().isEmpty() + && !incomingPackageEqualsCallingUidPackage(pp.getPackageName())) + && !hasGlobalPictureQualityServicePermission()) { + notifyError(null, PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); ContentValues values = getContentValues(null, pp.getProfileType(), pp.getName(), - pp.getPackageName(), + pp.getPackageName() == null || pp.getPackageName().isEmpty() + ? getPackageOfCallingUid() : pp.getPackageName(), pp.getInputId(), pp.getParameters()); @@ -104,9 +134,13 @@ public class MediaQualityService extends SystemService { @Override public void updatePictureProfile(String id, PictureProfile pp, UserHandle user) { - Long intId = mPictureProfileTempIdMap.getKey(id); + Long dbId = mPictureProfileTempIdMap.getKey(id); + if (!hasPermissionToUpdatePictureProfile(dbId, pp)) { + notifyError(id, PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } - ContentValues values = getContentValues(intId, + ContentValues values = getContentValues(dbId, pp.getProfileType(), pp.getName(), pp.getPackageName(), @@ -118,27 +152,51 @@ public class MediaQualityService extends SystemService { null, values); } + private boolean hasPermissionToUpdatePictureProfile(Long dbId, PictureProfile toUpdate) { + PictureProfile fromDb = getPictureProfile(dbId); + return fromDb.getProfileType() == toUpdate.getProfileType() + && fromDb.getPackageName().equals(toUpdate.getPackageName()) + && fromDb.getName().equals(toUpdate.getName()) + && fromDb.getName().equals(getPackageOfCallingUid()); + } + @Override public void removePictureProfile(String id, UserHandle user) { - Long intId = mPictureProfileTempIdMap.getKey(id); - if (intId != null) { + Long dbId = mPictureProfileTempIdMap.getKey(id); + + if (!hasPermissionToRemovePictureProfile(dbId)) { + notifyError(id, PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + + if (dbId != null) { SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); String selection = BaseParameters.PARAMETER_ID + " = ?"; - String[] selectionArgs = {Long.toString(intId)}; - db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, selection, + String[] selectionArgs = {Long.toString(dbId)}; + int result = db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, selection, selectionArgs); - mPictureProfileTempIdMap.remove(intId); + if (result == 0) { + notifyError(id, PictureProfile.ERROR_INVALID_ARGUMENT, + Binder.getCallingUid(), Binder.getCallingPid()); + } + mPictureProfileTempIdMap.remove(dbId); } } + private boolean hasPermissionToRemovePictureProfile(Long dbId) { + PictureProfile fromDb = getPictureProfile(dbId); + return fromDb.getName().equalsIgnoreCase(getPackageOfCallingUid()); + } + @Override public PictureProfile getPictureProfile(int type, String name, Bundle options, UserHandle user) { boolean includeParams = options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_TYPE + " = ? AND " - + BaseParameters.PARAMETER_NAME + " = ?"; - String[] selectionArguments = {Integer.toString(type), name}; + + BaseParameters.PARAMETER_NAME + " = ? AND " + + BaseParameters.PARAMETER_PACKAGE + " = ?"; + String[] selectionArguments = {Integer.toString(type), name, getPackageOfCallingUid()}; try ( Cursor cursor = getCursorAfterQuerying( @@ -156,13 +214,42 @@ public class MediaQualityService extends SystemService { return null; } cursor.moveToFirst(); - return getPictureProfileWithTempIdFromCursor(cursor); + return convertCursorToPictureProfileWithTempId(cursor); + } + } + + private PictureProfile getPictureProfile(Long dbId) { + String selection = BaseParameters.PARAMETER_ID + " = ?"; + String[] selectionArguments = {Long.toString(dbId)}; + + try ( + Cursor cursor = getCursorAfterQuerying( + mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, + getMediaProfileColumns(false), selection, selectionArguments) + ) { + int count = cursor.getCount(); + if (count == 0) { + return null; + } + if (count > 1) { + Log.wtf(TAG, String.format(Locale.US, "%d entries found for id=%d" + + " in %s. Should only ever be 0 or 1.", count, dbId, + mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME)); + return null; + } + cursor.moveToFirst(); + return convertCursorToPictureProfileWithTempId(cursor); } } @Override public List<PictureProfile> getPictureProfilesByPackage( String packageName, Bundle options, UserHandle user) { + if (!hasGlobalPictureQualityServicePermission()) { + notifyError(null, PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + boolean includeParams = options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; @@ -172,23 +259,31 @@ public class MediaQualityService extends SystemService { } @Override - public List<PictureProfile> getAvailablePictureProfiles(Bundle options, UserHandle user) { - String[] packageNames = mContext.getPackageManager().getPackagesForUid( - Binder.getCallingUid()); - if (packageNames != null && packageNames.length == 1 && !packageNames[0].isEmpty()) { - return getPictureProfilesByPackage(packageNames[0], options, user); + public List<PictureProfile> getAvailablePictureProfiles( + Bundle options, UserHandle user) { + String packageName = getPackageOfCallingUid(); + if (packageName != null) { + return getPictureProfilesByPackage(packageName, options, user); } return new ArrayList<>(); } @Override public boolean setDefaultPictureProfile(String profileId, UserHandle user) { + if (!hasGlobalPictureQualityServicePermission()) { + notifyError(profileId, PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } // TODO: pass the profile ID to MediaQuality HAL when ready. return false; } @Override public List<String> getPictureProfilePackageNames(UserHandle user) { + if (!hasGlobalPictureQualityServicePermission()) { + notifyError(null, PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } String [] column = {BaseParameters.PARAMETER_PACKAGE}; List<PictureProfile> pictureProfiles = getPictureProfilesBasedOnConditions(column, null, null); @@ -210,12 +305,19 @@ public class MediaQualityService extends SystemService { @Override public SoundProfile createSoundProfile(SoundProfile sp, UserHandle user) { + if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty() + && !incomingPackageEqualsCallingUidPackage(sp.getPackageName())) + && !hasGlobalPictureQualityServicePermission()) { + //TODO: error handling + return null; + } SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); ContentValues values = getContentValues(null, sp.getProfileType(), sp.getName(), - sp.getPackageName(), + sp.getPackageName() == null || sp.getPackageName().isEmpty() + ? getPackageOfCallingUid() : sp.getPackageName(), sp.getInputId(), sp.getParameters()); @@ -229,9 +331,14 @@ public class MediaQualityService extends SystemService { @Override public void updateSoundProfile(String id, SoundProfile sp, UserHandle user) { - Long intId = mSoundProfileTempIdMap.getKey(id); + Long dbId = mSoundProfileTempIdMap.getKey(id); - ContentValues values = getContentValues(intId, + if (!hasPermissionToUpdateSoundProfile(dbId, sp)) { + //TODO: error handling + return; + } + + ContentValues values = getContentValues(dbId, sp.getProfileType(), sp.getName(), sp.getPackageName(), @@ -242,27 +349,49 @@ public class MediaQualityService extends SystemService { db.replace(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, null, values); } + private boolean hasPermissionToUpdateSoundProfile(Long dbId, SoundProfile sp) { + SoundProfile fromDb = getSoundProfile(dbId); + return fromDb.getProfileType() == sp.getProfileType() + && fromDb.getPackageName().equals(sp.getPackageName()) + && fromDb.getName().equals(sp.getName()) + && fromDb.getName().equals(getPackageOfCallingUid()); + } + @Override public void removeSoundProfile(String id, UserHandle user) { Long intId = mSoundProfileTempIdMap.getKey(id); + if (!hasPermissionToRemoveSoundProfile(intId)) { + //TODO: error handling + return; + } + if (intId != null) { SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); String selection = BaseParameters.PARAMETER_ID + " = ?"; String[] selectionArgs = {Long.toString(intId)}; - db.delete(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, selection, + int result = db.delete(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, selection, selectionArgs); + if (result == 0) { + //TODO: error handling + } mSoundProfileTempIdMap.remove(intId); } } + private boolean hasPermissionToRemoveSoundProfile(Long dbId) { + SoundProfile fromDb = getSoundProfile(dbId); + return fromDb.getName().equalsIgnoreCase(getPackageOfCallingUid()); + } + @Override - public SoundProfile getSoundProfile(int type, String id, Bundle options, + public SoundProfile getSoundProfile(int type, String name, Bundle options, UserHandle user) { boolean includeParams = options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_TYPE + " = ? AND " - + BaseParameters.PARAMETER_ID + " = ?"; - String[] selectionArguments = {String.valueOf(type), id}; + + BaseParameters.PARAMETER_NAME + " = ? AND " + + BaseParameters.PARAMETER_PACKAGE + " = ?"; + String[] selectionArguments = {String.valueOf(type), name, getPackageOfCallingUid()}; try ( Cursor cursor = getCursorAfterQuerying( @@ -275,18 +404,47 @@ public class MediaQualityService extends SystemService { } if (count > 1) { Log.wtf(TAG, String.format(Locale.US, "%d entries found for id=%s" - + " in %s. Should only ever be 0 or 1.", count, id, + + " in %s. Should only ever be 0 or 1.", count, name, + mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME)); + return null; + } + cursor.moveToFirst(); + return convertCursorToSoundProfileWithTempId(cursor); + } + } + + private SoundProfile getSoundProfile(Long dbId) { + String selection = BaseParameters.PARAMETER_ID + " = ?"; + String[] selectionArguments = {Long.toString(dbId)}; + + try ( + Cursor cursor = getCursorAfterQuerying( + mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, + getMediaProfileColumns(false), selection, selectionArguments) + ) { + int count = cursor.getCount(); + if (count == 0) { + return null; + } + if (count > 1) { + Log.wtf(TAG, String.format(Locale.US, "%d entries found for id=%s " + + "in %s. Should only ever be 0 or 1.", count, dbId, mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME)); return null; } cursor.moveToFirst(); - return getSoundProfileWithTempIdFromCursor(cursor); + return convertCursorToSoundProfileWithTempId(cursor); } } @Override public List<SoundProfile> getSoundProfilesByPackage( String packageName, Bundle options, UserHandle user) { + if (!hasGlobalSoundQualityServicePermission()) { + //TODO: error handling + return new ArrayList<>(); + } + boolean includeParams = options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; @@ -296,24 +454,30 @@ public class MediaQualityService extends SystemService { } @Override - public List<SoundProfile> getAvailableSoundProfiles( - Bundle options, UserHandle user) { - String[] packageNames = mContext.getPackageManager().getPackagesForUid( - Binder.getCallingUid()); - if (packageNames != null && packageNames.length == 1 && !packageNames[0].isEmpty()) { - return getSoundProfilesByPackage(packageNames[0], options, user); + public List<SoundProfile> getAvailableSoundProfiles(Bundle options, UserHandle user) { + String packageName = getPackageOfCallingUid(); + if (packageName != null) { + return getSoundProfilesByPackage(packageName, options, user); } return new ArrayList<>(); } @Override public boolean setDefaultSoundProfile(String profileId, UserHandle user) { + if (!hasGlobalSoundQualityServicePermission()) { + //TODO: error handling + return false; + } // TODO: pass the profile ID to MediaQuality HAL when ready. return false; } @Override public List<String> getSoundProfilePackageNames(UserHandle user) { + if (!hasGlobalSoundQualityServicePermission()) { + //TODO: error handling + return new ArrayList<>(); + } String [] column = {BaseParameters.PARAMETER_NAME}; List<SoundProfile> soundProfiles = getSoundProfilesBasedOnConditions(column, null, null); @@ -323,6 +487,37 @@ public class MediaQualityService extends SystemService { .collect(Collectors.toList()); } + private String getPackageOfCallingUid() { + String[] packageNames = mPackageManager.getPackagesForUid( + Binder.getCallingUid()); + if (packageNames != null && packageNames.length == 1 && !packageNames[0].isEmpty()) { + return packageNames[0]; + } + return null; + } + + private boolean incomingPackageEqualsCallingUidPackage(String incomingPackage) { + return incomingPackage.equalsIgnoreCase(getPackageOfCallingUid()); + } + + private boolean hasGlobalPictureQualityServicePermission() { + return mPackageManager.checkPermission(android.Manifest.permission + .MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE, + mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED; + } + + private boolean hasGlobalSoundQualityServicePermission() { + return mPackageManager.checkPermission(android.Manifest.permission + .MANAGE_GLOBAL_SOUND_QUALITY_SERVICE, + mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED; + } + + private boolean hasReadColorZonesPermission() { + return mPackageManager.checkPermission(android.Manifest.permission + .READ_COLOR_ZONES, + mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED; + } + private void populateTempIdMap(BiMap<Long, String> map, Long id) { if (id != null && map.getValue(id) == null) { String uuid; @@ -430,7 +625,7 @@ public class MediaQualityService extends SystemService { return columns.toArray(new String[0]); } - private PictureProfile getPictureProfileWithTempIdFromCursor(Cursor cursor) { + private PictureProfile convertCursorToPictureProfileWithTempId(Cursor cursor) { return new PictureProfile( getTempId(mPictureProfileTempIdMap, cursor), getType(cursor), @@ -442,7 +637,7 @@ public class MediaQualityService extends SystemService { ); } - private SoundProfile getSoundProfileWithTempIdFromCursor(Cursor cursor) { + private SoundProfile convertCursorToSoundProfileWithTempId(Cursor cursor) { return new SoundProfile( getTempId(mSoundProfileTempIdMap, cursor), getType(cursor), @@ -502,7 +697,7 @@ public class MediaQualityService extends SystemService { ) { List<PictureProfile> pictureProfiles = new ArrayList<>(); while (cursor.moveToNext()) { - pictureProfiles.add(getPictureProfileWithTempIdFromCursor(cursor)); + pictureProfiles.add(convertCursorToPictureProfileWithTempId(cursor)); } return pictureProfiles; } @@ -517,30 +712,64 @@ public class MediaQualityService extends SystemService { ) { List<SoundProfile> soundProfiles = new ArrayList<>(); while (cursor.moveToNext()) { - soundProfiles.add(getSoundProfileWithTempIdFromCursor(cursor)); + soundProfiles.add(convertCursorToSoundProfileWithTempId(cursor)); } return soundProfiles; } } + private void notifyError(String profileId, int errorCode, int uid, int pid) { + UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM); + int n = userState.mCallbacks.beginBroadcast(); + + for (int i = 0; i < n; ++i) { + try { + IPictureProfileCallback callback = userState.mCallbacks.getBroadcastItem(i); + Pair<Integer, Integer> pidUid = userState.mCallbackPidUidMap.get(callback); + + if (pidUid.first == pid && pidUid.second == uid) { + userState.mCallbacks.getBroadcastItem(i).onError(profileId, errorCode); + } + } catch (RemoteException e) { + Slog.e(TAG, "failed to report added input to callback", e); + } + } + userState.mCallbacks.finishBroadcast(); + } + @Override public void registerPictureProfileCallback(final IPictureProfileCallback callback) { + int callingPid = Binder.getCallingPid(); + int callingUid = Binder.getCallingUid(); + + UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid()); + userState.mCallbackPidUidMap.put(callback, Pair.create(callingPid, callingUid)); } + @Override public void registerSoundProfileCallback(final ISoundProfileCallback callback) { } @Override public void registerAmbientBacklightCallback(IAmbientBacklightCallback callback) { + if (!hasReadColorZonesPermission()) { + //TODO: error handling + } } @Override public void setAmbientBacklightSettings( AmbientBacklightSettings settings, UserHandle user) { + if (!hasReadColorZonesPermission()) { + //TODO: error handling + } } @Override public void setAmbientBacklightEnabled(boolean enabled, UserHandle user) { + if (!hasReadColorZonesPermission()) { + //TODO: error handling + } } @Override @@ -551,20 +780,34 @@ public class MediaQualityService extends SystemService { @Override public List<String> getPictureProfileAllowList(UserHandle user) { + if (!hasGlobalPictureQualityServicePermission()) { + //TODO: error handling + return new ArrayList<>(); + } return new ArrayList<>(); } @Override public void setPictureProfileAllowList(List<String> packages, UserHandle user) { + if (!hasGlobalPictureQualityServicePermission()) { + //TODO: error handling + } } @Override public List<String> getSoundProfileAllowList(UserHandle user) { + if (!hasGlobalSoundQualityServicePermission()) { + //TODO: error handling + return new ArrayList<>(); + } return new ArrayList<>(); } @Override public void setSoundProfileAllowList(List<String> packages, UserHandle user) { + if (!hasGlobalSoundQualityServicePermission()) { + //TODO: error handling + } } @Override @@ -574,28 +817,94 @@ public class MediaQualityService extends SystemService { @Override public void setAutoPictureQualityEnabled(boolean enabled, UserHandle user) { + if (!hasGlobalPictureQualityServicePermission()) { + //TODO: error handling + } + + try { + if (mMediaQuality != null) { + mMediaQuality.setAutoPqEnabled(enabled); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto picture quality", e); + } } @Override public boolean isAutoPictureQualityEnabled(UserHandle user) { + try { + if (mMediaQuality != null) { + return mMediaQuality.getAutoPqEnabled(); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get auto picture quality", e); + } return false; } @Override public void setSuperResolutionEnabled(boolean enabled, UserHandle user) { + if (!hasGlobalPictureQualityServicePermission()) { + //TODO: error handling + } + + try { + if (mMediaQuality != null) { + mMediaQuality.setAutoSrEnabled(enabled); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto super resolution", e); + } } @Override public boolean isSuperResolutionEnabled(UserHandle user) { + try { + if (mMediaQuality != null) { + return mMediaQuality.getAutoSrEnabled(); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get auto super resolution", e); + } return false; } @Override public void setAutoSoundQualityEnabled(boolean enabled, UserHandle user) { + if (!hasGlobalSoundQualityServicePermission()) { + //TODO: error handling + } + + try { + if (mMediaQuality != null) { + mMediaQuality.setAutoAqEnabled(enabled); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto audio quality", e); + } } @Override public boolean isAutoSoundQualityEnabled(UserHandle user) { + try { + if (mMediaQuality != null) { + return mMediaQuality.getAutoAqEnabled(); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get auto audio quality", e); + } return false; } @@ -604,4 +913,38 @@ public class MediaQualityService extends SystemService { return false; } } + + private class MediaQualityManagerCallbackList extends + RemoteCallbackList<IPictureProfileCallback> { + @Override + public void onCallbackDied(IPictureProfileCallback callback) { + //todo + } + } + + private final class UserState { + // A list of callbacks. + private final MediaQualityManagerCallbackList mCallbacks = + new MediaQualityManagerCallbackList(); + + private final Map<IPictureProfileCallback, Pair<Integer, Integer>> mCallbackPidUidMap = + new HashMap<>(); + + private UserState(Context context, int userId) { + + } + } + + private UserState getOrCreateUserStateLocked(int userId) { + UserState userState = getUserStateLocked(userId); + if (userState == null) { + userState = new UserState(mContext, userId); + mUserStates.put(userId, userState); + } + return userState; + } + + private UserState getUserStateLocked(int userId) { + return mUserStates.get(userId); + } } diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java index 0b40d64e3a09..3f2c2228e453 100644 --- a/services/core/java/com/android/server/notification/ConditionProviders.java +++ b/services/core/java/com/android/server/notification/ConditionProviders.java @@ -325,7 +325,7 @@ public class ConditionProviders extends ManagedServices { for (int i = 0; i < N; i++) { final Condition c = conditions[i]; if (mCallback != null) { - mCallback.onConditionChanged(c.id, c); + mCallback.onConditionChanged(c.id, c, info.uid); } } } @@ -515,7 +515,7 @@ public class ConditionProviders extends ManagedServices { public interface Callback { void onServiceAdded(ComponentName component); - void onConditionChanged(Uri id, Condition condition); + void onConditionChanged(Uri id, Condition condition, int callerUid); } } diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java index 4b41696a4390..e47f8ae9d3a5 100644 --- a/services/core/java/com/android/server/notification/GroupHelper.java +++ b/services/core/java/com/android/server/notification/GroupHelper.java @@ -583,6 +583,15 @@ public class GroupHelper { final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey( record.getUserId(), pkgName, sectioner); + // The notification was part of a different section => trigger regrouping + final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record); + if (prevSectionKey != null && !fullAggregateGroupKey.equals(prevSectionKey)) { + if (DEBUG) { + Slog.i(TAG, "Section changed for: " + record); + } + maybeUngroupOnSectionChanged(record, prevSectionKey); + } + // This notification is already aggregated if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) { return false; @@ -652,10 +661,33 @@ public class GroupHelper { } /** + * A notification was added that was previously part of a different section and needs to trigger + * GH state cleanup. + */ + private void maybeUngroupOnSectionChanged(NotificationRecord record, + FullyQualifiedGroupKey prevSectionKey) { + maybeUngroupWithSections(record, prevSectionKey); + if (record.getGroupKey().equals(prevSectionKey.toString())) { + record.setOverrideGroupKey(null); + } + } + + /** * A notification was added that is app-grouped. */ private void maybeUngroupOnAppGrouped(NotificationRecord record) { - maybeUngroupWithSections(record, getSectionGroupKeyWithFallback(record)); + FullyQualifiedGroupKey currentSectionKey = getSectionGroupKeyWithFallback(record); + + // The notification was part of a different section => trigger regrouping + final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record); + if (prevSectionKey != null && !prevSectionKey.equals(currentSectionKey)) { + if (DEBUG) { + Slog.i(TAG, "Section changed for: " + record); + } + currentSectionKey = prevSectionKey; + } + + maybeUngroupWithSections(record, currentSectionKey); } /** diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java index 7cbbe2938fd5..5a425057ea89 100644 --- a/services/core/java/com/android/server/notification/NotificationDelegate.java +++ b/services/core/java/com/android/server/notification/NotificationDelegate.java @@ -107,4 +107,9 @@ public interface NotificationDelegate { * @param key the notification key */ void unbundleNotification(String key); + /** + * Called when the notification should be rebundled. + * @param key the notification key + */ + void rebundleNotification(String key); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index f50e8aa7eb7b..341038f878d9 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1888,6 +1888,36 @@ public class NotificationManagerService extends SystemService { } } } + + @Override + public void rebundleNotification(String key) { + if (!(notificationClassification() && notificationRegroupOnClassification())) { + return; + } + synchronized (mNotificationLock) { + NotificationRecord r = mNotificationsByKey.get(key); + if (r == null) { + return; + } + + if (DBG) { + Slog.v(TAG, "rebundleNotification: " + r); + } + + if (r.getBundleType() != Adjustment.TYPE_OTHER) { + final Bundle classifBundle = new Bundle(); + classifBundle.putInt(KEY_TYPE, r.getBundleType()); + Adjustment adj = new Adjustment(r.getSbn().getPackageName(), r.getKey(), + classifBundle, "rebundle", r.getUserId()); + applyAdjustmentLocked(r, adj, /* isPosted= */ true); + mRankingHandler.requestSort(); + } else { + if (DBG) { + Slog.w(TAG, "Can't rebundle. No valid bundle type for: " + r); + } + } + } + } }; NotificationManagerPrivate mNotificationManagerPrivate = new NotificationManagerPrivate() { @@ -3162,6 +3192,7 @@ public class NotificationManagerService extends SystemService { mAssistants.onBootPhaseAppsCanStart(); mConditionProviders.onBootPhaseAppsCanStart(); mHistoryManager.onBootPhaseAppsCanStart(); + mPreferencesHelper.onBootPhaseAppsCanStart(); migrateDefaultNAS(); maybeShowInitialReviewPermissionsNotification(); @@ -5903,8 +5934,9 @@ public class NotificationManagerService extends SystemService { // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined. @Override public List<ZenModeConfig.ZenRule> getZenRules() throws RemoteException { - enforcePolicyAccess(Binder.getCallingUid(), "getZenRules"); - return mZenModeHelper.getZenRules(getCallingZenUser()); + int callingUid = Binder.getCallingUid(); + enforcePolicyAccess(callingUid, "getZenRules"); + return mZenModeHelper.getZenRules(getCallingZenUser(), callingUid); } @Override @@ -5912,15 +5944,17 @@ public class NotificationManagerService extends SystemService { if (!android.app.Flags.modesApi()) { throw new IllegalStateException("getAutomaticZenRules called with flag off!"); } - enforcePolicyAccess(Binder.getCallingUid(), "getAutomaticZenRules"); - return mZenModeHelper.getAutomaticZenRules(getCallingZenUser()); + int callingUid = Binder.getCallingUid(); + enforcePolicyAccess(callingUid, "getAutomaticZenRules"); + return mZenModeHelper.getAutomaticZenRules(getCallingZenUser(), callingUid); } @Override public AutomaticZenRule getAutomaticZenRule(String id) throws RemoteException { Objects.requireNonNull(id, "Id is null"); - enforcePolicyAccess(Binder.getCallingUid(), "getAutomaticZenRule"); - return mZenModeHelper.getAutomaticZenRule(getCallingZenUser(), id); + int callingUid = Binder.getCallingUid(); + enforcePolicyAccess(callingUid, "getAutomaticZenRule"); + return mZenModeHelper.getAutomaticZenRule(getCallingZenUser(), id, callingUid); } @Override @@ -6065,8 +6099,9 @@ public class NotificationManagerService extends SystemService { @Condition.State public int getAutomaticZenRuleState(@NonNull String id) { Objects.requireNonNull(id, "id is null"); - enforcePolicyAccess(Binder.getCallingUid(), "getAutomaticZenRuleState"); - return mZenModeHelper.getAutomaticZenRuleState(getCallingZenUser(), id); + int callingUid = Binder.getCallingUid(); + enforcePolicyAccess(callingUid, "getAutomaticZenRuleState"); + return mZenModeHelper.getAutomaticZenRuleState(getCallingZenUser(), id, callingUid); } @Override @@ -7129,6 +7164,7 @@ public class NotificationManagerService extends SystemService { adjustments.putParcelable(KEY_TYPE, newChannel); logClassificationChannelAdjustmentReceived(r, isPosted, classification); + r.setBundleType(classification); } } r.addAdjustment(adjustment); @@ -9532,7 +9568,8 @@ public class NotificationManagerService extends SystemService { || !Objects.equals(oldSbn.getNotification().getGroup(), n.getNotification().getGroup()) || oldSbn.getNotification().flags - != n.getNotification().flags) { + != n.getNotification().flags + || !old.getChannel().getId().equals(r.getChannel().getId())) { synchronized (mNotificationLock) { final String autogroupName = notificationForceGrouping() ? diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 93f512bc7e17..81af0d8a6d80 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -36,10 +36,7 @@ import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.Person; -import android.content.ContentProvider; -import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ShortcutInfo; @@ -48,7 +45,6 @@ import android.media.AudioAttributes; import android.media.AudioSystem; import android.metrics.LogMaker; import android.net.Uri; -import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; @@ -226,6 +222,9 @@ public final class NotificationRecord { // lifetime extended. private boolean mCanceledAfterLifetimeExtension = false; + // type of the bundle if the notification was classified + private @Adjustment.Types int mBundleType = Adjustment.TYPE_OTHER; + public NotificationRecord(Context context, StatusBarNotification sbn, NotificationChannel channel) { this.sbn = sbn; @@ -471,6 +470,10 @@ public final class NotificationRecord { } } + if (android.service.notification.Flags.notificationClassification()) { + mBundleType = previous.mBundleType; + } + // Don't copy importance information or mGlobalSortKey, recompute them. } @@ -1493,23 +1496,14 @@ public final class NotificationRecord { final Notification notification = getNotification(); notification.visitUris((uri) -> { - if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { - visitGrantableUri(uri, false, false); - } else { - oldVisitGrantableUri(uri, false, false); - } + visitGrantableUri(uri, false, false); }); if (notification.getChannelId() != null) { NotificationChannel channel = getChannel(); if (channel != null) { - if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { - visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() - & NotificationChannel.USER_LOCKED_SOUND) != 0, true); - } else { - oldVisitGrantableUri(channel.getSound(), (channel.getUserLockedFields() - & NotificationChannel.USER_LOCKED_SOUND) != 0, true); - } + visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() + & NotificationChannel.USER_LOCKED_SOUND) != 0, true); } } } finally { @@ -1525,53 +1519,6 @@ public final class NotificationRecord { * {@link #mGrantableUris}. Otherwise, this will either log or throw * {@link SecurityException} depending on target SDK of enqueuing app. */ - private void oldVisitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { - if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; - - if (mGrantableUris != null && mGrantableUris.contains(uri)) { - return; // already verified this URI - } - - final int sourceUid = getSbn().getUid(); - final long ident = Binder.clearCallingIdentity(); - try { - // This will throw a SecurityException if the caller can't grant. - mUgmInternal.checkGrantUriPermission(sourceUid, null, - ContentProvider.getUriWithoutUserId(uri), - Intent.FLAG_GRANT_READ_URI_PERMISSION, - ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid))); - - if (mGrantableUris == null) { - mGrantableUris = new ArraySet<>(); - } - mGrantableUris.add(uri); - } catch (SecurityException e) { - if (!userOverriddenUri) { - if (isSound) { - mSound = Settings.System.DEFAULT_NOTIFICATION_URI; - Log.w(TAG, "Replacing " + uri + " from " + sourceUid + ": " + e.getMessage()); - } else { - if (mTargetSdkVersion >= Build.VERSION_CODES.P) { - throw e; - } else { - Log.w(TAG, - "Ignoring " + uri + " from " + sourceUid + ": " + e.getMessage()); - } - } - } - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - /** - * Note the presence of a {@link Uri} that should have permission granted to - * whoever will be rendering it. - * <p> - * If the enqueuing app has the ability to grant access, it will be added to - * {@link #mGrantableUris}. Otherwise, this will either log or throw - * {@link SecurityException} depending on target SDK of enqueuing app. - */ private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { if (mGrantableUris != null && mGrantableUris.contains(uri)) { @@ -1689,6 +1636,14 @@ public final class NotificationRecord { mCanceledAfterLifetimeExtension = canceledAfterLifetimeExtension; } + public @Adjustment.Types int getBundleType() { + return mBundleType; + } + + public void setBundleType(@Adjustment.Types int bundleType) { + mBundleType = bundleType; + } + /** * Whether this notification is a conversation notification. */ diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 15377d6b269a..36eabae69b22 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -82,7 +82,6 @@ import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntArray; -import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseBooleanArray; @@ -272,6 +271,15 @@ public class PreferencesHelper implements RankingConfig { updateMediaNotificationFilteringEnabled(); } + void onBootPhaseAppsCanStart() { + // IpcDataCaches must be invalidated once data becomes available, as queries will only + // begin to be cached after the first invalidation signal. At this point, we know about all + // notification channels. + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } + } + public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId) throws XmlPullParserException, IOException { int type = parser.getEventType(); @@ -531,12 +539,14 @@ public class PreferencesHelper implements RankingConfig { private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg, @UserIdInt int userId, int uid, int importance, int priority, int visibility, boolean showBadge, int bubblePreference, long creationTime) { + boolean created = false; final String key = packagePreferencesKey(pkg, uid); PackagePreferences r = (uid == UNKNOWN_UID) ? mRestoredWithoutUids.get(unrestoredPackageKey(pkg, userId)) : mPackagePreferences.get(key); if (r == null) { + created = true; r = new PackagePreferences(); r.pkg = pkg; r.uid = uid; @@ -572,6 +582,9 @@ public class PreferencesHelper implements RankingConfig { mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, userId)); } } + if (android.app.Flags.nmBinderPerfCacheChannels() && created) { + invalidateNotificationChannelCache(); + } return r; } @@ -664,6 +677,9 @@ public class PreferencesHelper implements RankingConfig { } NotificationChannel channel = new NotificationChannel(channelId, label, IMPORTANCE_LOW); p.channels.put(channelId, channel); + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } return channel; } @@ -1171,9 +1187,7 @@ public class PreferencesHelper implements RankingConfig { // Verify that the app has permission to read the sound Uri // Only check for new channels, as regular apps can only set sound // before creating. See: {@link NotificationChannel#setSound} - if (Flags.notificationVerifyChannelSoundUri()) { - PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid); - } + PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid); channel.setImportanceLockedByCriticalDeviceFunction( r.defaultAppLockedImportance || r.fixedImportance); @@ -1208,6 +1222,10 @@ public class PreferencesHelper implements RankingConfig { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + if (android.app.Flags.nmBinderPerfCacheChannels() && needsPolicyFileChange) { + invalidateNotificationChannelCache(); + } + return needsPolicyFileChange; } @@ -1229,6 +1247,9 @@ public class PreferencesHelper implements RankingConfig { } channel.unlockFields(USER_LOCKED_IMPORTANCE); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } @@ -1301,6 +1322,9 @@ public class PreferencesHelper implements RankingConfig { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } if (changed) { + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } updateConfig(); } } @@ -1537,6 +1561,10 @@ public class PreferencesHelper implements RankingConfig { if (channelBypassedDnd) { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + + if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannel) { + invalidateNotificationChannelCache(); + } return deletedChannel; } @@ -1566,6 +1594,9 @@ public class PreferencesHelper implements RankingConfig { } r.channels.remove(channelId); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } @Override @@ -1576,13 +1607,18 @@ public class PreferencesHelper implements RankingConfig { if (r == null) { return; } + boolean deleted = false; int N = r.channels.size() - 1; for (int i = N; i >= 0; i--) { String key = r.channels.keyAt(i); if (!DEFAULT_CHANNEL_ID.equals(key)) { r.channels.remove(key); + deleted = true; } } + if (android.app.Flags.nmBinderPerfCacheChannels() && deleted) { + invalidateNotificationChannelCache(); + } } } @@ -1613,6 +1649,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public void updateDefaultApps(int userId, ArraySet<String> toRemove, @@ -1642,6 +1681,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg, @@ -1757,6 +1799,9 @@ public class PreferencesHelper implements RankingConfig { if (groupBypassedDnd) { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannels.size() > 0) { + invalidateNotificationChannelCache(); + } return deletedChannels; } @@ -1902,8 +1947,13 @@ public class PreferencesHelper implements RankingConfig { } } } - if (!deletedChannelIds.isEmpty() && mCurrentUserHasChannelsBypassingDnd) { - updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + if (!deletedChannelIds.isEmpty()) { + if (mCurrentUserHasChannelsBypassingDnd) { + updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } return deletedChannelIds; } @@ -2196,6 +2246,11 @@ public class PreferencesHelper implements RankingConfig { PackagePreferences prefs = getOrCreatePackagePreferencesLocked(sourcePkg, sourceUid); prefs.delegate = new Delegate(delegatePkg, delegateUid, true); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + // If package delegates change, then which packages can get what channel information + // also changes, so we need to clear the cache. + invalidateNotificationChannelCache(); + } } /** @@ -2208,6 +2263,9 @@ public class PreferencesHelper implements RankingConfig { prefs.delegate.mEnabled = false; } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } /** @@ -2811,18 +2869,24 @@ public class PreferencesHelper implements RankingConfig { public void onUserRemoved(int userId) { synchronized (mLock) { + boolean removed = false; int N = mPackagePreferences.size(); for (int i = N - 1; i >= 0; i--) { PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i); if (UserHandle.getUserId(PackagePreferences.uid) == userId) { mPackagePreferences.removeAt(i); + removed = true; } } + if (android.app.Flags.nmBinderPerfCacheChannels() && removed) { + invalidateNotificationChannelCache(); + } } } protected void onLocaleChanged(Context context, int userId) { synchronized (mLock) { + boolean updated = false; int N = mPackagePreferences.size(); for (int i = 0; i < N; i++) { PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i); @@ -2833,10 +2897,14 @@ public class PreferencesHelper implements RankingConfig { DEFAULT_CHANNEL_ID).setName( context.getResources().getString( R.string.default_notification_channel_label)); + updated = true; } // TODO (b/346396459): Localize all reserved channels } } + if (android.app.Flags.nmBinderPerfCacheChannels() && updated) { + invalidateNotificationChannelCache(); + } } } @@ -2884,7 +2952,7 @@ public class PreferencesHelper implements RankingConfig { channel.getAudioAttributes().getUsage()); if (Settings.System.DEFAULT_NOTIFICATION_URI.equals( restoredUri)) { - Log.w(TAG, + Slog.w(TAG, "Could not restore sound: " + uri + " for channel: " + channel); } @@ -2922,6 +2990,9 @@ public class PreferencesHelper implements RankingConfig { if (updated) { updateConfig(); + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } return updated; } @@ -2939,6 +3010,9 @@ public class PreferencesHelper implements RankingConfig { p.priority = DEFAULT_PRIORITY; p.visibility = DEFAULT_VISIBILITY; p.showBadge = DEFAULT_SHOW_BADGE; + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } } } @@ -3123,6 +3197,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public void migrateNotificationPermissions(List<UserInfo> users) { @@ -3154,6 +3231,12 @@ public class PreferencesHelper implements RankingConfig { mRankingHandler.requestSort(); } + @VisibleForTesting + // Utility method for overriding in tests to confirm that the cache gets cleared. + protected void invalidateNotificationChannelCache() { + NotificationManager.invalidateNotificationChannelCache(); + } + private static String packagePreferencesKey(String pkg, int uid) { return pkg + "|" + uid; } diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java index 52d0c41614d5..d44baeb58a28 100644 --- a/services/core/java/com/android/server/notification/ZenModeConditions.java +++ b/services/core/java/com/android/server/notification/ZenModeConditions.java @@ -113,15 +113,18 @@ public class ZenModeConditions implements ConditionProviders.Callback { } @Override - public void onConditionChanged(Uri id, Condition condition) { + public void onConditionChanged(Uri id, Condition condition, int callingUid) { if (DEBUG) Log.d(TAG, "onConditionChanged " + id + " " + condition); ZenModeConfig config = mHelper.getConfig(); if (config == null) return; - final int callingUid = Binder.getCallingUid(); + if (!Flags.fixCallingUidFromCps()) { + // Old behavior: overwrite with known-bad callingUid (always system_server). + callingUid = Binder.getCallingUid(); + } // This change is known to be for UserHandle.CURRENT because ConditionProviders for // background users are not bound. - mHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, condition, + mHelper.setAutomaticZenRuleStateFromConditionProvider(UserHandle.CURRENT, id, condition, callingUid == Process.SYSTEM_UID ? ZenModeConfig.ORIGIN_SYSTEM : ZenModeConfig.ORIGIN_APP, callingUid); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index b571d62c0cba..0a63f3fb36d0 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -413,13 +413,13 @@ public class ZenModeHelper { } // TODO: b/310620812 - Make private (or inline) when MODES_API is inlined. - public List<ZenRule> getZenRules(UserHandle user) { + public List<ZenRule> getZenRules(UserHandle user, int callingUid) { List<ZenRule> rules = new ArrayList<>(); synchronized (mConfigLock) { ZenModeConfig config = getConfigLocked(user); if (config == null) return rules; for (ZenRule rule : config.automaticRules.values()) { - if (canManageAutomaticZenRule(rule)) { + if (canManageAutomaticZenRule(rule, callingUid)) { rules.add(rule); } } @@ -432,8 +432,8 @@ public class ZenModeHelper { * (which means the owned rules for a regular app, and every rule for system callers) together * with their ids. */ - Map<String, AutomaticZenRule> getAutomaticZenRules(UserHandle user) { - List<ZenRule> ruleList = getZenRules(user); + Map<String, AutomaticZenRule> getAutomaticZenRules(UserHandle user, int callingUid) { + List<ZenRule> ruleList = getZenRules(user, callingUid); HashMap<String, AutomaticZenRule> rules = new HashMap<>(ruleList.size()); for (ZenRule rule : ruleList) { rules.put(rule.id, zenRuleToAutomaticZenRule(rule)); @@ -441,7 +441,7 @@ public class ZenModeHelper { return rules; } - public AutomaticZenRule getAutomaticZenRule(UserHandle user, String id) { + public AutomaticZenRule getAutomaticZenRule(UserHandle user, String id, int callingUid) { ZenRule rule; synchronized (mConfigLock) { ZenModeConfig config = getConfigLocked(user); @@ -449,7 +449,7 @@ public class ZenModeHelper { rule = config.automaticRules.get(id); } if (rule == null) return null; - if (canManageAutomaticZenRule(rule)) { + if (canManageAutomaticZenRule(rule, callingUid)) { return zenRuleToAutomaticZenRule(rule); } return null; @@ -591,7 +591,7 @@ public class ZenModeHelper { + " reason=" + reason); } ZenModeConfig.ZenRule oldRule = config.automaticRules.get(ruleId); - if (oldRule == null || !canManageAutomaticZenRule(oldRule)) { + if (oldRule == null || !canManageAutomaticZenRule(oldRule, callingUid)) { throw new SecurityException( "Cannot update rules not owned by your condition provider"); } @@ -859,7 +859,7 @@ public class ZenModeHelper { newConfig = config.copy(); ZenRule ruleToRemove = newConfig.automaticRules.get(id); if (ruleToRemove == null) return false; - if (canManageAutomaticZenRule(ruleToRemove)) { + if (canManageAutomaticZenRule(ruleToRemove, callingUid)) { newConfig.automaticRules.remove(id); maybePreserveRemovedRule(newConfig, ruleToRemove, origin); if (ruleToRemove.getPkg() != null @@ -893,7 +893,8 @@ public class ZenModeHelper { newConfig = config.copy(); for (int i = newConfig.automaticRules.size() - 1; i >= 0; i--) { ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i)); - if (Objects.equals(rule.getPkg(), packageName) && canManageAutomaticZenRule(rule)) { + if (Objects.equals(rule.getPkg(), packageName) + && canManageAutomaticZenRule(rule, callingUid)) { newConfig.automaticRules.removeAt(i); maybePreserveRemovedRule(newConfig, rule, origin); } @@ -938,14 +939,14 @@ public class ZenModeHelper { } @Condition.State - int getAutomaticZenRuleState(UserHandle user, String id) { + int getAutomaticZenRuleState(UserHandle user, String id, int callingUid) { synchronized (mConfigLock) { ZenModeConfig config = getConfigLocked(user); if (config == null) { return Condition.STATE_UNKNOWN; } ZenRule rule = config.automaticRules.get(id); - if (rule == null || !canManageAutomaticZenRule(rule)) { + if (rule == null || !canManageAutomaticZenRule(rule, callingUid)) { return Condition.STATE_UNKNOWN; } if (Flags.modesApi() && Flags.modesUi()) { @@ -968,7 +969,7 @@ public class ZenModeHelper { newConfig = config.copy(); ZenRule rule = newConfig.automaticRules.get(id); if (Flags.modesApi()) { - if (rule != null && canManageAutomaticZenRule(rule)) { + if (rule != null && canManageAutomaticZenRule(rule, callingUid)) { setAutomaticZenRuleStateLocked(newConfig, Collections.singletonList(rule), condition, origin, callingUid); } @@ -980,8 +981,8 @@ public class ZenModeHelper { } } - void setAutomaticZenRuleState(UserHandle user, Uri ruleDefinition, Condition condition, - @ConfigOrigin int origin, int callingUid) { + void setAutomaticZenRuleStateFromConditionProvider(UserHandle user, Uri ruleDefinition, + Condition condition, @ConfigOrigin int origin, int callingUid) { checkSetRuleStateOrigin("setAutomaticZenRuleState(Uri ruleDefinition)", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { @@ -992,7 +993,7 @@ public class ZenModeHelper { List<ZenRule> matchingRules = findMatchingRules(newConfig, ruleDefinition, condition); if (Flags.modesApi()) { for (int i = matchingRules.size() - 1; i >= 0; i--) { - if (!canManageAutomaticZenRule(matchingRules.get(i))) { + if (!canManageAutomaticZenRule(matchingRules.get(i), callingUid)) { matchingRules.remove(i); } } @@ -1125,15 +1126,21 @@ public class ZenModeHelper { return count; } - public boolean canManageAutomaticZenRule(ZenRule rule) { - final int callingUid = Binder.getCallingUid(); + public boolean canManageAutomaticZenRule(ZenRule rule, int callingUid) { + if (!com.android.server.notification.Flags.fixCallingUidFromCps()) { + // Old behavior: ignore supplied callingUid and instead obtain it here. Will be + // incorrect if not currently handling a Binder call. + callingUid = Binder.getCallingUid(); + } + if (callingUid == 0 || callingUid == Process.SYSTEM_UID) { + // Checked specifically, because checkCallingPermission() will fail. return true; } else if (mContext.checkCallingPermission(android.Manifest.permission.MANAGE_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { return true; } else { - String[] packages = mPm.getPackagesForUid(Binder.getCallingUid()); + String[] packages = mPm.getPackagesForUid(callingUid); if (packages != null) { final int packageCount = packages.length; for (int i = 0; i < packageCount; i++) { @@ -2902,8 +2909,8 @@ public class ZenModeHelper { } /** - * Checks that the {@code origin} supplied to {@link #setAutomaticZenRuleState} overloads makes - * sense. + * Checks that the {@code origin} supplied to {@link #setAutomaticZenRuleState} or + * {@link #setAutomaticZenRuleStateFromConditionProvider} makes sense. */ private static void checkSetRuleStateOrigin(String method, @ConfigOrigin int origin) { if (!Flags.modesApi()) { diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index f15c23e110a4..c1ca9c23aef5 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -172,16 +172,6 @@ flag { } flag { - name: "notification_verify_channel_sound_uri" - namespace: "systemui" - description: "Verify Uri permission for sound when creating a notification channel" - bug: "337775777" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "notification_vibration_in_sound_uri_for_channel" namespace: "systemui" description: "Enables sound uri with vibration source in notification channel" @@ -196,4 +186,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "fix_calling_uid_from_cps" + namespace: "systemui" + description: "Correctly checks zen rule ownership when a CPS notifies with a Condition" + bug: "379722187" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/om/IdmapDaemon.java b/services/core/java/com/android/server/om/IdmapDaemon.java index 1b22154c10f6..d33c860343c5 100644 --- a/services/core/java/com/android/server/om/IdmapDaemon.java +++ b/services/core/java/com/android/server/om/IdmapDaemon.java @@ -28,6 +28,7 @@ import android.os.IBinder; import android.os.IIdmap2; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.StrictMode; import android.os.SystemClock; import android.os.SystemService; import android.text.TextUtils; @@ -40,7 +41,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; /** * To prevent idmap2d from continuously running, the idmap daemon will terminate after 10 seconds @@ -66,7 +66,7 @@ class IdmapDaemon { private static IdmapDaemon sInstance; private volatile IIdmap2 mService; - private final AtomicInteger mOpenedCount = new AtomicInteger(); + private int mOpenedCount = 0; private final Object mIdmapToken = new Object(); /** @@ -74,15 +74,20 @@ class IdmapDaemon { * finalized, the idmap service will be stopped after a period of time unless another connection * to the service is open. **/ - private class Connection implements AutoCloseable { + private final class Connection implements AutoCloseable { @Nullable private final IIdmap2 mIdmap2; private boolean mOpened = true; - private Connection(IIdmap2 idmap2) { + private Connection() { + mIdmap2 = null; + mOpened = false; + } + + private Connection(@NonNull IIdmap2 idmap2) { + mIdmap2 = idmap2; synchronized (mIdmapToken) { - mOpenedCount.incrementAndGet(); - mIdmap2 = idmap2; + ++mOpenedCount; } } @@ -94,20 +99,22 @@ class IdmapDaemon { } mOpened = false; - if (mOpenedCount.decrementAndGet() != 0) { + if (--mOpenedCount != 0) { // Only post the callback to stop the service if the service does not have an // open connection. return; } + final var service = mService; FgThread.getHandler().postDelayed(() -> { synchronized (mIdmapToken) { - // Only stop the service if the service does not have an open connection. - if (mService == null || mOpenedCount.get() != 0) { + // Only stop the service if it's the one we were scheduled for and + // it does not have an open connection. + if (mService != service || mOpenedCount != 0) { return; } - stopIdmapService(); + stopIdmapServiceLocked(); mService = null; } }, mIdmapToken, SERVICE_TIMEOUT_MS); @@ -175,6 +182,8 @@ class IdmapDaemon { } boolean idmapExists(String overlayPath, int userId) { + // The only way to verify an idmap is to read its state on disk. + final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); try (Connection c = connect()) { final IIdmap2 idmap2 = c.getIdmap2(); if (idmap2 == null) { @@ -187,6 +196,8 @@ class IdmapDaemon { } catch (Exception e) { Slog.wtf(TAG, "failed to check if idmap exists for " + overlayPath, e); return false; + } finally { + StrictMode.setThreadPolicy(oldPolicy); } } @@ -242,14 +253,16 @@ class IdmapDaemon { } catch (Exception e) { Slog.wtf(TAG, "failed to get all fabricated overlays", e); } finally { - try { - if (c.getIdmap2() != null && iteratorId != -1) { - c.getIdmap2().releaseFabricatedOverlayIterator(iteratorId); + if (c != null) { + try { + if (c.getIdmap2() != null && iteratorId != -1) { + c.getIdmap2().releaseFabricatedOverlayIterator(iteratorId); + } + } catch (RemoteException e) { + // ignore } - } catch (RemoteException e) { - // ignore + c.close(); } - c.close(); } return allInfos; } @@ -271,9 +284,11 @@ class IdmapDaemon { } @Nullable - private IBinder getIdmapService() throws TimeoutException, RemoteException { + private IBinder getIdmapServiceLocked() throws TimeoutException, RemoteException { try { - SystemService.start(IDMAP_DAEMON); + if (!SystemService.isRunning(IDMAP_DAEMON)) { + SystemService.start(IDMAP_DAEMON); + } } catch (RuntimeException e) { Slog.wtf(TAG, "Failed to enable idmap2 daemon", e); if (e.getMessage().contains("failed to set system property")) { @@ -306,9 +321,11 @@ class IdmapDaemon { walltimeMillis - endWalltimeMillis + SERVICE_CONNECT_WALLTIME_TIMEOUT_MS)); } - private static void stopIdmapService() { + private static void stopIdmapServiceLocked() { try { - SystemService.stop(IDMAP_DAEMON); + if (SystemService.isRunning(IDMAP_DAEMON)) { + SystemService.stop(IDMAP_DAEMON); + } } catch (RuntimeException e) { // If the idmap daemon cannot be disabled for some reason, it is okay // since we already finished invoking idmap. @@ -326,9 +343,9 @@ class IdmapDaemon { return new Connection(mService); } - IBinder binder = getIdmapService(); + IBinder binder = getIdmapServiceLocked(); if (binder == null) { - return new Connection(null); + return new Connection(); } mService = IIdmap2.Stub.asInterface(binder); diff --git a/services/core/java/com/android/server/om/OverlayActorEnforcer.java b/services/core/java/com/android/server/om/OverlayActorEnforcer.java index cc5c88b77293..d806770e5c91 100644 --- a/services/core/java/com/android/server/om/OverlayActorEnforcer.java +++ b/services/core/java/com/android/server/om/OverlayActorEnforcer.java @@ -19,7 +19,6 @@ package com.android.server.om; import android.annotation.NonNull; import android.content.om.OverlayInfo; import android.content.om.OverlayableInfo; -import android.content.res.Flags; import android.net.Uri; import android.os.Process; import android.text.TextUtils; @@ -163,15 +162,11 @@ public class OverlayActorEnforcer { return ActorState.UNABLE_TO_GET_TARGET_OVERLAYABLE; } - // Framework doesn't have <overlayable> declaration by design, and we still want to be able - // to enable its overlays from the packages with the permission. - if (targetOverlayable == null - && !(Flags.rroControlForAndroidNoOverlayable() && targetPackageName.equals( - "android"))) { + if (targetOverlayable == null) { return ActorState.MISSING_OVERLAYABLE; } - final String actor = targetOverlayable == null ? null : targetOverlayable.actor; + String actor = targetOverlayable.actor; if (TextUtils.isEmpty(actor)) { // If there's no actor defined, fallback to the legacy permission check try { diff --git a/services/core/java/com/android/server/om/OverlayReferenceMapper.java b/services/core/java/com/android/server/om/OverlayReferenceMapper.java index fdceabe74dd8..18de9952ed19 100644 --- a/services/core/java/com/android/server/om/OverlayReferenceMapper.java +++ b/services/core/java/com/android/server/om/OverlayReferenceMapper.java @@ -26,15 +26,13 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.CollectionUtils; import com.android.server.SystemConfig; import com.android.server.pm.pkg.AndroidPackage; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; /** @@ -121,20 +119,16 @@ public class OverlayReferenceMapper { return actorPair.first; } - @NonNull + @Nullable @Override - public Map<String, Set<String>> getTargetToOverlayables(@NonNull AndroidPackage pkg) { + public Pair<String, String> getTargetToOverlayables(@NonNull AndroidPackage pkg) { String target = pkg.getOverlayTarget(); if (TextUtils.isEmpty(target)) { - return Collections.emptyMap(); + return null; } String overlayable = pkg.getOverlayTargetOverlayableName(); - Map<String, Set<String>> targetToOverlayables = new HashMap<>(); - Set<String> overlayables = new HashSet<>(); - overlayables.add(overlayable); - targetToOverlayables.put(target, overlayables); - return targetToOverlayables; + return Pair.create(target, overlayable); } }; } @@ -174,7 +168,7 @@ public class OverlayReferenceMapper { } // TODO(b/135203078): Replace with isOverlay boolean flag check; fix test mocks - if (!mProvider.getTargetToOverlayables(pkg).isEmpty()) { + if (mProvider.getTargetToOverlayables(pkg) != null) { addOverlay(pkg, otherPkgs, changed); } @@ -245,20 +239,17 @@ public class OverlayReferenceMapper { String target = targetPkg.getPackageName(); removeTarget(target, changedPackages); - Map<String, String> overlayablesToActors = targetPkg.getOverlayables(); - for (String overlayable : overlayablesToActors.keySet()) { - String actor = overlayablesToActors.get(overlayable); + final Map<String, String> overlayablesToActors = targetPkg.getOverlayables(); + for (final var entry : overlayablesToActors.entrySet()) { + final String overlayable = entry.getKey(); + final String actor = entry.getValue(); addTargetToMap(actor, target, changedPackages); for (AndroidPackage overlayPkg : otherPkgs.values()) { - Map<String, Set<String>> targetToOverlayables = + var targetToOverlayables = mProvider.getTargetToOverlayables(overlayPkg); - Set<String> overlayables = targetToOverlayables.get(target); - if (CollectionUtils.isEmpty(overlayables)) { - continue; - } - - if (overlayables.contains(overlayable)) { + if (targetToOverlayables != null && targetToOverlayables.first.equals(target) + && Objects.equals(targetToOverlayables.second, overlayable)) { String overlay = overlayPkg.getPackageName(); addOverlayToMap(actor, target, overlay, changedPackages); } @@ -310,25 +301,22 @@ public class OverlayReferenceMapper { String overlay = overlayPkg.getPackageName(); removeOverlay(overlay, changedPackages); - Map<String, Set<String>> targetToOverlayables = + Pair<String, String> targetToOverlayables = mProvider.getTargetToOverlayables(overlayPkg); - for (Map.Entry<String, Set<String>> entry : targetToOverlayables.entrySet()) { - String target = entry.getKey(); - Set<String> overlayables = entry.getValue(); + if (targetToOverlayables != null) { + String target = targetToOverlayables.first; AndroidPackage targetPkg = otherPkgs.get(target); if (targetPkg == null) { - continue; + return; } - String targetPkgName = targetPkg.getPackageName(); Map<String, String> overlayableToActor = targetPkg.getOverlayables(); - for (String overlayable : overlayables) { - String actor = overlayableToActor.get(overlayable); - if (TextUtils.isEmpty(actor)) { - continue; - } - addOverlayToMap(actor, targetPkgName, overlay, changedPackages); + String overlayable = targetToOverlayables.second; + String actor = overlayableToActor.get(overlayable); + if (TextUtils.isEmpty(actor)) { + return; } + addOverlayToMap(actor, targetPkgName, overlay, changedPackages); } } } @@ -430,11 +418,11 @@ public class OverlayReferenceMapper { String getActorPkg(@NonNull String actor); /** - * Mock response of multiple overlay tags. + * Mock response of overlay tags. * * TODO(b/119899133): Replace with actual implementation; fix OverlayReferenceMapperTests */ - @NonNull - Map<String, Set<String>> getTargetToOverlayables(@NonNull AndroidPackage pkg); + @Nullable + Pair<String, String> getTargetToOverlayables(@NonNull AndroidPackage pkg); } } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 4c70d2347fb7..a0fbc008475c 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -125,7 +125,6 @@ import android.content.pm.SharedLibraryInfo; import android.content.pm.Signature; import android.content.pm.SigningDetails; import android.content.pm.VerifierInfo; -import android.content.pm.dex.DexMetadataHelper; import android.content.pm.parsing.result.ParseResult; import android.content.pm.parsing.result.ParseTypeImpl; import android.net.Uri; @@ -171,7 +170,6 @@ import com.android.internal.pm.pkg.component.ParsedIntentInfo; import com.android.internal.pm.pkg.component.ParsedPermission; import com.android.internal.pm.pkg.component.ParsedPermissionGroup; import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; -import com.android.internal.security.VerityUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; import com.android.server.EventLogTags; @@ -186,7 +184,6 @@ import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.pkg.SharedLibraryWrapper; import com.android.server.rollback.RollbackManagerInternal; -import com.android.server.security.FileIntegrityService; import com.android.server.utils.WatchedArrayMap; import com.android.server.utils.WatchedLongSparseArray; @@ -195,7 +192,6 @@ import dalvik.system.VMRuntime; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.security.DigestException; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -1165,11 +1161,8 @@ final class InstallPackageHelper { } try { doRenameLI(request, parsedPackage); - setUpFsVerity(parsedPackage); - } catch (Installer.InstallerException | IOException | DigestException - | NoSuchAlgorithmException | PrepareFailure e) { - request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP, - "Failed to set up verity: " + e); + } catch (PrepareFailure e) { + request.setError(e); return false; } @@ -2322,68 +2315,6 @@ final class InstallPackageHelper { } } - /** - * Set up fs-verity for the given package. For older devices that do not support fs-verity, - * this is a no-op. - */ - private void setUpFsVerity(AndroidPackage pkg) throws Installer.InstallerException, - PrepareFailure, IOException, DigestException, NoSuchAlgorithmException { - if (!PackageManagerServiceUtils.isApkVerityEnabled()) { - return; - } - - if (isIncrementalPath(pkg.getPath()) && IncrementalManager.getVersion() - < IncrementalManager.MIN_VERSION_TO_SUPPORT_FSVERITY) { - return; - } - - // Collect files we care for fs-verity setup. - ArrayMap<String, String> fsverityCandidates = new ArrayMap<>(); - fsverityCandidates.put(pkg.getBaseApkPath(), - VerityUtils.getFsveritySignatureFilePath(pkg.getBaseApkPath())); - - final String dmPath = DexMetadataHelper.buildDexMetadataPathForApk( - pkg.getBaseApkPath()); - if (new File(dmPath).exists()) { - fsverityCandidates.put(dmPath, VerityUtils.getFsveritySignatureFilePath(dmPath)); - } - - for (String path : pkg.getSplitCodePaths()) { - fsverityCandidates.put(path, VerityUtils.getFsveritySignatureFilePath(path)); - - final String splitDmPath = DexMetadataHelper.buildDexMetadataPathForApk(path); - if (new File(splitDmPath).exists()) { - fsverityCandidates.put(splitDmPath, - VerityUtils.getFsveritySignatureFilePath(splitDmPath)); - } - } - - var fis = FileIntegrityService.getService(); - for (Map.Entry<String, String> entry : fsverityCandidates.entrySet()) { - try { - final String filePath = entry.getKey(); - if (VerityUtils.hasFsverity(filePath)) { - continue; - } - - final String signaturePath = entry.getValue(); - if (new File(signaturePath).exists()) { - // If signature is provided, enable fs-verity first so that the file can be - // measured for signature check below. - VerityUtils.setUpFsverity(filePath); - - if (!fis.verifyPkcs7DetachedSignature(signaturePath, filePath)) { - throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE, - "fs-verity signature does not verify against a known key"); - } - } - } catch (IOException e) { - throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE, - "Failed to enable fs-verity: " + e); - } - } - } - private PackageFreezer freezePackageForInstall(String packageName, int userId, int installFlags, String killReason, int exitInfoReason, InstallRequest request) { if ((installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0) { @@ -3060,6 +2991,8 @@ final class InstallPackageHelper { } if (succeeded) { + Slog.i(TAG, "installation completed:" + packageName); + if (Flags.aslInApkAppMetadataSource() && pkgSetting.getAppMetadataSource() == APP_METADATA_SOURCE_APK) { if (!extractAppMetadataFromApk(request.getPkg(), diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index c6760431116e..1b41c3617a05 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -26,7 +26,6 @@ import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH; import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED; -import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE; import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR; import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK; @@ -824,8 +823,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private File mInheritedFilesBase; - @GuardedBy("mLock") - private boolean mVerityFoundForApks; /** * Both flags should be guarded with mLock whenever changes need to be in lockstep. @@ -864,7 +861,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } else { if (DexMetadataHelper.isDexMetadataFile(file)) return false; } - if (VerityUtils.isFsveritySignatureFile(file)) return false; if (ApkChecksums.isDigestOrDigestSignatureFile(file)) return false; return true; } @@ -3565,13 +3561,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { "Missing existing base package"); } - // Default to require only if existing base apk has fs-verity signature. - mVerityFoundForApks = PackageManagerServiceUtils.isApkVerityEnabled() - && params.mode == SessionParams.MODE_INHERIT_EXISTING - && VerityUtils.hasFsverity(pkgInfo.applicationInfo.getBaseCodePath()) - && (new File(VerityUtils.getFsveritySignatureFilePath( - pkgInfo.applicationInfo.getBaseCodePath()))).exists(); - final List<File> removedFiles = getRemovedFilesLocked(); final List<String> removeSplitList = new ArrayList<>(); if (!removedFiles.isEmpty()) { @@ -3972,24 +3961,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private void maybeStageFsveritySignatureLocked(File origFile, File targetFile, - boolean fsVerityRequired) throws PackageManagerException { - if (android.security.Flags.deprecateFsvSig()) { - return; - } - final File originalSignature = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (originalSignature.exists()) { - final File stagedSignature = new File( - VerityUtils.getFsveritySignatureFilePath(targetFile.getPath())); - stageFileLocked(originalSignature, stagedSignature); - } else if (fsVerityRequired) { - throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE, - "Missing corresponding fs-verity signature to " + origFile); - } - } - - @GuardedBy("mLock") private void maybeStageV4SignatureLocked(File origFile, File targetFile) throws PackageManagerException { final File originalSignature = new File(origFile.getPath() + V4Signature.EXT); @@ -4015,11 +3986,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { DexMetadataHelper.buildDexMetadataPathForApk(targetFile.getName())); stageFileLocked(dexMetadataFile, targetDexMetadataFile); - - // Also stage .dm.fsv_sig. .dm may be required to install with fs-verity signature on - // supported on older devices. - maybeStageFsveritySignatureLocked(dexMetadataFile, targetDexMetadataFile, - DexMetadataHelper.isFsVerityRequired()); } @FlaggedApi(com.android.art.flags.Flags.FLAG_ART_SERVICE_V3) @@ -4105,44 +4071,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private boolean isFsVerityRequiredForApk(File origFile, File targetFile) - throws PackageManagerException { - if (mVerityFoundForApks) { - return true; - } - - // We haven't seen .fsv_sig for any APKs. Treat it as not required until we see one. - final File originalSignature = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (!originalSignature.exists()) { - return false; - } - mVerityFoundForApks = true; - - // When a signature is found, also check any previous staged APKs since they also need to - // have fs-verity signature consistently. - for (File file : mResolvedStagedFiles) { - if (!file.getName().endsWith(".apk")) { - continue; - } - // Ignore the current targeting file. - if (targetFile.getName().equals(file.getName())) { - continue; - } - throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE, - "Previously staged apk is missing fs-verity signature"); - } - return true; - } - - @GuardedBy("mLock") private void resolveAndStageFileLocked(File origFile, File targetFile, String splitName, List<String> artManagedFilePaths) throws PackageManagerException { stageFileLocked(origFile, targetFile); - // Stage APK's fs-verity signature if present. - maybeStageFsveritySignatureLocked(origFile, targetFile, - isFsVerityRequiredForApk(origFile, targetFile)); // Stage APK's v4 signature if present, and fs-verity is supported. if (android.security.Flags.extendVbChainToUpdatedApk() && VerityUtils.isFsVeritySupported()) { @@ -4160,16 +4092,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private void maybeInheritFsveritySignatureLocked(File origFile) { - // Inherit the fsverity signature file if present. - final File fsveritySignatureFile = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (fsveritySignatureFile.exists()) { - mResolvedInheritedFiles.add(fsveritySignatureFile); - } - } - - @GuardedBy("mLock") private void maybeInheritV4SignatureLocked(File origFile) { // Inherit the v4 signature file if present. final File v4SignatureFile = new File(origFile.getPath() + V4Signature.EXT); @@ -4182,7 +4104,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private void inheritFileLocked(File origFile, List<String> artManagedFilePaths) { mResolvedInheritedFiles.add(origFile); - maybeInheritFsveritySignatureLocked(origFile); if (android.security.Flags.extendVbChainToUpdatedApk()) { maybeInheritV4SignatureLocked(origFile); } @@ -4193,13 +4114,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { artManagedFilePaths, origFile.getPath())) { File artManagedFile = new File(path); mResolvedInheritedFiles.add(artManagedFile); - maybeInheritFsveritySignatureLocked(artManagedFile); } } else { final File dexMetadataFile = DexMetadataHelper.findDexMetadataForFile(origFile); if (dexMetadataFile != null) { mResolvedInheritedFiles.add(dexMetadataFile); - maybeInheritFsveritySignatureLocked(dexMetadataFile); } } // Inherit the digests if present. diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 7af39f74d0d6..3e376b6958ec 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -520,22 +520,6 @@ public class PackageManagerServiceUtils { } } - /** Default is to not use fs-verity since it depends on kernel support. */ - private static final int FSVERITY_DISABLED = 0; - - /** Standard fs-verity. */ - private static final int FSVERITY_ENABLED = 2; - - /** Returns true if standard APK Verity is enabled. */ - static boolean isApkVerityEnabled() { - if (android.security.Flags.deprecateFsvSig()) { - return false; - } - return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R - || SystemProperties.getInt("ro.apk_verity.mode", FSVERITY_DISABLED) - == FSVERITY_ENABLED; - } - /** * Verifies that signatures match. * @returns {@code true} if the compat signatures were matched; otherwise, {@code false}. diff --git a/services/core/java/com/android/server/pm/ResilientAtomicFile.java b/services/core/java/com/android/server/pm/ResilientAtomicFile.java index 3aefc5a64926..473ed6136e9a 100644 --- a/services/core/java/com/android/server/pm/ResilientAtomicFile.java +++ b/services/core/java/com/android/server/pm/ResilientAtomicFile.java @@ -23,6 +23,7 @@ import android.os.ParcelFileDescriptor; import android.util.Log; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.security.FileIntegrity; import libcore.io.IoUtils; @@ -121,6 +122,11 @@ final class ResilientAtomicFile implements Closeable { } public void finishWrite(FileOutputStream str) throws IOException { + finishWrite(str, true /* doFsVerity */); + } + + @VisibleForTesting + public void finishWrite(FileOutputStream str, final boolean doFsVerity) throws IOException { if (mMainOutStream != str) { throw new IllegalStateException("Invalid incoming stream."); } @@ -145,13 +151,15 @@ final class ResilientAtomicFile implements Closeable { finalizeOutStream(reserveOutStream); } - // Protect both main and reserve using fs-verity. - try (ParcelFileDescriptor mainPfd = ParcelFileDescriptor.dup(mainInStream.getFD()); - ParcelFileDescriptor copyPfd = ParcelFileDescriptor.dup(reserveInStream.getFD())) { - FileIntegrity.setUpFsVerity(mainPfd); - FileIntegrity.setUpFsVerity(copyPfd); - } catch (IOException e) { - Slog.e(LOG_TAG, "Failed to verity-protect " + mDebugName, e); + if (doFsVerity) { + // Protect both main and reserve using fs-verity. + try (ParcelFileDescriptor mainPfd = ParcelFileDescriptor.dup(mainInStream.getFD()); + ParcelFileDescriptor copyPfd = ParcelFileDescriptor.dup(reserveInStream.getFD())) { + FileIntegrity.setUpFsVerity(mainPfd); + FileIntegrity.setUpFsVerity(copyPfd); + } catch (IOException e) { + Slog.e(LOG_TAG, "Failed to verity-protect " + mDebugName, e); + } } } catch (IOException e) { Slog.e(LOG_TAG, "Failed to write reserve copy " + mDebugName + ": " + mReserveCopy, e); diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java index 17d7a14d9129..e1b76222072e 100644 --- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java +++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java @@ -612,7 +612,7 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable final PackageSetting staticLibPkgSetting = mPm.getPackageSettingForMutation(sharedLibraryInfo.getPackageName()); if (staticLibPkgSetting == null) { - Slog.wtf(TAG, "Shared lib without setting: " + sharedLibraryInfo); + Slog.w(TAG, "Shared lib without setting: " + sharedLibraryInfo); continue; } for (int u = 0; u < installedUserCount; u++) { diff --git a/services/core/java/com/android/server/pm/ShortcutPackageItem.java b/services/core/java/com/android/server/pm/ShortcutPackageItem.java index 44789e4c4de2..027da4986ce6 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackageItem.java +++ b/services/core/java/com/android/server/pm/ShortcutPackageItem.java @@ -179,7 +179,7 @@ abstract class ShortcutPackageItem { itemOut.endDocument(); os.flush(); - file.finishWrite(os); + mShortcutUser.mService.injectFinishWrite(file, os); } catch (XmlPullParserException | IOException e) { Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e); file.failWrite(os); diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 2785da5cbdbd..373c1ed3c386 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -1008,7 +1008,7 @@ public class ShortcutService extends IShortcutService.Stub { out.endDocument(); // Close. - file.finishWrite(outs); + injectFinishWrite(file, outs); } catch (IOException e) { Slog.w(TAG, "Failed to write to file " + file.getBaseFile(), e); file.failWrite(outs); @@ -1096,7 +1096,7 @@ public class ShortcutService extends IShortcutService.Stub { saveUserInternalLocked(userId, os, /* forBackup= */ false); } - file.finishWrite(os); + injectFinishWrite(file, os); // Remove all dangling bitmap files. cleanupDanglingBitmapDirectoriesLocked(userId); @@ -5067,6 +5067,12 @@ public class ShortcutService extends IShortcutService.Stub { return Build.FINGERPRINT; } + // Injection point. + void injectFinishWrite(@NonNull final ResilientAtomicFile file, + @NonNull final FileOutputStream os) throws IOException { + file.finishWrite(os); + } + final void wtf(String message) { wtf(message, /* exception= */ null); } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 8249d65868cd..81956fbb55e6 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -6661,7 +6661,7 @@ public class UserManagerService extends IUserManager.Stub { + userId); } new Thread(() -> { - getActivityManagerInternal().onUserRemoved(userId); + getActivityManagerInternal().onUserRemoving(userId); removeUserState(userId); }).start(); } @@ -6701,6 +6701,7 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mUsersLock) { removeUserDataLU(userId); mIsUserManaged.delete(userId); + getActivityManagerInternal().onUserRemoved(userId); } synchronized (mUserStates) { mUserStates.delete(userId); diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 672eb4caf798..9d840d0c0d35 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -1681,8 +1681,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { // handle overflow if (attributionChainId < 0) { - attributionChainId = 0; sAttributionChainIds.set(0); + attributionChainId = sAttributionChainIds.incrementAndGet(); } return attributionChainId; } diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java index 7a5a14d8d3c2..b32943704dc4 100644 --- a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java +++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java @@ -293,8 +293,8 @@ public class PackageUserStateImpl extends WatchableImpl implements PackageUserSt if (mOverlayPaths == null && mSharedLibraryOverlayPaths == null) { return null; } - final OverlayPaths.Builder newPaths = new OverlayPaths.Builder(); - newPaths.addAll(mOverlayPaths); + final OverlayPaths.Builder newPaths = mOverlayPaths == null + ? new OverlayPaths.Builder() : new OverlayPaths.Builder(mOverlayPaths); if (mSharedLibraryOverlayPaths != null) { for (final OverlayPaths libOverlayPaths : mSharedLibraryOverlayPaths.values()) { newPaths.addAll(libOverlayPaths); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 5ab59657d4ce..7f511e1e2aa1 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -85,15 +85,16 @@ import static android.view.contentprotection.flags.Flags.createAccessibilityOver import static com.android.hardware.input.Flags.enableNew25q2Keycodes; import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures; +import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures; import static com.android.hardware.input.Flags.inputManagerLifecycleSupport; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.hardware.input.Flags.modifierShortcutDump; import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; import static com.android.server.GestureLauncherService.DOUBLE_POWER_TAP_COUNT_THRESHOLD; import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser; import static com.android.server.flags.Flags.newBugreportKeyboardShortcut; -import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_UNCOVERED; @@ -502,6 +503,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { private TalkbackShortcutController mTalkbackShortcutController; + private VoiceAccessShortcutController mVoiceAccessShortcutController; + private WindowWakeUpPolicy mWindowWakeUpPolicy; /** @@ -562,8 +565,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { volatile boolean mPowerKeyHandled; volatile boolean mBackKeyHandled; volatile boolean mEndCallKeyHandled; - volatile boolean mCameraGestureTriggered; - volatile boolean mCameraGestureTriggeredDuringGoingToSleep; + volatile boolean mPowerButtonLaunchGestureTriggered; + volatile boolean mPowerButtonLaunchGestureTriggeredDuringGoingToSleep; /** * {@code true} if the device is entering a low-power state; {@code false otherwise}. @@ -2265,6 +2268,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { return new TalkbackShortcutController(mContext); } + VoiceAccessShortcutController getVoiceAccessShortcutController() { + return new VoiceAccessShortcutController(mContext); + } + WindowWakeUpPolicy getWindowWakeUpPolicy() { return new WindowWakeUpPolicy(mContext); } @@ -2512,6 +2519,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { com.android.internal.R.integer.config_keyguardDrawnTimeout); mKeyguardDelegate = injector.getKeyguardServiceDelegate(); mTalkbackShortcutController = injector.getTalkbackShortcutController(); + mVoiceAccessShortcutController = injector.getVoiceAccessShortcutController(); mWindowWakeUpPolicy = injector.getWindowWakeUpPolicy(); initKeyCombinationRules(); initSingleKeyGestureRules(injector.getLooper()); @@ -4262,6 +4270,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { .isAccessibilityShortcutAvailable(false); case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK: return enableTalkbackAndMagnifierKeyGestures(); + case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: + return enableVoiceAccessKeyGestures(); default: return false; } @@ -4492,6 +4502,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } break; + case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: + if (enableVoiceAccessKeyGestures()) { + if (complete) { + mVoiceAccessShortcutController.toggleVoiceAccess(mCurrentUserId); + } + return true; + } + break; case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION: AppLaunchData data = event.getAppLaunchData(); if (complete && canLaunchApp && data != null @@ -5893,7 +5911,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mGestureLauncherService == null) { return false; } - mCameraGestureTriggered = false; + mPowerButtonLaunchGestureTriggered = false; final MutableBoolean outLaunched = new MutableBoolean(false); final boolean intercept = mGestureLauncherService.interceptPowerKeyDown(event, interactive, outLaunched); @@ -5903,9 +5921,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { // detector from processing the power key later on. return intercept; } - mCameraGestureTriggered = true; + mPowerButtonLaunchGestureTriggered = true; if (mRequestedOrSleepingDefaultDisplay) { - mCameraGestureTriggeredDuringGoingToSleep = true; + mPowerButtonLaunchGestureTriggeredDuringGoingToSleep = true; // Wake device up early to prevent display doing redundant turning off/on stuff. mWindowWakeUpPolicy.wakeUpFromPowerKeyCameraGesture(); } @@ -6282,13 +6300,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mKeyguardDelegate != null) { mKeyguardDelegate.onFinishedGoingToSleep(pmSleepReason, - mCameraGestureTriggeredDuringGoingToSleep); + mPowerButtonLaunchGestureTriggeredDuringGoingToSleep); } if (mDisplayFoldController != null) { mDisplayFoldController.finishedGoingToSleep(); } - mCameraGestureTriggeredDuringGoingToSleep = false; - mCameraGestureTriggered = false; + mPowerButtonLaunchGestureTriggeredDuringGoingToSleep = false; + mPowerButtonLaunchGestureTriggered = false; } // Called on the PowerManager's Notifier thread. @@ -6319,10 +6337,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { mDefaultDisplayRotation.updateOrientationListener(); if (mKeyguardDelegate != null) { - mKeyguardDelegate.onStartedWakingUp(pmWakeReason, mCameraGestureTriggered); + mKeyguardDelegate.onStartedWakingUp(pmWakeReason, mPowerButtonLaunchGestureTriggered); } - mCameraGestureTriggered = false; + mPowerButtonLaunchGestureTriggered = false; } // Called on the PowerManager's Notifier thread. diff --git a/services/core/java/com/android/server/policy/TalkbackShortcutController.java b/services/core/java/com/android/server/policy/TalkbackShortcutController.java index 9e16a7d5e83a..efda337527d4 100644 --- a/services/core/java/com/android/server/policy/TalkbackShortcutController.java +++ b/services/core/java/com/android/server/policy/TalkbackShortcutController.java @@ -18,20 +18,15 @@ package com.android.server.policy; import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE; -import android.accessibilityservice.AccessibilityServiceInfo; import android.content.ComponentName; import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; import android.os.UserHandle; import android.provider.Settings; -import android.view.accessibility.AccessibilityManager; import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; import com.android.internal.accessibility.util.AccessibilityUtils; import com.android.internal.annotations.VisibleForTesting; -import java.util.List; import java.util.Set; /** @@ -42,7 +37,6 @@ import java.util.Set; class TalkbackShortcutController { private static final String TALKBACK_LABEL = "TalkBack"; private final Context mContext; - private final PackageManager mPackageManager; public enum ShortcutSource { GESTURE, @@ -51,7 +45,6 @@ class TalkbackShortcutController { TalkbackShortcutController(Context context) { mContext = context; - mPackageManager = mContext.getPackageManager(); } /** @@ -63,7 +56,10 @@ class TalkbackShortcutController { boolean toggleTalkback(int userId, ShortcutSource source) { final Set<ComponentName> enabledServices = AccessibilityUtils.getEnabledServicesFromSettings(mContext, userId); - ComponentName componentName = getTalkbackComponent(); + ComponentName componentName = + AccessibilityUtils.getInstalledAccessibilityServiceComponentNameByLabel( + mContext, TALKBACK_LABEL); + ; if (componentName == null) { return false; } @@ -83,21 +79,6 @@ class TalkbackShortcutController { return isTalkbackAlreadyEnabled; } - private ComponentName getTalkbackComponent() { - AccessibilityManager accessibilityManager = mContext.getSystemService( - AccessibilityManager.class); - List<AccessibilityServiceInfo> serviceInfos = - accessibilityManager.getInstalledAccessibilityServiceList(); - - for (AccessibilityServiceInfo service : serviceInfos) { - final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; - if (isTalkback(serviceInfo)) { - return new ComponentName(serviceInfo.packageName, serviceInfo.name); - } - } - return null; - } - boolean isTalkBackShortcutGestureEnabled() { return Settings.System.getIntForUser(mContext.getContentResolver(), Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED, @@ -120,9 +101,4 @@ class TalkbackShortcutController { ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE, /* serviceEnabled= */ true); } - - private boolean isTalkback(ServiceInfo info) { - return TALKBACK_LABEL.equals(info.loadLabel(mPackageManager).toString()) - && (info.applicationInfo.isSystemApp() || info.applicationInfo.isUpdatedSystemApp()); - } } diff --git a/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java b/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java new file mode 100644 index 000000000000..a37fb1140e06 --- /dev/null +++ b/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.policy; + +import android.content.ComponentName; +import android.content.Context; +import android.util.Slog; + +import com.android.internal.accessibility.util.AccessibilityUtils; + +import androidx.annotation.VisibleForTesting; + +import java.util.Set; + +/** This class controls voice access shortcut related operations such as toggling, querying. */ +class VoiceAccessShortcutController { + private static final String TAG = VoiceAccessShortcutController.class.getSimpleName(); + private static final String VOICE_ACCESS_LABEL = "Voice Access"; + + private final Context mContext; + + VoiceAccessShortcutController(Context context) { + mContext = context; + } + + /** + * A function that toggles voice access service. + * + * @return whether voice access is enabled after being toggled. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + boolean toggleVoiceAccess(int userId) { + final Set<ComponentName> enabledServices = + AccessibilityUtils.getEnabledServicesFromSettings(mContext, userId); + ComponentName componentName = + AccessibilityUtils.getInstalledAccessibilityServiceComponentNameByLabel( + mContext, VOICE_ACCESS_LABEL); + if (componentName == null) { + Slog.e(TAG, "Toggle Voice Access failed due to componentName being null"); + return false; + } + + boolean newState = !enabledServices.contains(componentName); + AccessibilityUtils.setAccessibilityServiceState(mContext, componentName, newState, userId); + + return newState; + } +} diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java index da8b01ac86fb..587447b8af26 100644 --- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java +++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java @@ -198,7 +198,7 @@ public class KeyguardServiceDelegate { if (mKeyguardState.interactiveState == INTERACTIVE_STATE_AWAKE || mKeyguardState.interactiveState == INTERACTIVE_STATE_WAKING) { mKeyguardService.onStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN, - false /* cameraGestureTriggered */); + false /* powerButtonLaunchGestureTriggered */); } if (mKeyguardState.interactiveState == INTERACTIVE_STATE_AWAKE) { mKeyguardService.onFinishedWakingUp(); @@ -319,10 +319,10 @@ public class KeyguardServiceDelegate { } public void onStartedWakingUp( - @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) { + @PowerManager.WakeReason int pmWakeReason, boolean powerButtonLaunchGestureTriggered) { if (mKeyguardService != null) { if (DEBUG) Log.v(TAG, "onStartedWakingUp()"); - mKeyguardService.onStartedWakingUp(pmWakeReason, cameraGestureTriggered); + mKeyguardService.onStartedWakingUp(pmWakeReason, powerButtonLaunchGestureTriggered); } mKeyguardState.interactiveState = INTERACTIVE_STATE_WAKING; } @@ -383,9 +383,11 @@ public class KeyguardServiceDelegate { } public void onFinishedGoingToSleep( - @PowerManager.GoToSleepReason int pmSleepReason, boolean cameraGestureTriggered) { + @PowerManager.GoToSleepReason int pmSleepReason, + boolean powerButtonLaunchGestureTriggered) { if (mKeyguardService != null) { - mKeyguardService.onFinishedGoingToSleep(pmSleepReason, cameraGestureTriggered); + mKeyguardService.onFinishedGoingToSleep(pmSleepReason, + powerButtonLaunchGestureTriggered); } mKeyguardState.interactiveState = INTERACTIVE_STATE_SLEEP; } diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java index cd789eaed1b3..f2342e0d5688 100644 --- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java +++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java @@ -113,9 +113,10 @@ public class KeyguardServiceWrapper implements IKeyguardService { @Override public void onFinishedGoingToSleep( - @PowerManager.GoToSleepReason int pmSleepReason, boolean cameraGestureTriggered) { + @PowerManager.GoToSleepReason int pmSleepReason, + boolean powerButtonLaunchGestureTriggered) { try { - mService.onFinishedGoingToSleep(pmSleepReason, cameraGestureTriggered); + mService.onFinishedGoingToSleep(pmSleepReason, powerButtonLaunchGestureTriggered); } catch (RemoteException e) { Slog.w(TAG , "Remote Exception", e); } @@ -123,9 +124,9 @@ public class KeyguardServiceWrapper implements IKeyguardService { @Override public void onStartedWakingUp( - @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) { + @PowerManager.WakeReason int pmWakeReason, boolean powerButtonLaunchGestureTriggered) { try { - mService.onStartedWakingUp(pmWakeReason, cameraGestureTriggered); + mService.onStartedWakingUp(pmWakeReason, powerButtonLaunchGestureTriggered); } catch (RemoteException e) { Slog.w(TAG , "Remote Exception", e); } diff --git a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java index 7808c4ed50a4..e09ab600a1dc 100644 --- a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java +++ b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java @@ -28,7 +28,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; -import android.content.pm.Signature; import android.os.Environment; import android.permission.flags.Flags; import android.provider.Settings; @@ -312,17 +311,10 @@ public class RoleServicePlatformHelperImpl implements RoleServicePlatformHelper DataOutputStream dataOutputStream = new DataOutputStream(new BufferedOutputStream(mdos)); packageManagerInternal.forEachInstalledPackage(pkg -> { try { - dataOutputStream.writeUTF(pkg.getPackageName()); - dataOutputStream.writeLong(pkg.getLongVersionCode()); + dataOutputStream.writeUTF(pkg.getPath()); dataOutputStream.writeInt(packageManagerInternal.getApplicationEnabledState( pkg.getPackageName(), userId)); - final Set<String> requestedPermissions = pkg.getRequestedPermissions(); - dataOutputStream.writeInt(requestedPermissions.size()); - for (String permissionName : requestedPermissions) { - dataOutputStream.writeUTF(permissionName); - } - final ArraySet<String> enabledComponents = packageManagerInternal.getEnabledComponents(pkg.getPackageName(), userId); final int enabledComponentsSize = CollectionUtils.size(enabledComponents); @@ -337,10 +329,6 @@ public class RoleServicePlatformHelperImpl implements RoleServicePlatformHelper for (int i = 0; i < disabledComponentsSize; i++) { dataOutputStream.writeUTF(disabledComponents.valueAt(i)); } - - for (final Signature signature : pkg.getSigningDetails().getSignatures()) { - dataOutputStream.write(signature.toByteArray()); - } } catch (IOException e) { // Never happens for MessageDigestOutputStream and DataOutputStream. throw new AssertionError(e); diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java index 42dbb7974fe2..f46fa446a0ba 100644 --- a/services/core/java/com/android/server/power/ThermalManagerService.java +++ b/services/core/java/com/android/server/power/ThermalManagerService.java @@ -155,6 +155,9 @@ public class ThermalManagerService extends SystemService { @VisibleForTesting final TemperatureWatcher mTemperatureWatcher; + @VisibleForTesting + final AtomicBoolean mIsHalSkinForecastSupported = new AtomicBoolean(false); + private final ThermalHalWrapper.WrapperThermalChangedCallback mWrapperCallback = new ThermalHalWrapper.WrapperThermalChangedCallback() { @Override @@ -254,6 +257,18 @@ public class ThermalManagerService extends SystemService { } onTemperatureMapChangedLocked(); mTemperatureWatcher.getAndUpdateThresholds(); + // we only check forecast if a single SKIN sensor threshold is reported + synchronized (mTemperatureWatcher.mSamples) { + if (mTemperatureWatcher.mSevereThresholds.size() == 1) { + try { + mIsHalSkinForecastSupported.set( + Flags.allowThermalHalSkinForecast() + && !Float.isNaN(mHalWrapper.forecastSkinTemperature(10))); + } catch (UnsupportedOperationException e) { + Slog.i(TAG, "Thermal HAL does not support forecastSkinTemperature"); + } + } + } mHalReady.set(true); } } @@ -1092,6 +1107,8 @@ public class ThermalManagerService extends SystemService { protected abstract List<TemperatureThreshold> getTemperatureThresholds(boolean shouldFilter, int type); + protected abstract float forecastSkinTemperature(int forecastSeconds); + protected abstract boolean connectToHal(); protected abstract void dump(PrintWriter pw, String prefix); @@ -1124,8 +1141,16 @@ public class ThermalManagerService extends SystemService { @VisibleForTesting static class ThermalHalAidlWrapper extends ThermalHalWrapper implements IBinder.DeathRecipient { /* Proxy object for the Thermal HAL AIDL service. */ + + @GuardedBy("mHalLock") private IThermal mInstance = null; + private IThermal getHalInstance() { + synchronized (mHalLock) { + return mInstance; + } + } + /** Callback for Thermal HAL AIDL. */ private final IThermalChangedCallback mThermalCallbackAidl = new IThermalChangedCallback.Stub() { @@ -1169,154 +1194,183 @@ public class ThermalManagerService extends SystemService { @Override protected List<Temperature> getCurrentTemperatures(boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<Temperature> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<Temperature> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final android.hardware.thermal.Temperature[] halRet = + shouldFilter ? instance.getTemperaturesWithType(type) + : instance.getTemperatures(); + if (halRet == null) { return ret; } - try { - final android.hardware.thermal.Temperature[] halRet = - shouldFilter ? mInstance.getTemperaturesWithType(type) - : mInstance.getTemperatures(); - if (halRet == null) { - return ret; + for (android.hardware.thermal.Temperature t : halRet) { + if (!Temperature.isValidStatus(t.throttlingStatus)) { + Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus + + " received from AIDL HAL"); + t.throttlingStatus = Temperature.THROTTLING_NONE; } - for (android.hardware.thermal.Temperature t : halRet) { - if (!Temperature.isValidStatus(t.throttlingStatus)) { - Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus - + " received from AIDL HAL"); - t.throttlingStatus = Temperature.THROTTLING_NONE; - } - if (shouldFilter && t.type != type) { - continue; - } - ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus)); + if (shouldFilter && t.type != type) { + continue; } - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e); - connectToHal(); + ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus)); + } + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; } @Override protected List<CoolingDevice> getCurrentCoolingDevices(boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<CoolingDevice> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<CoolingDevice> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter + ? instance.getCoolingDevicesWithType(type) + : instance.getCoolingDevices(); + if (halRet == null) { return ret; } - try { - final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter - ? mInstance.getCoolingDevicesWithType(type) - : mInstance.getCoolingDevices(); - if (halRet == null) { - return ret; + for (android.hardware.thermal.CoolingDevice t : halRet) { + if (!CoolingDevice.isValidType(t.type)) { + Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL"); + continue; } - for (android.hardware.thermal.CoolingDevice t : halRet) { - if (!CoolingDevice.isValidType(t.type)) { - Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL"); - continue; - } - if (shouldFilter && t.type != type) { - continue; - } - ret.add(new CoolingDevice(t.value, t.type, t.name)); + if (shouldFilter && t.type != type) { + continue; } - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e); - connectToHal(); + ret.add(new CoolingDevice(t.value, t.type, t.name)); + } + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; } @Override @NonNull protected List<TemperatureThreshold> getTemperatureThresholds( boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<TemperatureThreshold> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<TemperatureThreshold> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final TemperatureThreshold[] halRet = + shouldFilter ? instance.getTemperatureThresholdsWithType(type) + : instance.getTemperatureThresholds(); + if (halRet == null) { return ret; } - try { - final TemperatureThreshold[] halRet = - shouldFilter ? mInstance.getTemperatureThresholdsWithType(type) - : mInstance.getTemperatureThresholds(); - if (halRet == null) { - return ret; - } - if (shouldFilter) { - return Arrays.stream(halRet).filter(t -> t.type == type).collect( - Collectors.toList()); - } - return Arrays.asList(halRet); - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e); - connectToHal(); + if (shouldFilter) { + return Arrays.stream(halRet).filter(t -> t.type == type).collect( + Collectors.toList()); + } + return Arrays.asList(halRet); + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; + } + + @Override + protected float forecastSkinTemperature(int forecastSeconds) { + final IThermal instance = getHalInstance(); + if (instance == null) { + return Float.NaN; + } + try { + return instance.forecastSkinTemperature(forecastSeconds); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't forecastSkinTemperature, reconnecting...", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); + } + } + return Float.NaN; } @Override protected boolean connectToHal() { synchronized (mHalLock) { - IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService( - IThermal.DESCRIPTOR + "/default")); - initProxyAndRegisterCallback(binder); + return connectToHalIfNeededLocked(mInstance); } + } + + @GuardedBy("mHalLock") + protected boolean connectToHalIfNeededLocked(IThermal instance) { + if (instance != mInstance) { + // instance has been updated since last used + return true; + } + IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService( + IThermal.DESCRIPTOR + "/default")); + initProxyAndRegisterCallbackLocked(binder); return mInstance != null; } @VisibleForTesting void initProxyAndRegisterCallback(IBinder binder) { synchronized (mHalLock) { - if (binder != null) { - mInstance = IThermal.Stub.asInterface(binder); + initProxyAndRegisterCallbackLocked(binder); + } + } + + @GuardedBy("mHalLock") + protected void initProxyAndRegisterCallbackLocked(IBinder binder) { + if (binder != null) { + mInstance = IThermal.Stub.asInterface(binder); + try { + binder.linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + connectToHal(); + } + if (mInstance != null) { try { - binder.linkToDeath(this, 0); + Slog.i(TAG, "Thermal HAL AIDL service connected with version " + + mInstance.getInterfaceVersion()); } catch (RemoteException e) { - Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + Slog.e(TAG, "Unable to read interface version from Thermal HAL", e); connectToHal(); + return; } - if (mInstance != null) { - try { - Slog.i(TAG, "Thermal HAL AIDL service connected with version " - + mInstance.getInterfaceVersion()); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to read interface version from Thermal HAL", e); - connectToHal(); - return; - } - registerThermalChangedCallback(); + try { + mInstance.registerThermalChangedCallback(mThermalCallbackAidl); + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status", + e); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + connectToHal(); } } } } - @VisibleForTesting - void registerThermalChangedCallback() { - try { - mInstance.registerThermalChangedCallback(mThermalCallbackAidl); - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status", - e); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); - connectToHal(); - } - } - @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { @@ -1445,6 +1499,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 1.0"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1583,6 +1642,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 1.1"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1749,6 +1813,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 2.0"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1977,6 +2046,39 @@ public class ThermalManagerService extends SystemService { float getForecast(int forecastSeconds) { synchronized (mSamples) { + // If we don't have any thresholds, we can't normalize the temperatures, + // so return early + if (mSevereThresholds.isEmpty()) { + Slog.e(TAG, "No temperature thresholds found"); + FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, + Binder.getCallingUid(), + THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD, + Float.NaN, forecastSeconds); + return Float.NaN; + } + } + if (mIsHalSkinForecastSupported.get()) { + float threshold = -1f; + synchronized (mSamples) { + // we only do forecast if a single SKIN sensor threshold is reported + if (mSevereThresholds.size() == 1) { + threshold = mSevereThresholds.valueAt(0); + } + } + if (threshold > 0) { + try { + final float forecastTemperature = + mHalWrapper.forecastSkinTemperature(forecastSeconds); + return normalizeTemperature(forecastTemperature, threshold); + } catch (UnsupportedOperationException e) { + Slog.wtf(TAG, "forecastSkinTemperature returns unsupported"); + } catch (Exception e) { + Slog.e(TAG, "forecastSkinTemperature fails"); + } + return Float.NaN; + } + } + synchronized (mSamples) { mLastForecastCallTimeMillis = SystemClock.elapsedRealtime(); if (mSamples.isEmpty()) { getAndUpdateTemperatureSamples(); @@ -1993,17 +2095,6 @@ public class ThermalManagerService extends SystemService { return Float.NaN; } - // If we don't have any thresholds, we can't normalize the temperatures, - // so return early - if (mSevereThresholds.isEmpty()) { - Slog.e(TAG, "No temperature thresholds found"); - FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, - Binder.getCallingUid(), - THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD, - Float.NaN, forecastSeconds); - return Float.NaN; - } - if (mCachedHeadrooms.contains(forecastSeconds)) { // TODO(b/360486877): replace with metrics Slog.d(TAG, diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index a6f2a3757dcb..1cf24fcd8594 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -20,7 +20,6 @@ import static android.os.Flags.adpfUseFmqChannel; import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR; import static com.android.server.power.hint.Flags.adpfSessionTag; -import static com.android.server.power.hint.Flags.cpuHeadroomAffinityCheck; import static com.android.server.power.hint.Flags.powerhintThreadCleanup; import static com.android.server.power.hint.Flags.resetOnForkEnabled; @@ -1604,8 +1603,7 @@ public final class HintManagerService extends SystemService { } } } - if (cpuHeadroomAffinityCheck() && mCheckHeadroomAffinity - && params.tids.length > 1) { + if (mCheckHeadroomAffinity && params.tids.length > 1) { checkThreadAffinityForTids(params.tids); } halParams.tids = params.tids; diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java index 14539d544bf9..50db1e4ac30e 100644 --- a/services/core/java/com/android/server/rollback/RollbackStore.java +++ b/services/core/java/com/android/server/rollback/RollbackStore.java @@ -84,8 +84,12 @@ class RollbackStore { */ private static List<Rollback> loadRollbacks(File rollbackDataDir) { List<Rollback> rollbacks = new ArrayList<>(); - rollbackDataDir.mkdirs(); - for (File rollbackDir : rollbackDataDir.listFiles()) { + File[] rollbackDirs = rollbackDataDir.listFiles(); + if (rollbackDirs == null) { + Slog.e(TAG, "Folder doesn't exist: " + rollbackDataDir); + return rollbacks; + } + for (File rollbackDir : rollbackDirs) { if (rollbackDir.isDirectory()) { try { rollbacks.add(loadRollback(rollbackDir)); diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java index d69150d88e4f..a1f72be7a039 100644 --- a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java @@ -15,7 +15,6 @@ */ package com.android.server.selinux; -import android.provider.DeviceConfig; import android.text.TextUtils; import android.util.Slog; @@ -34,10 +33,6 @@ class SelinuxAuditLogBuilder { private static final String TAG = "SelinuxAuditLogs"; - // This config indicates which Selinux logs for source domains to collect. The string will be - // inserted into a regex, so it must follow the regex syntax. For example, a valid value would - // be "system_server|untrusted_app". - @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain"; private static final Matcher NO_OP_MATCHER = Pattern.compile("no-op^").matcher(""); private static final String TCONTEXT_PATTERN = "u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*"; @@ -50,7 +45,7 @@ class SelinuxAuditLogBuilder { private Iterator<String> mTokens; private final SelinuxAuditLog mAuditLog = new SelinuxAuditLog(); - SelinuxAuditLogBuilder() { + SelinuxAuditLogBuilder(String auditDomain) { Matcher scontextMatcher = NO_OP_MATCHER; Matcher tcontextMatcher = NO_OP_MATCHER; Matcher pathMatcher = NO_OP_MATCHER; @@ -59,10 +54,7 @@ class SelinuxAuditLogBuilder { Pattern.compile( TextUtils.formatSimple( "u:r:(?<stype>%s):s0(:c)?(?<scategories>((,c)?\\d+)+)*", - DeviceConfig.getString( - DeviceConfig.NAMESPACE_ADSERVICES, - CONFIG_SELINUX_AUDIT_DOMAIN, - "no_match^"))) + auditDomain)) .matcher(""); tcontextMatcher = Pattern.compile(TCONTEXT_PATTERN).matcher(""); pathMatcher = Pattern.compile(PATH_PATTERN).matcher(""); diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java index c655d46eb9f4..0aa705892376 100644 --- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java @@ -15,6 +15,7 @@ */ package com.android.server.selinux; +import android.provider.DeviceConfig; import android.util.EventLog; import android.util.EventLog.Event; import android.util.Log; @@ -32,6 +33,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,9 +45,16 @@ class SelinuxAuditLogsCollector { private static final String SELINUX_PATTERN = "^.*\\bavc:\\s+(?<denial>.*)$"; + // This config indicates which Selinux logs for source domains to collect. The string will be + // inserted into a regex, so it must follow the regex syntax. For example, a valid value would + // be "system_server|untrusted_app". + @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain"; + @VisibleForTesting static final String DEFAULT_SELINUX_AUDIT_DOMAIN = "no_match^"; + @VisibleForTesting static final Matcher SELINUX_MATCHER = Pattern.compile(SELINUX_PATTERN).matcher(""); + private final Supplier<String> mAuditDomainSupplier; private final RateLimiter mRateLimiter; private final QuotaLimiter mQuotaLimiter; @@ -53,11 +62,26 @@ class SelinuxAuditLogsCollector { AtomicBoolean mStopRequested = new AtomicBoolean(false); - SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { + SelinuxAuditLogsCollector( + Supplier<String> auditDomainSupplier, + RateLimiter rateLimiter, + QuotaLimiter quotaLimiter) { + mAuditDomainSupplier = auditDomainSupplier; mRateLimiter = rateLimiter; mQuotaLimiter = quotaLimiter; } + SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { + this( + () -> + DeviceConfig.getString( + DeviceConfig.NAMESPACE_ADSERVICES, + CONFIG_SELINUX_AUDIT_DOMAIN, + DEFAULT_SELINUX_AUDIT_DOMAIN), + rateLimiter, + quotaLimiter); + } + public void setStopRequested(boolean stopRequested) { mStopRequested.set(stopRequested); } @@ -108,7 +132,8 @@ class SelinuxAuditLogsCollector { } private boolean writeAuditLogs(Queue<Event> logLines) { - final SelinuxAuditLogBuilder auditLogBuilder = new SelinuxAuditLogBuilder(); + final SelinuxAuditLogBuilder auditLogBuilder = + new SelinuxAuditLogBuilder(mAuditDomainSupplier.get()); int auditsWritten = 0; while (!mStopRequested.get() && !logLines.isEmpty()) { diff --git a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java index 2088e411f842..383135233049 100644 --- a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java +++ b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java @@ -142,11 +142,8 @@ class AggregatedMobileDataStatsPuller { private final RateLimiter mRateLimiter; AggregatedMobileDataStatsPuller(@NonNull NetworkStatsManager networkStatsManager) { - if (DEBUG) { - if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { - Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, - TAG + "-AggregatedMobileDataStatsPullerInit"); - } + if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-Init"); } mRateLimiter = new RateLimiter(/* window= */ Duration.ofSeconds(1)); @@ -173,10 +170,16 @@ class AggregatedMobileDataStatsPuller { public void noteUidProcessState(int uid, int state, long unusedElapsedRealtime, long unusedUptime) { - mMobileDataStatsHandler.post( + if (mRateLimiter.tryAcquire()) { + mMobileDataStatsHandler.post( () -> { noteUidProcessStateImpl(uid, state); }); + } else { + synchronized (mLock) { + mUidPreviousState.put(uid, state); + } + } } public int pullDataBytesTransfer(List<StatsEvent> data) { @@ -209,29 +212,27 @@ class AggregatedMobileDataStatsPuller { } private void noteUidProcessStateImpl(int uid, int state) { - if (mRateLimiter.tryAcquire()) { - // noteUidProcessStateImpl can be called back to back several times while - // the updateNetworkStats loops over several stats for multiple uids - // and during the first call in a batch of proc state change event it can - // contain info for uid with unknown previous state yet which can happen due to a few - // reasons: - // - app was just started - // - app was started before the ActivityManagerService - // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN - if (mNetworkStatsManager != null) { - updateNetworkStats(mNetworkStatsManager); - } else { - Slog.w(TAG, "noteUidProcessStateLocked() can not get mNetworkStatsManager"); - } + // noteUidProcessStateImpl can be called back to back several times while + // the updateNetworkStats loops over several stats for multiple uids + // and during the first call in a batch of proc state change event it can + // contain info for uid with unknown previous state yet which can happen due to a few + // reasons: + // - app was just started + // - app was started before the ActivityManagerService + // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN + if (mNetworkStatsManager != null) { + updateNetworkStats(mNetworkStatsManager); + } else { + Slog.w(TAG, "noteUidProcessStateLocked() can not get mNetworkStatsManager"); + } + synchronized (mLock) { + mUidPreviousState.put(uid, state); } - mUidPreviousState.put(uid, state); } private void updateNetworkStats(NetworkStatsManager networkStatsManager) { - if (DEBUG) { - if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { - Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStats"); - } + if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStats"); } final NetworkStats latestStats = networkStatsManager.getMobileUidStats(); @@ -256,20 +257,25 @@ class AggregatedMobileDataStatsPuller { } private void updateNetworkStatsDelta(NetworkStats delta) { + if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStatsDelta"); + } synchronized (mLock) { for (NetworkStats.Entry entry : delta) { - if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) { - continue; - } - MobileDataStats stats = getUidStatsForPreviousStateLocked(entry.getUid()); - if (stats != null) { - stats.addTxBytes(entry.getTxBytes()); - stats.addRxBytes(entry.getRxBytes()); - stats.addTxPackets(entry.getTxPackets()); - stats.addRxPackets(entry.getRxPackets()); + if (entry.getRxPackets() != 0 || entry.getTxPackets() != 0) { + MobileDataStats stats = getUidStatsForPreviousStateLocked(entry.getUid()); + if (stats != null) { + stats.addTxBytes(entry.getTxBytes()); + stats.addRxBytes(entry.getRxBytes()); + stats.addTxPackets(entry.getTxPackets()); + stats.addRxPackets(entry.getRxPackets()); + } } } } + if (DEBUG) { + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + } } @GuardedBy("mLock") @@ -298,18 +304,12 @@ class AggregatedMobileDataStatsPuller { } private static boolean isEmpty(NetworkStats stats) { - long totalRxPackets = 0; - long totalTxPackets = 0; for (NetworkStats.Entry entry : stats) { - if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) { - continue; + if (entry.getRxPackets() != 0 || entry.getTxPackets() != 0) { + // at least one non empty entry located + return false; } - totalRxPackets += entry.getRxPackets(); - totalTxPackets += entry.getTxPackets(); - // at least one non empty entry located - break; } - final long totalPackets = totalRxPackets + totalTxPackets; - return totalPackets == 0; + return true; } } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 4ed5f90f2852..a19a3422af06 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -2199,6 +2199,19 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D }); } + /** + * Called when the notification should be rebundled. + * @param key the notification key + */ + @Override + public void rebundleNotification(String key) { + enforceStatusBarService(); + enforceValidCallingUser(); + Binder.withCleanCallingIdentity(() -> { + mNotificationDelegate.rebundleNotification(key); + }); + } + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, diff --git a/services/core/java/com/android/server/timedetector/ServerFlags.java b/services/core/java/com/android/server/timedetector/ServerFlags.java index 2049a0288f5a..b651c7ba34c3 100644 --- a/services/core/java/com/android/server/timedetector/ServerFlags.java +++ b/services/core/java/com/android/server/timedetector/ServerFlags.java @@ -72,8 +72,12 @@ public final class ServerFlags { KEY_TIME_ZONE_DETECTOR_AUTO_DETECTION_ENABLED_DEFAULT, KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED, KEY_ENHANCED_METRICS_COLLECTION_ENABLED, + KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED, + KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT, + KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED, + KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED }) - @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @Retention(RetentionPolicy.SOURCE) @interface DeviceConfigKey {} @@ -192,6 +196,31 @@ public final class ServerFlags { "enhanced_metrics_collection_enabled"; /** + * The key to control support for time zone notifications under certain circumstances. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED = + "time_zone_notifications_supported"; + + /** + * The key for the default value used to determine whether time zone notifications is enabled + * when the user hasn't explicitly set it yet. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT = + "time_zone_notifications_enabled_default"; + + /** + * The key to control support for time zone notifications tracking under certain circumstances. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED = + "time_zone_notifications_tracking_supported"; + + /** + * The key to control support for time zone manual change tracking under certain circumstances. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED = + "time_zone_manual_change_tracking_supported"; + + /** * The registered listeners and the keys to trigger on. The value is explicitly a HashSet to * ensure O(1) lookup performance when working out whether a listener should trigger. */ diff --git a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java index 3579246b660f..0495f54cb154 100644 --- a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java +++ b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java @@ -286,7 +286,8 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { // This check is racey, but the whole settings update process is racey. This check prevents // a ConfigurationChangeListener callback triggering due to ContentObserver's still // triggering *sometimes* for no-op updates. Because callbacks are async this is necessary - // for stable behavior during tests. + // for stable behavior during tests. This behavior is copied from + // setAutoDetectionEnabledIfRequired and assumed to be the correct way. if (getAutoDetectionEnabledSetting() != enabled) { Settings.Global.putInt(mCr, Settings.Global.AUTO_TIME, enabled ? 1 : 0); } diff --git a/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java index fc659c5cb627..c4c86a429dd6 100644 --- a/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java +++ b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java @@ -65,6 +65,10 @@ public final class ConfigurationInternal { private final boolean mUserConfigAllowed; private final boolean mLocationEnabledSetting; private final boolean mGeoDetectionEnabledSetting; + private final boolean mNotificationsSupported; + private final boolean mNotificationsEnabledSetting; + private final boolean mNotificationTrackingSupported; + private final boolean mManualChangeTrackingSupported; private ConfigurationInternal(Builder builder) { mTelephonyDetectionSupported = builder.mTelephonyDetectionSupported; @@ -78,6 +82,10 @@ public final class ConfigurationInternal { mUserConfigAllowed = builder.mUserConfigAllowed; mLocationEnabledSetting = builder.mLocationEnabledSetting; mGeoDetectionEnabledSetting = builder.mGeoDetectionEnabledSetting; + mNotificationsSupported = builder.mNotificationsSupported; + mNotificationsEnabledSetting = builder.mNotificationsEnabledSetting; + mNotificationTrackingSupported = builder.mNotificationsTrackingSupported; + mManualChangeTrackingSupported = builder.mManualChangeTrackingSupported; } /** Returns true if the device supports any form of auto time zone detection. */ @@ -104,6 +112,27 @@ public final class ConfigurationInternal { } /** + * Returns true if the device supports time-related notifications. + */ + public boolean areNotificationsSupported() { + return mNotificationsSupported; + } + + /** + * Returns true if the device supports tracking of time-related notifications. + */ + public boolean isNotificationTrackingSupported() { + return areNotificationsSupported() && mNotificationTrackingSupported; + } + + /** + * Returns true if the device supports tracking of time zone manual changes. + */ + public boolean isManualChangeTrackingSupported() { + return mManualChangeTrackingSupported; + } + + /** * Returns {@code true} if location time zone detection should run when auto time zone detection * is enabled on supported devices, even when the user has not enabled the algorithm explicitly * in settings. Enabled for internal testing only. See {@link #isGeoDetectionExecutionEnabled()} @@ -223,6 +252,15 @@ public final class ConfigurationInternal { && getGeoDetectionRunInBackgroundEnabledSetting(); } + /** Returns true if time-related notifications can be shown on this device. */ + public boolean getNotificationsEnabledBehavior() { + return areNotificationsSupported() && getNotificationsEnabledSetting(); + } + + private boolean getNotificationsEnabledSetting() { + return mNotificationsEnabledSetting; + } + @NonNull public TimeZoneCapabilities asCapabilities(boolean bypassUserPolicyChecks) { UserHandle userHandle = UserHandle.of(mUserId); @@ -283,6 +321,14 @@ public final class ConfigurationInternal { } builder.setSetManualTimeZoneCapability(suggestManualTimeZoneCapability); + final @CapabilityState int configureNotificationsEnabledCapability; + if (areNotificationsSupported()) { + configureNotificationsEnabledCapability = CAPABILITY_POSSESSED; + } else { + configureNotificationsEnabledCapability = CAPABILITY_NOT_SUPPORTED; + } + builder.setConfigureNotificationsEnabledCapability(configureNotificationsEnabledCapability); + return builder.build(); } @@ -291,6 +337,7 @@ public final class ConfigurationInternal { return new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(getAutoDetectionEnabledSetting()) .setGeoDetectionEnabled(getGeoDetectionEnabledSetting()) + .setNotificationsEnabled(getNotificationsEnabledSetting()) .build(); } @@ -307,6 +354,9 @@ public final class ConfigurationInternal { if (newConfiguration.hasIsGeoDetectionEnabled()) { builder.setGeoDetectionEnabledSetting(newConfiguration.isGeoDetectionEnabled()); } + if (newConfiguration.hasIsNotificationsEnabled()) { + builder.setNotificationsEnabledSetting(newConfiguration.areNotificationsEnabled()); + } return builder.build(); } @@ -328,7 +378,11 @@ public final class ConfigurationInternal { && mEnhancedMetricsCollectionEnabled == that.mEnhancedMetricsCollectionEnabled && mAutoDetectionEnabledSetting == that.mAutoDetectionEnabledSetting && mLocationEnabledSetting == that.mLocationEnabledSetting - && mGeoDetectionEnabledSetting == that.mGeoDetectionEnabledSetting; + && mGeoDetectionEnabledSetting == that.mGeoDetectionEnabledSetting + && mNotificationsSupported == that.mNotificationsSupported + && mNotificationsEnabledSetting == that.mNotificationsEnabledSetting + && mNotificationTrackingSupported == that.mNotificationTrackingSupported + && mManualChangeTrackingSupported == that.mManualChangeTrackingSupported; } @Override @@ -336,7 +390,9 @@ public final class ConfigurationInternal { return Objects.hash(mUserId, mUserConfigAllowed, mTelephonyDetectionSupported, mGeoDetectionSupported, mTelephonyFallbackSupported, mGeoDetectionRunInBackgroundEnabled, mEnhancedMetricsCollectionEnabled, - mAutoDetectionEnabledSetting, mLocationEnabledSetting, mGeoDetectionEnabledSetting); + mAutoDetectionEnabledSetting, mLocationEnabledSetting, mGeoDetectionEnabledSetting, + mNotificationsSupported, mNotificationsEnabledSetting, + mNotificationTrackingSupported, mManualChangeTrackingSupported); } @Override @@ -352,6 +408,10 @@ public final class ConfigurationInternal { + ", mAutoDetectionEnabledSetting=" + mAutoDetectionEnabledSetting + ", mLocationEnabledSetting=" + mLocationEnabledSetting + ", mGeoDetectionEnabledSetting=" + mGeoDetectionEnabledSetting + + ", mNotificationsSupported=" + mNotificationsSupported + + ", mNotificationsEnabledSetting=" + mNotificationsEnabledSetting + + ", mNotificationTrackingSupported=" + mNotificationTrackingSupported + + ", mManualChangeTrackingSupported=" + mManualChangeTrackingSupported + '}'; } @@ -370,6 +430,10 @@ public final class ConfigurationInternal { private boolean mAutoDetectionEnabledSetting; private boolean mLocationEnabledSetting; private boolean mGeoDetectionEnabledSetting; + private boolean mNotificationsSupported; + private boolean mNotificationsEnabledSetting; + private boolean mNotificationsTrackingSupported; + private boolean mManualChangeTrackingSupported; /** * Creates a new Builder. @@ -390,6 +454,10 @@ public final class ConfigurationInternal { this.mAutoDetectionEnabledSetting = toCopy.mAutoDetectionEnabledSetting; this.mLocationEnabledSetting = toCopy.mLocationEnabledSetting; this.mGeoDetectionEnabledSetting = toCopy.mGeoDetectionEnabledSetting; + this.mNotificationsSupported = toCopy.mNotificationsSupported; + this.mNotificationsEnabledSetting = toCopy.mNotificationsEnabledSetting; + this.mNotificationsTrackingSupported = toCopy.mNotificationTrackingSupported; + this.mManualChangeTrackingSupported = toCopy.mManualChangeTrackingSupported; } /** @@ -475,6 +543,38 @@ public final class ConfigurationInternal { return this; } + /** + * Sets the value of the time notification setting for this user. + */ + public Builder setNotificationsEnabledSetting(boolean enabled) { + mNotificationsEnabledSetting = enabled; + return this; + } + + /** + * Sets whether time zone notifications are supported on this device. + */ + public Builder setNotificationsSupported(boolean enabled) { + mNotificationsSupported = enabled; + return this; + } + + /** + * Sets whether time zone notification tracking is supported on this device. + */ + public Builder setNotificationsTrackingSupported(boolean supported) { + mNotificationsTrackingSupported = supported; + return this; + } + + /** + * Sets whether time zone manual change tracking are supported on this device. + */ + public Builder setManualChangeTrackingSupported(boolean supported) { + mManualChangeTrackingSupported = supported; + return this; + } + /** Returns a new {@link ConfigurationInternal}. */ @NonNull public ConfigurationInternal build() { diff --git a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java index f1248a3f5f0e..d809fc6b6eea 100644 --- a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java +++ b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java @@ -68,7 +68,11 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT, ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE, ServerFlags.KEY_TIME_ZONE_DETECTOR_AUTO_DETECTION_ENABLED_DEFAULT, - ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED + ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED, + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED, + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT, + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED, + ServerFlags.KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED ); /** @@ -100,11 +104,16 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { @Nullable private static ServiceConfigAccessor sInstance; - @NonNull private final Context mContext; - @NonNull private final ServerFlags mServerFlags; - @NonNull private final ContentResolver mCr; - @NonNull private final UserManager mUserManager; - @NonNull private final LocationManager mLocationManager; + @NonNull + private final Context mContext; + @NonNull + private final ServerFlags mServerFlags; + @NonNull + private final ContentResolver mCr; + @NonNull + private final UserManager mUserManager; + @NonNull + private final LocationManager mLocationManager; @GuardedBy("this") @NonNull @@ -193,6 +202,9 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { contentResolver.registerContentObserver( Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE_EXPLICIT), true, contentObserver); + contentResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.TIME_ZONE_NOTIFICATIONS), true, + contentObserver); // Add async callbacks for user scoped location settings being changed. contentResolver.registerContentObserver( @@ -331,6 +343,14 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { setGeoDetectionEnabledSettingIfRequired(userId, geoDetectionEnabledSetting); } } + + if (areNotificationsSupported()) { + if (requestedConfigurationUpdates.hasIsNotificationsEnabled()) { + setNotificationsEnabledSetting( + requestedConfigurationUpdates.areNotificationsEnabled()); + } + setNotificationsEnabledIfRequired(newConfiguration.areNotificationsEnabled()); + } } @Override @@ -348,6 +368,10 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { .setUserConfigAllowed(isUserConfigAllowed(userId)) .setLocationEnabledSetting(getLocationEnabledSetting(userId)) .setGeoDetectionEnabledSetting(getGeoDetectionEnabledSetting(userId)) + .setNotificationsSupported(areNotificationsSupported()) + .setNotificationsEnabledSetting(getNotificationsEnabledSetting()) + .setNotificationsTrackingSupported(isNotificationTrackingSupported()) + .setManualChangeTrackingSupported(isManualChangeTrackingSupported()) .build(); } @@ -421,6 +445,49 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { } } + private boolean areNotificationsSupported() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED, + getConfigBoolean(R.bool.config_enableTimeZoneNotificationsSupported)); + } + + private boolean isNotificationTrackingSupported() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED, + getConfigBoolean(R.bool.config_enableTimeZoneNotificationsTrackingSupported)); + } + + private boolean isManualChangeTrackingSupported() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED, + getConfigBoolean(R.bool.config_enableTimeZoneManualChangeTrackingSupported)); + } + + private boolean getNotificationsEnabledSetting() { + final boolean notificationsEnabledByDefault = areNotificationsEnabledByDefault(); + return Settings.Global.getInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS, + (notificationsEnabledByDefault ? 1 : 0) /* defaultValue */) != 0; + } + + private boolean areNotificationsEnabledByDefault() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT, true); + } + + private void setNotificationsEnabledSetting(boolean enabled) { + Settings.Global.putInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS, enabled ? 1 : 0); + } + + private void setNotificationsEnabledIfRequired(boolean enabled) { + // This check is racey, but the whole settings update process is racey. This check prevents + // a ConfigurationChangeListener callback triggering due to ContentObserver's still + // triggering *sometimes* for no-op updates. Because callbacks are async this is necessary + // for stable behavior during tests. + if (getNotificationsEnabledSetting() != enabled) { + Settings.Global.putInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS, enabled ? 1 : 0); + } + } + @Override public void addLocationTimeZoneManagerConfigListener( @NonNull StateChangeListener listener) { @@ -441,8 +508,7 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { @Override public boolean isGeoTimeZoneDetectionFeatureSupportedInConfig() { - return mContext.getResources().getBoolean( - com.android.internal.R.bool.config_enableGeolocationTimeZoneDetection); + return getConfigBoolean(R.bool.config_enableGeolocationTimeZoneDetection); } @Override @@ -660,8 +726,7 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { private boolean isTelephonyFallbackSupported() { return mServerFlags.getBoolean( ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED, - getConfigBoolean( - com.android.internal.R.bool.config_supportTelephonyTimeZoneFallback)); + getConfigBoolean(R.bool.config_supportTelephonyTimeZoneFallback)); } private boolean getConfigBoolean(int providerEnabledConfigId) { diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java new file mode 100644 index 000000000000..e14326cc2d53 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.ElapsedRealtimeLong; +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.util.IndentingPrintWriter; + +import com.android.server.SystemTimeZone.TimeZoneConfidence; +import com.android.server.timezonedetector.TimeZoneDetectorStrategy.Origin; + +import java.util.Objects; + +public interface TimeZoneChangeListener { + + /** Record a time zone change. */ + void process(TimeZoneChangeEvent event); + + /** Dump internal state. */ + void dump(IndentingPrintWriter ipw); + + class TimeZoneChangeEvent { + + private final @ElapsedRealtimeLong long mElapsedRealtimeMillis; + private final @CurrentTimeMillisLong long mUnixEpochTimeMillis; + private final @Origin int mOrigin; + private final @UserIdInt int mUserId; + private final String mOldZoneId; + private final String mNewZoneId; + private final @TimeZoneConfidence int mNewConfidence; + private final String mCause; + + public TimeZoneChangeEvent(@ElapsedRealtimeLong long elapsedRealtimeMillis, + @CurrentTimeMillisLong long unixEpochTimeMillis, + @Origin int origin, @UserIdInt int userId, @NonNull String oldZoneId, + @NonNull String newZoneId, int newConfidence, @NonNull String cause) { + mElapsedRealtimeMillis = elapsedRealtimeMillis; + mUnixEpochTimeMillis = unixEpochTimeMillis; + mOrigin = origin; + mUserId = userId; + mOldZoneId = Objects.requireNonNull(oldZoneId); + mNewZoneId = Objects.requireNonNull(newZoneId); + mNewConfidence = newConfidence; + mCause = Objects.requireNonNull(cause); + } + + public @ElapsedRealtimeLong long getElapsedRealtimeMillis() { + return mElapsedRealtimeMillis; + } + + public @CurrentTimeMillisLong long getUnixEpochTimeMillis() { + return mUnixEpochTimeMillis; + } + + public @Origin int getOrigin() { + return mOrigin; + } + + /** + * The ID of the user that triggered the change. + * + * <p>If automatic time zone is turned on, the user ID returned is the system's user id. + */ + public @UserIdInt int getUserId() { + return mUserId; + } + + public String getOldZoneId() { + return mOldZoneId; + } + + public String getNewZoneId() { + return mNewZoneId; + } + + @Override + public String toString() { + return "TimeZoneChangeEvent{" + + "mElapsedRealtimeMillis=" + mElapsedRealtimeMillis + + ", mUnixEpochTimeMillis=" + mUnixEpochTimeMillis + + ", mOrigin=" + mOrigin + + ", mUserId=" + mUserId + + ", mOldZoneId='" + mOldZoneId + '\'' + + ", mNewZoneId='" + mNewZoneId + '\'' + + ", mNewConfidence=" + mNewConfidence + + ", mCause='" + mCause + '\'' + + '}'; + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java index d914544566ff..af02ad88ad6a 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java @@ -18,6 +18,7 @@ package com.android.server.timezonedetector; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.time.ITimeZoneDetectorListener; import android.app.time.TimeZoneCapabilitiesAndConfig; @@ -73,6 +74,7 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub } @Override + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") public void onStart() { // Obtain / create the shared dependencies. Context context = getContext(); @@ -81,7 +83,7 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub ServiceConfigAccessor serviceConfigAccessor = ServiceConfigAccessorImpl.getInstance(context); TimeZoneDetectorStrategy timeZoneDetectorStrategy = - TimeZoneDetectorStrategyImpl.create(handler, serviceConfigAccessor); + TimeZoneDetectorStrategyImpl.create(context, handler, serviceConfigAccessor); DeviceActivityMonitor deviceActivityMonitor = DeviceActivityMonitorImpl.create(context, handler); diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java index 37e67c921634..8cfbe9daa970 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java @@ -15,6 +15,7 @@ */ package com.android.server.timezonedetector; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.time.TimeZoneCapabilitiesAndConfig; @@ -24,6 +25,11 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.util.IndentingPrintWriter; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + /** * The interface for the class that is responsible for setting the time zone on a device, used by * {@link TimeZoneDetectorService} and {@link TimeZoneDetectorInternal}. @@ -97,6 +103,22 @@ import android.util.IndentingPrintWriter; * @hide */ public interface TimeZoneDetectorStrategy extends Dumpable { + @IntDef({ ORIGIN_UNKNOWN, ORIGIN_MANUAL, ORIGIN_TELEPHONY, ORIGIN_LOCATION }) + @Retention(RetentionPolicy.SOURCE) + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + @interface Origin {} + + /** Used when the origin of the time zone value cannot be inferred. */ + @Origin int ORIGIN_UNKNOWN = 0; + + /** Used when a time zone value originated from a user / manual settings. */ + @Origin int ORIGIN_MANUAL = 1; + + /** Used when a time zone value originated from a telephony signal. */ + @Origin int ORIGIN_TELEPHONY = 2; + + /** Used when a time zone value originated from a location signal. */ + @Origin int ORIGIN_LOCATION = 3; /** * Adds a listener that will be triggered when something changes that could affect the result diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java index dddb46f80724..19a28ddcdaeb 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java @@ -28,6 +28,7 @@ import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW; import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.time.DetectorStatusTypes; import android.app.time.LocationTimeZoneAlgorithmStatus; @@ -39,8 +40,10 @@ import android.app.time.TimeZoneDetectorStatus; import android.app.time.TimeZoneState; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; +import android.content.Context; import android.os.Handler; import android.os.TimestampedValue; +import android.os.UserHandle; import android.util.IndentingPrintWriter; import android.util.Slog; @@ -72,12 +75,14 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * Returns the device's currently configured time zone. May return an empty string. */ - @NonNull String getDeviceTimeZone(); + @NonNull + String getDeviceTimeZone(); /** * Returns the confidence of the device's current time zone. */ - @TimeZoneConfidence int getDeviceTimeZoneConfidence(); + @TimeZoneConfidence + int getDeviceTimeZoneConfidence(); /** * Sets the device's time zone, associated confidence, and records a debug log entry. @@ -115,7 +120,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for an empty or invalid telephony suggestion. * - * Used to score telephony suggestions where there is no zone. + * <p>Used to score telephony suggestions where there is no zone. */ @VisibleForTesting public static final int TELEPHONY_SCORE_NONE = 0; @@ -123,11 +128,11 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a low quality telephony suggestion. * - * Used to score suggestions where: - * The suggested zone ID is one of several possibilities, and the possibilities have different - * offsets. + * <p>Used to score suggestions where: + * The suggested zone ID is one of several possibilities, + * and the possibilities have different offsets. * - * You would have to be quite desperate to want to use this choice. + * <p>You would have to be quite desperate to want to use this choice. */ @VisibleForTesting public static final int TELEPHONY_SCORE_LOW = 1; @@ -135,7 +140,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a medium quality telephony suggestion. * - * Used for: + * <p>Used for: * The suggested zone ID is one of several possibilities but at least the possibilities have the * same offset. Users would get the correct time but for the wrong reason. i.e. their device may * switch to DST at the wrong time and (for example) their calendar events. @@ -146,7 +151,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a high quality telephony suggestion. * - * Used for: + * <p>Used for: * The suggestion was for one zone ID and the answer was unambiguous and likely correct given * the info available. */ @@ -156,7 +161,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a highest quality telephony suggestion. * - * Used for: + * <p>Used for: * Suggestions that must "win" because they constitute test or emulator zone ID. */ @VisibleForTesting @@ -206,7 +211,8 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat private final ServiceConfigAccessor mServiceConfigAccessor; @GuardedBy("this") - @NonNull private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>(); + @NonNull + private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>(); /** * A snapshot of the current detector status. A local copy is cached because it is relatively @@ -244,8 +250,10 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * Creates a new instance of {@link TimeZoneDetectorStrategyImpl}. */ + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") public static TimeZoneDetectorStrategyImpl create( - @NonNull Handler handler, @NonNull ServiceConfigAccessor serviceConfigAccessor) { + @NonNull Context context, @NonNull Handler handler, + @NonNull ServiceConfigAccessor serviceConfigAccessor) { Environment environment = new EnvironmentImpl(handler); return new TimeZoneDetectorStrategyImpl(serviceConfigAccessor, environment); @@ -468,7 +476,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // later disables automatic time zone detection. mLatestManualSuggestion.set(suggestion); - setDeviceTimeZoneIfRequired(timeZoneId, cause); + setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_MANUAL, userId, cause); return true; } @@ -685,7 +693,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // GeolocationTimeZoneSuggestion has no measure of quality. We assume all suggestions are // reliable. - String zoneId; + String timeZoneId; // Introduce bias towards the device's current zone when there are multiple zone suggested. String deviceTimeZone = mEnvironment.getDeviceTimeZone(); @@ -694,11 +702,12 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat Slog.d(LOG_TAG, "Geo tz suggestion contains current device time zone. Applying bias."); } - zoneId = deviceTimeZone; + timeZoneId = deviceTimeZone; } else { - zoneId = zoneIds.get(0); + timeZoneId = zoneIds.get(0); } - setDeviceTimeZoneIfRequired(zoneId, detectionReason); + setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_LOCATION, UserHandle.USER_SYSTEM, + detectionReason); return true; } @@ -779,8 +788,8 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // Paranoia: Every suggestion above the SCORE_USAGE_THRESHOLD should have a non-null time // zone ID. - String zoneId = bestTelephonySuggestion.suggestion.getZoneId(); - if (zoneId == null) { + String timeZoneId = bestTelephonySuggestion.suggestion.getZoneId(); + if (timeZoneId == null) { Slog.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:" + " bestTelephonySuggestion=" + bestTelephonySuggestion + ", detectionReason=" + detectionReason); @@ -790,11 +799,12 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat String cause = "Found good suggestion:" + " bestTelephonySuggestion=" + bestTelephonySuggestion + ", detectionReason=" + detectionReason; - setDeviceTimeZoneIfRequired(zoneId, cause); + setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_TELEPHONY, UserHandle.USER_SYSTEM, cause); } @GuardedBy("this") - private void setDeviceTimeZoneIfRequired(@NonNull String newZoneId, @NonNull String cause) { + private void setDeviceTimeZoneIfRequired(@NonNull String newZoneId, @Origin int origin, + @UserIdInt int userId, @NonNull String cause) { String currentZoneId = mEnvironment.getDeviceTimeZone(); // All manual and automatic suggestions are considered high confidence as low-quality // suggestions are not currently passed on. diff --git a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java index 54ae047a2858..0b676ff7d590 100644 --- a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java +++ b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java @@ -100,6 +100,11 @@ final class BasicToPwleSegmentAdapter implements VibrationSegmentsAdapter { } VibratorInfo.FrequencyProfile frequencyProfile = info.getFrequencyProfile(); + if (frequencyProfile.isEmpty()) { + // The frequency profile has an invalid frequency range, so keep the segments unchanged. + return repeatIndex; + } + float[] frequenciesHz = frequencyProfile.getFrequenciesHz(); float[] accelerationsGs = frequencyProfile.getOutputAccelerationsGs(); diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 89c7a3d89a54..6f308aa9b706 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -1631,7 +1631,7 @@ class ActivityMetricsLogger { int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION; if (isAppCompateStateChangedToLetterboxed(state)) { - positionToLog = activity.mAppCompatController.getAppCompatReachabilityOverrides() + positionToLog = activity.mAppCompatController.getReachabilityOverrides() .getLetterboxPositionForLogging(); } FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED, diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 093df8c5075c..1fe61590a531 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -232,6 +232,7 @@ import static com.android.server.wm.IdentifierProto.USER_ID; import static com.android.server.wm.StartingData.AFTER_TRANSACTION_COPY_TO_CLIENT; import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE; import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY; +import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; @@ -2814,9 +2815,27 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A attachStartingSurfaceToAssociatedTask(); } + /** + * If the device is locked and the app does not request showWhenLocked, + * defer removing the starting window until the transition is complete. + * This prevents briefly appearing the app context and causing secure concern. + */ + void deferStartingWindowRemovalForKeyguardUnoccluding() { + if (mStartingData.mRemoveAfterTransaction != AFTER_TRANSITION_FINISH + && isKeyguardLocked() && !canShowWhenLockedInner(this) && !isVisibleRequested() + && mTransitionController.inTransition(this)) { + mStartingData.mRemoveAfterTransaction = AFTER_TRANSITION_FINISH; + } + } + void removeStartingWindow() { boolean prevEligibleForLetterboxEducation = isEligibleForLetterboxEducation(); + if (mStartingData != null + && mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) { + return; + } + if (transferSplashScreenIfNeeded()) { return; } @@ -3209,7 +3228,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A true /* forActivity */)) { return false; } - if (mAppCompatController.mAllowRestrictedResizability.getAsBoolean()) { + if (mAppCompatController.getResizeOverrides().allowRestrictedResizability()) { return false; } // If the user preference respects aspect ratio, then it becomes non-resizable. @@ -3240,8 +3259,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // The caller will check both application and activity level property. return true; } - return !AppCompatController.allowRestrictedResizability(wms.mContext.getPackageManager(), - appInfo.packageName); + return !AppCompatResizeOverrides.allowRestrictedResizability( + wms.mContext.getPackageManager(), appInfo.packageName); } boolean isResizeable() { @@ -4261,7 +4280,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } void finishRelaunching() { - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .setRelaunchingAfterRequestedOrientationChanged(false); mTaskSupervisor.getActivityMetricsLogger().notifyActivityRelaunched(this); @@ -4655,6 +4674,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A tStartingWindow.mToken = this; tStartingWindow.mActivityRecord = this; + if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) { + mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE; + } if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSACTION_REMOVE_DIRECTLY) { // The removal of starting window should wait for window drawn of current // activity. @@ -8125,10 +8147,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (task != null && requestedOrientation == SCREEN_ORIENTATION_BEHIND) { // We use Task here because we want to be consistent with what happens in // multi-window mode where other tasks orientations are ignored. - final ActivityRecord belowCandidate = task.getActivity( - a -> a.canDefineOrientationForActivitiesAbove() /* callback */, - this /* boundary */, false /* includeBoundary */, - true /* traverseTopToBottom */); + final ActivityRecord belowCandidate = task.getActivityBelowForDefiningOrientation(this); if (belowCandidate != null) { return belowCandidate.getRequestedConfigurationOrientation(forDisplay); } @@ -8222,7 +8241,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mLastReportedConfiguration.getMergedConfiguration())) { ensureActivityConfiguration(false /* ignoreVisibility */); if (mPendingRelaunchCount > originalRelaunchingCount) { - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .setRelaunchingAfterRequestedOrientationChanged(true); } if (mTransitionController.inPlayingTransition(this)) { @@ -8435,8 +8454,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ @ActivityInfo.SizeChangesSupportMode private int supportsSizeChanges() { - if (mAppCompatController.getAppCompatResizeOverrides() - .shouldOverrideForceNonResizeApp()) { + final AppCompatResizeOverrides resizeOverrides = mAppCompatController.getResizeOverrides(); + if (resizeOverrides.shouldOverrideForceNonResizeApp()) { return SIZE_CHANGES_UNSUPPORTED_OVERRIDE; } @@ -8444,8 +8463,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return SIZE_CHANGES_SUPPORTED_METADATA; } - if (mAppCompatController.getAppCompatResizeOverrides() - .shouldOverrideForceResizeApp()) { + if (resizeOverrides.shouldOverrideForceResizeApp()) { return SIZE_CHANGES_SUPPORTED_OVERRIDE; } @@ -8745,7 +8763,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A navBarInsets = Insets.NONE; } final AppCompatReachabilityOverrides reachabilityOverrides = - mAppCompatController.getAppCompatReachabilityOverrides(); + mAppCompatController.getReachabilityOverrides(); // Horizontal position int offsetX = 0; if (parentBounds.width() != screenResolvedBoundsWidth) { @@ -10218,10 +10236,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mAppCompatController.getAppCompatAspectRatioOverrides() .shouldOverrideMinAspectRatio()); proto.write(SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP, - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .shouldIgnoreOrientationRequestLoop()); proto.write(SHOULD_OVERRIDE_FORCE_RESIZE_APP, - mAppCompatController.getAppCompatResizeOverrides().shouldOverrideForceResizeApp()); + mAppCompatController.getResizeOverrides().shouldOverrideForceResizeApp()); proto.write(SHOULD_ENABLE_USER_ASPECT_RATIO_SETTINGS, mAppCompatController.getAppCompatAspectRatioOverrides() .shouldEnableUserAspectRatioSettings()); diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index 4433d64f0d00..6d0e8eacd438 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -15,23 +15,17 @@ */ package com.android.server.wm; -import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY; - import android.annotation.NonNull; import android.content.pm.PackageManager; import com.android.server.wm.utils.OptPropFactory; import java.io.PrintWriter; -import java.util.function.BooleanSupplier; /** * Allows the interaction with all the app compat policies and configurations */ class AppCompatController { - - @NonNull - private final ActivityRecord mActivityRecord; @NonNull private final TransparentPolicy mTransparentPolicy; @NonNull @@ -39,7 +33,7 @@ class AppCompatController { @NonNull private final AppCompatAspectRatioPolicy mAppCompatAspectRatioPolicy; @NonNull - private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy; + private final AppCompatReachabilityPolicy mReachabilityPolicy; @NonNull private final DesktopAppCompatAspectRatioPolicy mDesktopAppCompatAspectRatioPolicy; @NonNull @@ -50,56 +44,28 @@ class AppCompatController { private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy; @NonNull private final AppCompatSizeCompatModePolicy mAppCompatSizeCompatModePolicy; - @NonNull - final BooleanSupplier mAllowRestrictedResizability; AppCompatController(@NonNull WindowManagerService wmService, @NonNull ActivityRecord activityRecord) { - mActivityRecord = activityRecord; final PackageManager packageManager = wmService.mContext.getPackageManager(); final OptPropFactory optPropBuilder = new OptPropFactory(packageManager, activityRecord.packageName); mAppCompatDeviceStateQuery = new AppCompatDeviceStateQuery(activityRecord); mTransparentPolicy = new TransparentPolicy(activityRecord, wmService.mAppCompatConfiguration); - mAppCompatOverrides = new AppCompatOverrides(activityRecord, + mAppCompatOverrides = new AppCompatOverrides(activityRecord, packageManager, wmService.mAppCompatConfiguration, optPropBuilder, mAppCompatDeviceStateQuery); mOrientationPolicy = new AppCompatOrientationPolicy(activityRecord, mAppCompatOverrides); mAppCompatAspectRatioPolicy = new AppCompatAspectRatioPolicy(activityRecord, mTransparentPolicy, mAppCompatOverrides); - mAppCompatReachabilityPolicy = new AppCompatReachabilityPolicy(mActivityRecord, + mReachabilityPolicy = new AppCompatReachabilityPolicy(activityRecord, wmService.mAppCompatConfiguration); - mAppCompatLetterboxPolicy = new AppCompatLetterboxPolicy(mActivityRecord, + mAppCompatLetterboxPolicy = new AppCompatLetterboxPolicy(activityRecord, wmService.mAppCompatConfiguration); mDesktopAppCompatAspectRatioPolicy = new DesktopAppCompatAspectRatioPolicy(activityRecord, mAppCompatOverrides, mTransparentPolicy, wmService.mAppCompatConfiguration); - mAppCompatSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(mActivityRecord, + mAppCompatSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord, mAppCompatOverrides); - mAllowRestrictedResizability = AppCompatUtils.asLazy(() -> { - // Application level. - if (allowRestrictedResizability(packageManager, mActivityRecord.packageName)) { - return true; - } - // Activity level. - try { - return packageManager.getPropertyAsUser( - PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY, - mActivityRecord.mActivityComponent.getPackageName(), - mActivityRecord.mActivityComponent.getClassName(), - mActivityRecord.mUserId).getBoolean(); - } catch (PackageManager.NameNotFoundException e) { - return false; - } - }); - } - - static boolean allowRestrictedResizability(PackageManager pm, String packageName) { - try { - return pm.getProperty(PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY, packageName) - .getBoolean(); - } catch (PackageManager.NameNotFoundException e) { - return false; - } } @NonNull @@ -123,8 +89,8 @@ class AppCompatController { } @NonNull - AppCompatOrientationOverrides getAppCompatOrientationOverrides() { - return mAppCompatOverrides.getAppCompatOrientationOverrides(); + AppCompatOrientationOverrides getOrientationOverrides() { + return mAppCompatOverrides.getOrientationOverrides(); } @NonNull @@ -138,13 +104,13 @@ class AppCompatController { } @NonNull - AppCompatResizeOverrides getAppCompatResizeOverrides() { - return mAppCompatOverrides.getAppCompatResizeOverrides(); + AppCompatResizeOverrides getResizeOverrides() { + return mAppCompatOverrides.getResizeOverrides(); } @NonNull - AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() { - return mAppCompatReachabilityPolicy; + AppCompatReachabilityPolicy getReachabilityPolicy() { + return mReachabilityPolicy; } @NonNull @@ -158,8 +124,8 @@ class AppCompatController { } @NonNull - AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return mAppCompatOverrides.getAppCompatReachabilityOverrides(); + AppCompatReachabilityOverrides getReachabilityOverrides() { + return mAppCompatOverrides.getReachabilityOverrides(); } @NonNull diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java index e929fb414340..449458665b63 100644 --- a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java @@ -154,7 +154,7 @@ class AppCompatLetterboxPolicy { @VisibleForTesting boolean shouldShowLetterboxUi(@NonNull WindowState mainWindow) { - if (mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides() + if (mActivityRecord.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged()) { return mLastShouldShowLetterboxUi; } @@ -205,7 +205,7 @@ class AppCompatLetterboxPolicy { } pw.println(prefix + " letterboxReason=" + AppCompatUtils.getLetterboxReasonString(mActivityRecord, mainWin)); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy().dump(pw, prefix); + mActivityRecord.mAppCompatController.getReachabilityPolicy().dump(pw, prefix); final AppCompatLetterboxOverrides letterboxOverride = mActivityRecord.mAppCompatController .getAppCompatLetterboxOverrides(); pw.println(prefix + " letterboxBackgroundColor=" + Integer.toHexString( @@ -276,12 +276,12 @@ class AppCompatLetterboxPolicy { final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord .mAppCompatController.getAppCompatLetterboxOverrides(); final AppCompatReachabilityPolicy reachabilityPolicy = mActivityRecord - .mAppCompatController.getAppCompatReachabilityPolicy(); + .mAppCompatController.getReachabilityPolicy(); mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null), mActivityRecord.mWmService.mTransactionFactory, reachabilityPolicy, letterboxOverrides, this::getLetterboxParentSurface); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(mLetterbox::getInnerFrame); } final Point letterboxPosition = new Point(); @@ -291,7 +291,7 @@ class AppCompatLetterboxPolicy { final Rect innerFrame = new Rect(); calculateLetterboxInnerBounds(mActivityRecord, w, innerFrame); mLetterbox.layout(spaceToFill, innerFrame, letterboxPosition); - if (mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides() + if (mActivityRecord.mAppCompatController.getReachabilityOverrides() .isDoubleTapEvent()) { // We need to notify Shell that letterbox position has changed. mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); @@ -321,7 +321,7 @@ class AppCompatLetterboxPolicy { mLetterbox.destroy(); mLetterbox = null; } - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(null); } @@ -415,7 +415,7 @@ class AppCompatLetterboxPolicy { calculateLetterboxPosition(mActivityRecord, mLetterboxPosition); calculateLetterboxOuterBounds(mActivityRecord, mOuterBounds); calculateLetterboxInnerBounds(mActivityRecord, w, mInnerBounds); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(() -> mInnerBounds); } @@ -438,7 +438,7 @@ class AppCompatLetterboxPolicy { mLetterboxPosition.set(0, 0); mInnerBounds.setEmpty(); mOuterBounds.setEmpty(); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(null); } diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java index c84711d4be51..af83668f1188 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java @@ -113,7 +113,7 @@ class AppCompatOrientationOverrides { // Task to ensure that Activity Embedding is excluded. return mActivityRecord.isVisibleRequested() && mActivityRecord.getTaskFragment() != null && mActivityRecord.getTaskFragment().getWindowingMode() == WINDOWING_MODE_FULLSCREEN - && mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides() + && mActivityRecord.mAppCompatController.getOrientationOverrides() .isOverrideRespectRequestedOrientationEnabled(); } diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java index 16e20297dcf3..fc758ef90995 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java @@ -94,7 +94,7 @@ class AppCompatOrientationPolicy { return SCREEN_ORIENTATION_PORTRAIT; } - if (mAppCompatOverrides.getAppCompatOrientationOverrides() + if (mAppCompatOverrides.getOrientationOverrides() .isAllowOrientationOverrideOptOut()) { return candidate; } @@ -108,7 +108,7 @@ class AppCompatOrientationPolicy { } final AppCompatOrientationOverrides.OrientationOverridesState capabilityState = - mAppCompatOverrides.getAppCompatOrientationOverrides() + mAppCompatOverrides.getOrientationOverrides() .mOrientationOverridesState; if (capabilityState.mIsOverrideToReverseLandscapeOrientationEnabled @@ -170,7 +170,7 @@ class AppCompatOrientationPolicy { boolean shouldIgnoreRequestedOrientation( @ActivityInfo.ScreenOrientation int requestedOrientation) { final AppCompatOrientationOverrides orientationOverrides = - mAppCompatOverrides.getAppCompatOrientationOverrides(); + mAppCompatOverrides.getOrientationOverrides(); if (orientationOverrides.shouldEnableIgnoreOrientationRequest()) { if (orientationOverrides.getIsRelaunchingAfterRequestedOrientationChanged()) { Slog.w(TAG, "Ignoring orientation update to " diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java index 2f03105846bd..9fb54db23d55 100644 --- a/services/core/java/com/android/server/wm/AppCompatOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java @@ -17,6 +17,7 @@ package com.android.server.wm; import android.annotation.NonNull; +import android.content.pm.PackageManager; import com.android.server.wm.utils.OptPropFactory; @@ -26,7 +27,7 @@ import com.android.server.wm.utils.OptPropFactory; public class AppCompatOverrides { @NonNull - private final AppCompatOrientationOverrides mAppCompatOrientationOverrides; + private final AppCompatOrientationOverrides mOrientationOverrides; @NonNull private final AppCompatCameraOverrides mAppCompatCameraOverrides; @NonNull @@ -34,35 +35,37 @@ public class AppCompatOverrides { @NonNull private final AppCompatFocusOverrides mAppCompatFocusOverrides; @NonNull - private final AppCompatResizeOverrides mAppCompatResizeOverrides; + private final AppCompatResizeOverrides mResizeOverrides; @NonNull - private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides; + private final AppCompatReachabilityOverrides mReachabilityOverrides; @NonNull private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides; AppCompatOverrides(@NonNull ActivityRecord activityRecord, + @NonNull PackageManager packageManager, @NonNull AppCompatConfiguration appCompatConfiguration, @NonNull OptPropFactory optPropBuilder, @NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery) { mAppCompatCameraOverrides = new AppCompatCameraOverrides(activityRecord, appCompatConfiguration, optPropBuilder); - mAppCompatOrientationOverrides = new AppCompatOrientationOverrides(activityRecord, + mOrientationOverrides = new AppCompatOrientationOverrides(activityRecord, appCompatConfiguration, optPropBuilder, mAppCompatCameraOverrides); - mAppCompatReachabilityOverrides = new AppCompatReachabilityOverrides(activityRecord, + mReachabilityOverrides = new AppCompatReachabilityOverrides(activityRecord, appCompatConfiguration, appCompatDeviceStateQuery); mAppCompatAspectRatioOverrides = new AppCompatAspectRatioOverrides(activityRecord, appCompatConfiguration, optPropBuilder, appCompatDeviceStateQuery, - mAppCompatReachabilityOverrides); + mReachabilityOverrides); mAppCompatFocusOverrides = new AppCompatFocusOverrides(activityRecord, appCompatConfiguration, optPropBuilder); - mAppCompatResizeOverrides = new AppCompatResizeOverrides(activityRecord, optPropBuilder); + mResizeOverrides = new AppCompatResizeOverrides(activityRecord, packageManager, + optPropBuilder); mAppCompatLetterboxOverrides = new AppCompatLetterboxOverrides(activityRecord, appCompatConfiguration); } @NonNull - AppCompatOrientationOverrides getAppCompatOrientationOverrides() { - return mAppCompatOrientationOverrides; + AppCompatOrientationOverrides getOrientationOverrides() { + return mOrientationOverrides; } @NonNull @@ -81,13 +84,13 @@ public class AppCompatOverrides { } @NonNull - AppCompatResizeOverrides getAppCompatResizeOverrides() { - return mAppCompatResizeOverrides; + AppCompatResizeOverrides getResizeOverrides() { + return mResizeOverrides; } @NonNull - AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return mAppCompatReachabilityOverrides; + AppCompatReachabilityOverrides getReachabilityOverrides() { + return mReachabilityOverrides; } @NonNull diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java index d03a80387657..087edc184b6f 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java @@ -77,7 +77,7 @@ class AppCompatReachabilityPolicy { void dump(@NonNull PrintWriter pw, @NonNull String prefix) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivityRecord.mAppCompatController.getReachabilityOverrides(); pw.println(prefix + " isVerticalThinLetterboxed=" + reachabilityOverrides .isVerticalThinLetterboxed()); pw.println(prefix + " isHorizontalThinLetterboxed=" + reachabilityOverrides @@ -96,7 +96,7 @@ class AppCompatReachabilityPolicy { private void handleHorizontalDoubleTap(int x) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivityRecord.mAppCompatController.getReachabilityOverrides(); if (!reachabilityOverrides.isHorizontalReachabilityEnabled() || mActivityRecord.isInTransition()) { return; @@ -142,7 +142,7 @@ class AppCompatReachabilityPolicy { private void handleVerticalDoubleTap(int y) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivityRecord.mAppCompatController.getReachabilityOverrides(); if (!reachabilityOverrides.isVerticalReachabilityEnabled() || mActivityRecord.isInTransition()) { return; diff --git a/services/core/java/com/android/server/wm/AppCompatResizeOverrides.java b/services/core/java/com/android/server/wm/AppCompatResizeOverrides.java index 60c18254eca7..fa53153dd143 100644 --- a/services/core/java/com/android/server/wm/AppCompatResizeOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatResizeOverrides.java @@ -19,13 +19,17 @@ package com.android.server.wm; import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP; import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY; import static com.android.server.wm.AppCompatUtils.isChangeEnabled; import android.annotation.NonNull; +import android.content.pm.PackageManager; import com.android.server.wm.utils.OptPropFactory; +import java.util.function.BooleanSupplier; + /** * Encapsulate app compat logic about resizability. */ @@ -37,11 +41,40 @@ class AppCompatResizeOverrides { @NonNull private final OptPropFactory.OptProp mAllowForceResizeOverrideOptProp; + @NonNull + private final BooleanSupplier mAllowRestrictedResizability; + AppCompatResizeOverrides(@NonNull ActivityRecord activityRecord, + @NonNull PackageManager packageManager, @NonNull OptPropFactory optPropBuilder) { mActivityRecord = activityRecord; mAllowForceResizeOverrideOptProp = optPropBuilder.create( PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES); + mAllowRestrictedResizability = AppCompatUtils.asLazy(() -> { + // Application level. + if (allowRestrictedResizability(packageManager, mActivityRecord.packageName)) { + return true; + } + // Activity level. + try { + return packageManager.getPropertyAsUser( + PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY, + mActivityRecord.mActivityComponent.getPackageName(), + mActivityRecord.mActivityComponent.getClassName(), + mActivityRecord.mUserId).getBoolean(); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + }); + } + + static boolean allowRestrictedResizability(PackageManager pm, String packageName) { + try { + return pm.getProperty(PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY, packageName) + .getBoolean(); + } catch (PackageManager.NameNotFoundException e) { + return false; + } } /** @@ -75,4 +108,9 @@ class AppCompatResizeOverrides { return mAllowForceResizeOverrideOptProp.shouldEnableWithOptInOverrideAndOptOutProperty( isChangeEnabled(mActivityRecord, FORCE_NON_RESIZE_APP)); } + + /** @see android.view.WindowManager#PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY */ + boolean allowRestrictedResizability() { + return mAllowRestrictedResizability.getAsBoolean(); + } } diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index 9f88bc952351..e28dddc496e1 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -138,7 +138,7 @@ final class AppCompatUtils { return; } final AppCompatReachabilityOverrides reachabilityOverrides = top.mAppCompatController - .getAppCompatReachabilityOverrides(); + .getReachabilityOverrides(); final boolean isTopActivityResumed = top.getOrganizedTask() == task && top.isState(RESUMED); final boolean isTopActivityVisible = top.getOrganizedTask() == task && top.isVisible(); // Whether the direct top activity is in size compat mode. diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index a4e58ef923b8..d6ae65193121 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -108,9 +108,7 @@ final class ContentRecorder implements WindowContainerListener { ContentRecorder(@NonNull DisplayContent displayContent) { this(displayContent, new RemoteMediaProjectionManagerWrapper(displayContent.mDisplayId), - new DisplayManagerFlags().isConnectedDisplayManagementEnabled() - && !new DisplayManagerFlags() - .isPixelAnisotropyCorrectionInLogicalDisplayEnabled() + !new DisplayManagerFlags().isPixelAnisotropyCorrectionInLogicalDisplayEnabled() && displayContent.getDisplayInfo().type == Display.TYPE_EXTERNAL); } diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java index f40d636b522a..b932ef362aca 100644 --- a/services/core/java/com/android/server/wm/DisplayArea.java +++ b/services/core/java/com/android/server/wm/DisplayArea.java @@ -264,7 +264,7 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> { // that should be respected, Check all activities in display to make sure any eligible // activity should be respected. final ActivityRecord activity = mDisplayContent.getActivity((r) -> - r.mAppCompatController.getAppCompatOrientationOverrides() + r.mAppCompatController.getOrientationOverrides() .shouldRespectRequestedOrientationDueToOverride()); return activity != null; } diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java index d49a507c9e11..5bec4424269a 100644 --- a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java @@ -25,6 +25,8 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; +import static android.window.DisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT; import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; import static android.window.DisplayAreaOrganizer.FEATURE_FULLSCREEN_MAGNIFICATION; import static android.window.DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT; @@ -151,6 +153,12 @@ public abstract class DisplayAreaPolicy { .all() .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, TYPE_SECURE_SYSTEM_OVERLAY) + .build()) + .addFeature(new Feature.Builder(wmService.mPolicy, "AppZoomOut", + FEATURE_APP_ZOOM_OUT) + .all() + .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, + TYPE_STATUS_BAR, TYPE_NOTIFICATION_SHADE, TYPE_WALLPAPER) .build()); } rootHierarchy diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 145c7b37fcdc..d32c31f1c1c7 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1822,7 +1822,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ private void applyFixedRotationForNonTopVisibleActivityIfNeeded(@NonNull ActivityRecord ar, @ActivityInfo.ScreenOrientation int topOrientation) { - final int orientation = ar.getRequestedOrientation(); + int orientation = ar.getRequestedOrientation(); + if (orientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) { + final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(ar); + if (nextCandidate != null) { + orientation = nextCandidate.getRequestedOrientation(); + } + } if (orientation == topOrientation || ar.inMultiWindowMode() || ar.getRequestedConfigurationOrientation() == ORIENTATION_UNDEFINED) { return; @@ -1864,9 +1870,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return ROTATION_UNDEFINED; } if (activityOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) { - final ActivityRecord nextCandidate = getActivity( - a -> a.canDefineOrientationForActivitiesAbove() /* callback */, - r /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */); + final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(r); if (nextCandidate != null) { r = nextCandidate; activityOrientation = r.getOverrideOrientation(); @@ -2964,7 +2968,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (!handlesOrientationChangeFromDescendant(orientation)) { ActivityRecord topActivity = topRunningActivity(/* considerKeyguardState= */ true); if (topActivity != null && topActivity.mAppCompatController - .getAppCompatOrientationOverrides() + .getOrientationOverrides() .shouldUseDisplayLandscapeNaturalOrientation()) { ProtoLog.v(WM_DEBUG_ORIENTATION, "Display id=%d is ignoring orientation request for %d, return %d" @@ -4291,7 +4295,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return target; } if (android.view.inputmethod.Flags.refactorInsetsController()) { - final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked(); + final DisplayContent defaultDc = getUserMainDisplayContent(); return defaultDc.mRemoteInsetsControlTarget; } else { return getImeFallback(); @@ -4301,11 +4305,26 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp InsetsControlTarget getImeFallback() { // host is in non-default display that doesn't support system decor, default to // default display's StatusBar to control IME (when available), else let system control it. - final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked(); - WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar(); + final DisplayContent defaultDc = getUserMainDisplayContent(); + final WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar(); return statusBar != null ? statusBar : defaultDc.mRemoteInsetsControlTarget; } + private DisplayContent getUserMainDisplayContent() { + final DisplayContent defaultDc; + if (android.view.inputmethod.Flags.fallbackDisplayForSecondaryUserOnSecondaryDisplay()) { + final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId); + defaultDc = mWmService.getUserMainDisplayContentLocked(userId); + if (defaultDc == null) { + throw new IllegalStateException( + "No default display was assigned to user " + userId); + } + } else { + defaultDc = mWmService.getDefaultDisplayContentLocked(); + } + return defaultDc; + } + /** * Returns the corresponding IME insets control target according the IME target type. * @@ -4841,8 +4860,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // The control target could be the RemoteInsetsControlTarget (if the focussed // view is on a virtual display that can not show the IME (and therefore it will // be shown on the default display) - if (isDefaultDisplay && mRemoteInsetsControlTarget != null) { - return mRemoteInsetsControlTarget; + if (android.view.inputmethod.Flags + .fallbackDisplayForSecondaryUserOnSecondaryDisplay()) { + if (isUserMainDisplay() && mRemoteInsetsControlTarget != null) { + return mRemoteInsetsControlTarget; + } + } else { + if (isDefaultDisplay && mRemoteInsetsControlTarget != null) { + return mRemoteInsetsControlTarget; + } } } return null; @@ -4858,6 +4884,16 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * Returns {@code true} if {@link #mDisplayId} corresponds to the user's main display. + * + * <p>Visible background users may have other than DEFAULT_DISPLAY marked as their main display. + */ + private boolean isUserMainDisplay() { + final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId); + return mDisplayId == mWmService.mUmInternal.getMainDisplayAssignedToUser(userId); + } + + /** * Computes the window the IME should be attached to. */ @VisibleForTesting diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 98ed6f76b2f9..54ae80cfe98a 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -103,6 +103,8 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { // again, so that the control with leash can be eventually dispatched if (!mGivenInsetsReady && isServerVisible() && !givenInsetsPending && mControlTarget != null) { + ProtoLog.d(WM_DEBUG_IME, + "onPostLayout: IME control ready to be dispatched, ws=%s", ws); mGivenInsetsReady = true; ImeTracker.forLogging().onProgress(mStatsToken, ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED); @@ -118,6 +120,8 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED); mStatsToken = null; } else if (wasServerVisible && !isServerVisible()) { + ProtoLog.d(WM_DEBUG_IME, "onPostLayout: setImeShowing(false) was: %s, ws=%s", + isImeShowing(), ws); setImeShowing(false); } } @@ -621,6 +625,7 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { // request (cancelling the initial show) or hide request (aborting the initial show). logIsScheduledAndReadyToShowIme(!visible /* aborted */); } + ProtoLog.d(WM_DEBUG_IME, "receiveImeStatsToken: visible=%s", visible); if (visible) { ImeTracker.forLogging().onCancelled( mStatsToken, ImeTracker.PHASE_WM_ABORT_SHOW_IME_POST_LAYOUT); diff --git a/services/core/java/com/android/server/wm/InputConfigAdapter.java b/services/core/java/com/android/server/wm/InputConfigAdapter.java index ae6e72464555..e3ffe716271c 100644 --- a/services/core/java/com/android/server/wm/InputConfigAdapter.java +++ b/services/core/java/com/android/server/wm/InputConfigAdapter.java @@ -76,9 +76,6 @@ class InputConfigAdapter { LayoutParams.FLAG_NOT_TOUCHABLE, InputConfig.NOT_TOUCHABLE, false /* inverted */), new FlagMapping( - LayoutParams.FLAG_SPLIT_TOUCH, - InputConfig.PREVENT_SPLITTING, true /* inverted */), - new FlagMapping( LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, InputConfig.WATCH_OUTSIDE_TOUCH, false /* inverted */), new FlagMapping( diff --git a/services/core/java/com/android/server/wm/PersisterQueue.java b/services/core/java/com/android/server/wm/PersisterQueue.java index 9dc3d6a81338..bc16a566bfef 100644 --- a/services/core/java/com/android/server/wm/PersisterQueue.java +++ b/services/core/java/com/android/server/wm/PersisterQueue.java @@ -86,6 +86,34 @@ class PersisterQueue { mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThread"); } + /** + * Busy wait until {@link #mLazyTaskWriterThread} is in {@link Thread.State#WAITING}, or + * times out. This indicates the thread is waiting for new tasks to appear. If the wait + * succeeds, this queue waits at least {@link #mPreTaskDelayMs} milliseconds before running the + * next task. + * + * <p>This is for testing purposes only. + * + * @param timeoutMillis the maximum time of waiting in milliseconds + * @return {@code true} if the thread is in {@link Thread.State#WAITING} at return + */ + @VisibleForTesting + boolean waitUntilWritingThreadIsWaiting(long timeoutMillis) { + final long timeoutTime = SystemClock.uptimeMillis() + timeoutMillis; + do { + Thread.State state; + synchronized (this) { + state = mLazyTaskWriterThread.getState(); + } + if (state == Thread.State.WAITING) { + return true; + } + Thread.yield(); + } while (SystemClock.uptimeMillis() < timeoutTime); + + return false; + } + synchronized void startPersisting() { if (!mLazyTaskWriterThread.isAlive()) { mLazyTaskWriterThread.start(); diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java index a5454546341b..3eb13c52cca6 100644 --- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java +++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java @@ -407,10 +407,8 @@ class SnapshotPersistQueue { bitmap.recycle(); final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId); - try { - FileOutputStream fos = new FileOutputStream(file); + try (FileOutputStream fos = new FileOutputStream(file)) { swBitmap.compress(JPEG, COMPRESS_QUALITY, fos); - fos.close(); } catch (IOException e) { Slog.e(TAG, "Unable to open " + file + " for persisting.", e); return false; @@ -428,10 +426,8 @@ class SnapshotPersistQueue { swBitmap.recycle(); final File lowResFile = mPersistInfoProvider.getLowResolutionBitmapFile(mId, mUserId); - try { - FileOutputStream lowResFos = new FileOutputStream(lowResFile); + try (FileOutputStream lowResFos = new FileOutputStream(lowResFile)) { lowResBitmap.compress(JPEG, COMPRESS_QUALITY, lowResFos); - lowResFos.close(); } catch (IOException e) { Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e); return false; diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java index 7349224ddcd8..1a7a6196cf85 100644 --- a/services/core/java/com/android/server/wm/StartingData.java +++ b/services/core/java/com/android/server/wm/StartingData.java @@ -31,11 +31,18 @@ public abstract class StartingData { static final int AFTER_TRANSACTION_REMOVE_DIRECTLY = 1; /** Do copy splash screen to client after transaction done. */ static final int AFTER_TRANSACTION_COPY_TO_CLIENT = 2; + /** + * Remove the starting window after transition finish. + * Used when activity doesn't request show when locked, so the app window should never show to + * the user if device is locked. + **/ + static final int AFTER_TRANSITION_FINISH = 3; @IntDef(prefix = { "AFTER_TRANSACTION" }, value = { AFTER_TRANSACTION_IDLE, AFTER_TRANSACTION_REMOVE_DIRECTLY, AFTER_TRANSACTION_COPY_TO_CLIENT, + AFTER_TRANSITION_FINISH, }) @interface AfterTransaction {} diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index d92301ba4f6f..9c1cf6e6bf62 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5255,6 +5255,10 @@ class Task extends TaskFragment { return false; } + if (!mTaskSupervisor.readyToResume()) { + return false; + } + final ActivityRecord topActivity = topRunningActivity(true /* focusableOnly */); if (topActivity == null) { // There are no activities left in this task, let's look somewhere else. diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index f0faa8e4691f..f4a455a9c2dd 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -70,6 +70,8 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN; +import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE; +import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; @@ -1374,6 +1376,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { enterAutoPip = true; } } + + if (ar.mStartingData != null && ar.mStartingData.mRemoveAfterTransaction + == AFTER_TRANSITION_FINISH + && (!ar.isVisible() || !ar.mTransitionController.inTransition(ar))) { + ar.mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE; + ar.removeStartingWindow(); + } final ChangeInfo changeInfo = mChanges.get(ar); // Due to transient-hide, there may be some activities here which weren't in the // transition. @@ -1412,6 +1421,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (!tr.isAttached() || !tr.isVisibleRequested() || !tr.inPinnedWindowingMode()) return; final ActivityRecord currTop = tr.getTopNonFinishingActivity(); + if (currTop == null) return; if (currTop.inPinnedWindowingMode()) return; Slog.e(TAG, "Enter-PIP was started but not completed, this is a Shell/SysUI" + " bug. This state breaks gesture-nav, so attempting clean-up."); diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index aa60f939f9aa..54a3d4179e3d 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1659,6 +1659,12 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return ORIENTATION_UNDEFINED; } + @Nullable + ActivityRecord getActivityBelowForDefiningOrientation(ActivityRecord from) { + return getActivity(ActivityRecord::canDefineOrientationForActivitiesAbove, + from /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */); + } + /** * Calls {@link #setOrientation(int, WindowContainer)} with {@code null} to the last 2 * parameters. diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 3c6778ecbb30..e4ef3d186bdb 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -7473,6 +7473,23 @@ public class WindowManagerService extends IWindowManager.Stub return mRoot.getDisplayContent(DEFAULT_DISPLAY); } + /** + * Returns the main display content for the user passed as parameter. + * + * <p>Visible background users may have their own designated main display, distinct from the + * system default display (DEFAULT_DISPLAY). Visible background users operate independently + * with their own main displays. These secondary user main displays host the secondary home + * activities. + */ + @Nullable + DisplayContent getUserMainDisplayContentLocked(@UserIdInt int userId) { + final int userMainDisplayId = mUmInternal.getMainDisplayAssignedToUser(userId); + if (userMainDisplayId == -1) { + return null; + } + return mRoot.getDisplayContent(userMainDisplayId); + } + public void onOverlayChanged() { // Post to display thread so it can get the latest display info. mH.post(() -> { @@ -10177,9 +10194,10 @@ public class WindowManagerService extends IWindowManager.Stub throw new SecurityException("Access denied to process: " + pid + ", must have permission " + Manifest.permission.ACCESS_FPS_COUNTER); } - - if (mRoot.anyTaskForId(taskId) == null) { - throw new IllegalArgumentException("no task with taskId: " + taskId); + synchronized (mGlobalLock) { + if (mRoot.anyTaskForId(taskId) == null) { + throw new IllegalArgumentException("no task with taskId: " + taskId); + } } mTaskFpsCallbackController.registerListener(taskId, callback); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index b43e334d6e1a..d69b06ad71ea 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -126,6 +126,7 @@ import static com.android.server.wm.IdentifierProto.USER_ID; import static com.android.server.wm.MoveAnimationSpecProto.DURATION_MS; import static com.android.server.wm.MoveAnimationSpecProto.FROM; import static com.android.server.wm.MoveAnimationSpecProto.TO; +import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL; @@ -1920,6 +1921,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } final ActivityRecord atoken = mActivityRecord; if (atoken != null) { + if (atoken.mStartingData != null && mAttrs.type != TYPE_APPLICATION_STARTING + && atoken.mStartingData.mRemoveAfterTransaction + == AFTER_TRANSITION_FINISH) { + // Preventing app window from visible during un-occluding animation playing due to + // alpha blending. + return false; + } final boolean isVisible = isStartingWindowAssociatedToTask() ? mStartingData.mAssociatedTask.isVisible() : atoken.isVisible(); return ((!isParentWindowHidden() && isVisible) @@ -2925,7 +2933,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final int mask = FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD | FLAG_ALLOW_LOCK_WHILE_SCREEN_ON; WindowManager.LayoutParams sa = mActivityRecord.mStartingWindow.mAttrs; + final boolean wasShowWhenLocked = (sa.flags & FLAG_SHOW_WHEN_LOCKED) != 0; + final boolean removeShowWhenLocked = (mAttrs.flags & FLAG_SHOW_WHEN_LOCKED) == 0; sa.flags = (sa.flags & ~mask) | (mAttrs.flags & mask); + if (Flags.keepAppWindowHideWhileLocked() && wasShowWhenLocked && removeShowWhenLocked) { + // Trigger unoccluding animation if needed. + mActivityRecord.checkKeyguardFlagsChanged(); + mActivityRecord.deferStartingWindowRemovalForKeyguardUnoccluding(); + } } } @@ -5424,7 +5439,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // change then delay the position update until it has redrawn to avoid any flickers. final boolean isLetterboxedAndRelaunching = activityRecord != null && activityRecord.areBoundsLetterboxed() - && activityRecord.mAppCompatController.getAppCompatOrientationOverrides() + && activityRecord.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged(); if (surfaceResizedWithoutMoveAnimation || isLetterboxedAndRelaunching) { applyWithNextDraw(mSetSurfacePositionConsumer); diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 65cf4ee733dd..911c686c711f 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -343,9 +343,10 @@ public: void setPointerDisplayId(ui::LogicalDisplayId displayId); int32_t getMousePointerSpeed(); void setPointerSpeed(int32_t speed); - void setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId, bool enabled); + void setMouseScalingEnabled(ui::LogicalDisplayId displayId, bool enabled); void setMouseReverseVerticalScrollingEnabled(bool enabled); void setMouseScrollingAccelerationEnabled(bool enabled); + void setMouseScrollingSpeed(int32_t speed); void setMouseSwapPrimaryButtonEnabled(bool enabled); void setMouseAccelerationEnabled(bool enabled); void setTouchpadPointerSpeed(int32_t speed); @@ -473,8 +474,8 @@ private: // Pointer speed. int32_t pointerSpeed{0}; - // Displays on which its associated mice will have pointer acceleration disabled. - std::set<ui::LogicalDisplayId> displaysWithMousePointerAccelerationDisabled{}; + // Displays on which its associated mice will have all scaling disabled. + std::set<ui::LogicalDisplayId> displaysWithMouseScalingDisabled{}; // True if pointer gestures are enabled. bool pointerGesturesEnabled{true}; @@ -500,6 +501,9 @@ private: // True if mouse scrolling acceleration is enabled. bool mouseScrollingAccelerationEnabled{true}; + // The mouse scrolling speed, as a number from -7 (slowest) to 7 (fastest). + int32_t mouseScrollingSpeed{0}; + // True if mouse vertical scrolling is reversed. bool mouseReverseVerticalScrollingEnabled{false}; @@ -599,9 +603,8 @@ void NativeInputManager::dump(std::string& dump) { dump += StringPrintf(INDENT "System UI Lights Out: %s\n", toString(mLocked.systemUiLightsOut)); dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed); - dump += StringPrintf(INDENT "Display with Mouse Pointer Acceleration Disabled: %s\n", - dumpSet(mLocked.displaysWithMousePointerAccelerationDisabled, - streamableToString) + dump += StringPrintf(INDENT "Display with Mouse Scaling Disabled: %s\n", + dumpSet(mLocked.displaysWithMouseScalingDisabled, streamableToString) .c_str()); dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n", toString(mLocked.pointerGesturesEnabled)); @@ -830,19 +833,20 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon std::scoped_lock _l(mLock); outConfig->mousePointerSpeed = mLocked.pointerSpeed; - outConfig->displaysWithMousePointerAccelerationDisabled = - mLocked.displaysWithMousePointerAccelerationDisabled; + outConfig->displaysWithMouseScalingDisabled = mLocked.displaysWithMouseScalingDisabled; outConfig->pointerVelocityControlParameters.scale = exp2f(mLocked.pointerSpeed * POINTER_SPEED_EXPONENT); outConfig->pointerVelocityControlParameters.acceleration = - mLocked.displaysWithMousePointerAccelerationDisabled.count( - mLocked.pointerDisplayId) == 0 + mLocked.displaysWithMouseScalingDisabled.count(mLocked.pointerDisplayId) == 0 ? android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION : 1; outConfig->wheelVelocityControlParameters.acceleration = mLocked.mouseScrollingAccelerationEnabled ? android::os::IInputConstants::DEFAULT_MOUSE_WHEEL_ACCELERATION : 1; + outConfig->wheelVelocityControlParameters.scale = mLocked.mouseScrollingAccelerationEnabled + ? 1 + : exp2f(mLocked.mouseScrollingSpeed * POINTER_SPEED_EXPONENT); outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled; outConfig->pointerCaptureRequest = mLocked.pointerCaptureRequest; @@ -1451,6 +1455,21 @@ void NativeInputManager::setMouseScrollingAccelerationEnabled(bool enabled) { InputReaderConfiguration::Change::POINTER_SPEED); } +void NativeInputManager::setMouseScrollingSpeed(int32_t speed) { + { // acquire lock + std::scoped_lock _l(mLock); + + if (mLocked.mouseScrollingSpeed == speed) { + return; + } + + mLocked.mouseScrollingSpeed = speed; + } // release lock + + mInputManager->getReader().requestRefreshConfiguration( + InputReaderConfiguration::Change::POINTER_SPEED); +} + void NativeInputManager::setMouseSwapPrimaryButtonEnabled(bool enabled) { { // acquire lock std::scoped_lock _l(mLock); @@ -1497,23 +1516,21 @@ void NativeInputManager::setPointerSpeed(int32_t speed) { InputReaderConfiguration::Change::POINTER_SPEED); } -void NativeInputManager::setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId, - bool enabled) { +void NativeInputManager::setMouseScalingEnabled(ui::LogicalDisplayId displayId, bool enabled) { { // acquire lock std::scoped_lock _l(mLock); - const bool oldEnabled = - mLocked.displaysWithMousePointerAccelerationDisabled.count(displayId) == 0; + const bool oldEnabled = mLocked.displaysWithMouseScalingDisabled.count(displayId) == 0; if (oldEnabled == enabled) { return; } - ALOGI("Setting mouse pointer acceleration to %s on display %s", toString(enabled), + ALOGI("Setting mouse pointer scaling to %s on display %s", toString(enabled), displayId.toString().c_str()); if (enabled) { - mLocked.displaysWithMousePointerAccelerationDisabled.erase(displayId); + mLocked.displaysWithMouseScalingDisabled.erase(displayId); } else { - mLocked.displaysWithMousePointerAccelerationDisabled.emplace(displayId); + mLocked.displaysWithMouseScalingDisabled.emplace(displayId); } } // release lock @@ -2567,11 +2584,11 @@ static void nativeSetPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed im->setPointerSpeed(speed); } -static void nativeSetMousePointerAccelerationEnabled(JNIEnv* env, jobject nativeImplObj, - jint displayId, jboolean enabled) { +static void nativeSetMouseScalingEnabled(JNIEnv* env, jobject nativeImplObj, jint displayId, + jboolean enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - im->setMousePointerAccelerationEnabled(ui::LogicalDisplayId{displayId}, enabled); + im->setMouseScalingEnabled(ui::LogicalDisplayId{displayId}, enabled); } static void nativeSetTouchpadPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) { @@ -3243,6 +3260,11 @@ static void nativeSetMouseScrollingAccelerationEnabled(JNIEnv* env, jobject nati im->setMouseScrollingAccelerationEnabled(enabled); } +static void nativeSetMouseScrollingSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + im->setMouseScrollingSpeed(speed); +} + static void nativeSetMouseReverseVerticalScrollingEnabled(JNIEnv* env, jobject nativeImplObj, bool enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -3313,12 +3335,12 @@ static const JNINativeMethod gInputManagerMethods[] = { {"transferTouch", "(Landroid/os/IBinder;I)Z", (void*)nativeTransferTouchOnDisplay}, {"getMousePointerSpeed", "()I", (void*)nativeGetMousePointerSpeed}, {"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed}, - {"setMousePointerAccelerationEnabled", "(IZ)V", - (void*)nativeSetMousePointerAccelerationEnabled}, + {"setMouseScalingEnabled", "(IZ)V", (void*)nativeSetMouseScalingEnabled}, {"setMouseReverseVerticalScrollingEnabled", "(Z)V", (void*)nativeSetMouseReverseVerticalScrollingEnabled}, {"setMouseScrollingAccelerationEnabled", "(Z)V", (void*)nativeSetMouseScrollingAccelerationEnabled}, + {"setMouseScrollingSpeed", "(I)V", (void*)nativeSetMouseScrollingSpeed}, {"setMouseSwapPrimaryButtonEnabled", "(Z)V", (void*)nativeSetMouseSwapPrimaryButtonEnabled}, {"setMouseAccelerationEnabled", "(Z)V", (void*)nativeSetMouseAccelerationEnabled}, {"setTouchpadPointerSpeed", "(I)V", (void*)nativeSetTouchpadPointerSpeed}, diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 2627895b8c63..d2d388401e23 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -8909,11 +8909,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (parent) { Preconditions.checkCallAuthorization( - isProfileOwnerOfOrganizationOwnedDevice(getCallerIdentity().getUserId())); + isProfileOwnerOfOrganizationOwnedDevice(caller.getUserId())); + // If a DPC is querying on the parent instance, make sure it's only querying the parent + // user of itself. Querying any other user is not allowed. + Preconditions.checkArgument(caller.getUserId() == userHandle); } + int affectedUserId = parent ? getProfileParentId(userHandle) : userHandle; Boolean disallowed = mDevicePolicyEngine.getResolvedPolicy( PolicyDefinition.SCREEN_CAPTURE_DISABLED, - userHandle); + affectedUserId); return disallowed != null && disallowed; } @@ -14669,7 +14673,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled, PersistableBundle options) { - if (Flags.secondaryLockscreenApiEnabled()) { + if (Flags.secondaryLockscreenApiEnabled() && mSupervisionManagerInternal != null) { final CallerIdentity caller = getCallerIdentity(); final boolean isRoleHolder = isCallerSystemSupervisionRoleHolder(caller); synchronized (getLockObject()) { @@ -14680,16 +14684,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { caller.getUserId()); } - if (mSupervisionManagerInternal != null) { - mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser( - caller.getUserId(), enabled, options); - } else { - synchronized (getLockObject()) { - DevicePolicyData policy = getUserData(caller.getUserId()); - policy.mSecondaryLockscreenEnabled = enabled; - saveSettingsLocked(caller.getUserId()); - } - } + mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser( + caller.getUserId(), enabled, options); } else { Objects.requireNonNull(who, "ComponentName is null"); @@ -21907,7 +21903,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { accountToMigrate, sourceUser, targetUser, - /* callback= */ null, /* handler= */ null) + /* handler= */ null, /* callback= */ null) .getResult(60 * 3, TimeUnit.SECONDS); if (copySucceeded) { logCopyAccountStatus(COPY_ACCOUNT_SUCCEEDED, callerPackage); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index c5d42ad9f081..8e06ed8cc283 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -2714,16 +2714,18 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(AuthService.class); t.traceEnd(); - if (android.security.Flags.secureLockdown()) { - t.traceBegin("StartSecureLockDeviceService.Lifecycle"); - mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class); - t.traceEnd(); - } + if (!isWatch && !isTv && !isAutomotive) { + if (android.security.Flags.secureLockdown()) { + t.traceBegin("StartSecureLockDeviceService.Lifecycle"); + mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class); + t.traceEnd(); + } - if (android.adaptiveauth.Flags.enableAdaptiveAuth()) { - t.traceBegin("StartAuthenticationPolicyService"); - mSystemServiceManager.startService(AuthenticationPolicyService.class); - t.traceEnd(); + if (android.adaptiveauth.Flags.enableAdaptiveAuth()) { + t.traceBegin("StartAuthenticationPolicyService"); + mSystemServiceManager.startService(AuthenticationPolicyService.class); + t.traceEnd(); + } } if (!isWatch) { diff --git a/services/print/Android.bp b/services/print/Android.bp index 0dfceaa3a9d9..b77cf162d984 100644 --- a/services/print/Android.bp +++ b/services/print/Android.bp @@ -18,8 +18,21 @@ java_library_static { name: "services.print", defaults: ["platform_service_defaults"], srcs: [":services.print-sources"], + static_libs: ["print_flags_lib"], libs: ["services.core"], lint: { baseline_filename: "lint-baseline.xml", }, } + +aconfig_declarations { + name: "print_flags", + package: "com.android.server.print", + container: "system", + srcs: ["**/flags.aconfig"], +} + +java_aconfig_library { + name: "print_flags_lib", + aconfig_declarations: "print_flags", +} diff --git a/services/print/java/com/android/server/print/RemotePrintService.java b/services/print/java/com/android/server/print/RemotePrintService.java index 502cd2c60f4a..b85671581cc5 100644 --- a/services/print/java/com/android/server/print/RemotePrintService.java +++ b/services/print/java/com/android/server/print/RemotePrintService.java @@ -572,7 +572,8 @@ final class RemotePrintService implements DeathRecipient { boolean wasBound = mContext.bindServiceAsUser(mIntent, mServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE - | Context.BIND_INCLUDE_CAPABILITIES | Context.BIND_ALLOW_INSTANT, + | (Flags.doNotIncludeCapabilities() ? 0 : Context.BIND_INCLUDE_CAPABILITIES) + | Context.BIND_ALLOW_INSTANT, new UserHandle(mUserId)); if (!wasBound) { diff --git a/services/print/java/com/android/server/print/flags.aconfig b/services/print/java/com/android/server/print/flags.aconfig new file mode 100644 index 000000000000..42d142545660 --- /dev/null +++ b/services/print/java/com/android/server/print/flags.aconfig @@ -0,0 +1,9 @@ +package: "com.android.server.print" +container: "system" + +flag { + name: "do_not_include_capabilities" + namespace: "printing" + description: "Do not use the flag Context.BIND_INCLUDE_CAPABILITIES when binding to the service" + bug: "291281543" +} diff --git a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java index 4e9fff230bac..b80d68de0f2e 100644 --- a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java +++ b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java @@ -37,6 +37,7 @@ import static org.testng.Assert.expectThrows; import android.annotation.UserIdInt; import android.app.Application; +import android.app.backup.BackupManagerInternal; import android.app.backup.IBackupManagerMonitor; import android.app.backup.IBackupObserver; import android.app.backup.IFullBackupRestoreObserver; @@ -52,6 +53,7 @@ import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.util.SparseArray; +import com.android.server.LocalServices; import com.android.server.SystemService.TargetUser; import com.android.server.backup.testing.TransportData; import com.android.server.testing.shadows.ShadowApplicationPackageManager; @@ -229,7 +231,7 @@ public class BackupManagerServiceRoboTest { setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false); IBinder agentBinder = mock(IBinder.class); - backupManagerService.agentConnected(mUserOneId, TEST_PACKAGE, agentBinder); + backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserOneId, agentBinder); verify(mUserOneBackupAgentConnectionManager).agentConnected(TEST_PACKAGE, agentBinder); } @@ -242,7 +244,7 @@ public class BackupManagerServiceRoboTest { setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false); IBinder agentBinder = mock(IBinder.class); - backupManagerService.agentConnected(mUserTwoId, TEST_PACKAGE, agentBinder); + backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserTwoId, agentBinder); verify(mUserOneBackupAgentConnectionManager, never()).agentConnected(TEST_PACKAGE, agentBinder); @@ -1549,6 +1551,7 @@ public class BackupManagerServiceRoboTest { @Test public void testOnStart_publishesService() { BackupManagerService backupManagerService = mock(BackupManagerService.class); + LocalServices.removeServiceForTest(BackupManagerInternal.class); BackupManagerService.Lifecycle lifecycle = spy(new BackupManagerService.Lifecycle(mContext, backupManagerService)); doNothing().when(lifecycle).publishService(anyString(), any()); diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java index 7277fd79fdd5..66aaa562b873 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java @@ -45,6 +45,7 @@ import android.os.UserHandle; import android.platform.test.annotations.Presubmit; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Pair; import android.util.SparseArray; import androidx.annotation.NonNull; @@ -78,10 +79,7 @@ import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Set; @Presubmit @RunWith(JUnit4.class) @@ -885,18 +883,15 @@ public class AppsFilterImplTest { return null; } - @NonNull + @Nullable @Override - public Map<String, Set<String>> getTargetToOverlayables( + public Pair<String, String> getTargetToOverlayables( @NonNull AndroidPackage pkg) { if (overlay.getPackageName().equals(pkg.getPackageName())) { - Map<String, Set<String>> map = new ArrayMap<>(); - Set<String> set = new ArraySet<>(); - set.add(overlay.getOverlayTargetOverlayableName()); - map.put(overlay.getOverlayTarget(), set); - return map; + return Pair.create(overlay.getOverlayTarget(), + overlay.getOverlayTargetOverlayableName()); } - return Collections.emptyMap(); + return null; } }, mMockHandler); @@ -977,18 +972,15 @@ public class AppsFilterImplTest { return null; } - @NonNull + @Nullable @Override - public Map<String, Set<String>> getTargetToOverlayables( + public Pair<String, String> getTargetToOverlayables( @NonNull AndroidPackage pkg) { if (overlay.getPackageName().equals(pkg.getPackageName())) { - Map<String, Set<String>> map = new ArrayMap<>(); - Set<String> set = new ArraySet<>(); - set.add(overlay.getOverlayTargetOverlayableName()); - map.put(overlay.getOverlayTarget(), set); - return map; + return Pair.create(overlay.getOverlayTarget(), + overlay.getOverlayTargetOverlayableName()); } - return Collections.emptyMap(); + return null; } }, mMockHandler); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index a9ad435762ad..02e5470e8673 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -415,7 +415,6 @@ public class DisplayManagerServiceTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false); mLocalServiceKeeperRule.overrideLocalService( InputManagerInternal.class, mMockInputManagerInternal); @@ -2797,30 +2796,7 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectExternalDisplay_withoutDisplayManagement_shouldAddDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false); - manageDisplaysPermission(/* granted= */ true); - DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); - DisplayManagerService.BinderService bs = displayManager.new BinderService(); - LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); - FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); - bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS); - callback.expectsEvent(EVENT_DISPLAY_ADDED); - - FakeDisplayDevice displayDevice = - createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL); - callback.waitForExpectedEvent(); - - LogicalDisplay display = - logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true); - assertThat(display.isEnabledLocked()).isTrue(); - assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_ADDED); - - } - - @Test - public void testConnectExternalDisplay_withDisplayManagement_shouldDisableDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testConnectExternalDisplay_shouldDisableDisplay() { manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -2849,9 +2825,8 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectExternalDisplay_withDisplayManagementAndSysprop_shouldEnableDisplay() { + public void testConnectExternalDisplay_withSysprop_shouldEnableDisplay() { Assume.assumeTrue(Build.IS_ENG || Build.IS_USERDEBUG); - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); doAnswer((Answer<Boolean>) invocationOnMock -> true) .when(() -> SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)); manageDisplaysPermission(/* granted= */ true); @@ -2883,8 +2858,7 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectExternalDisplay_withDisplayManagement_allowsEnableAndDisableDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testConnectExternalDisplay_allowsEnableAndDisableDisplay() { when(mMockFlags.isApplyDisplayChangedDuringDisplayAddedEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); LocalServices.addService(WindowManagerPolicy.class, mMockedWindowManagerPolicy); @@ -2955,8 +2929,7 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectInternalDisplay_withDisplayManagement_shouldConnectAndAddDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testConnectInternalDisplay_shouldConnectAndAddDisplay() { manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); @@ -3011,7 +2984,7 @@ public class DisplayManagerServiceTest { DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); - bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS); + bs.registerCallbackWithEventMask(callback, STANDARD_DISPLAY_EVENTS); callback.expectsEvent(EVENT_DISPLAY_ADDED); FakeDisplayDevice displayDevice = @@ -3032,8 +3005,7 @@ public class DisplayManagerServiceTest { } @Test - public void testEnableExternalDisplay_withDisplayManagement_shouldSignalDisplayAdded() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testEnableExternalDisplay_shouldSignalDisplayAdded() { manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -3062,8 +3034,7 @@ public class DisplayManagerServiceTest { } @Test - public void testEnableExternalDisplay_withoutPermission_shouldThrowException() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testEnableExternalDisplay_shouldThrowException() { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3087,8 +3058,7 @@ public class DisplayManagerServiceTest { } @Test - public void testEnableInternalDisplay_withManageDisplays_shouldSignalAdded() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testEnableInternalDisplay_shouldSignalAdded() { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3115,8 +3085,7 @@ public class DisplayManagerServiceTest { } @Test - public void testDisableInternalDisplay_withDisplayManagement_shouldSignalRemove() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testDisableInternalDisplay_shouldSignalRemove() { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3140,7 +3109,6 @@ public class DisplayManagerServiceTest { @Test public void testDisableExternalDisplay_shouldSignalDisplayRemoved() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3181,7 +3149,6 @@ public class DisplayManagerServiceTest { @Test public void testDisableExternalDisplay_withoutPermission_shouldThrowException() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3207,7 +3174,6 @@ public class DisplayManagerServiceTest { @Test public void testRemoveExternalDisplay_whenDisabled_shouldSignalDisconnected() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -3244,7 +3210,6 @@ public class DisplayManagerServiceTest { @Test public void testRegisterCallback_withoutPermission_shouldThrow() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); @@ -3255,7 +3220,6 @@ public class DisplayManagerServiceTest { @Test public void testRemoveExternalDisplay_whenEnabled_shouldSignalRemovedAndDisconnected() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -3288,7 +3252,6 @@ public class DisplayManagerServiceTest { @Test public void testRemoveInternalDisplay_whenEnabled_shouldSignalRemovedAndDisconnected() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java index 782262d3f7c9..a48a88cecbc2 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java @@ -22,7 +22,6 @@ import static android.view.Display.TYPE_INTERNAL; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -47,7 +46,6 @@ import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.notifications.DisplayNotificationManager; import com.android.server.testutils.TestHandler; -import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import org.junit.Before; @@ -124,7 +122,6 @@ public class ExternalDisplayPolicyTest { public void setup() throws Exception { MockitoAnnotations.initMocks(this); mHandler = new TestHandler(/*callback=*/ null); - when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); when(mMockedFlags.isConnectedDisplayErrorHandlingEnabled()).thenReturn(true); when(mMockedInjector.getFlags()).thenReturn(mMockedFlags); when(mMockedInjector.getLogicalDisplayMapper()).thenReturn(mMockedLogicalDisplayMapper); @@ -173,16 +170,6 @@ public class ExternalDisplayPolicyTest { } @Test - public void testTryEnableExternalDisplay_featureDisabled(@TestParameter final boolean enable) { - when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn(false); - mExternalDisplayPolicy.setExternalDisplayEnabledLocked(mMockedLogicalDisplay, enable); - mHandler.flush(); - verify(mMockedLogicalDisplayMapper, never()).setDisplayEnabledLocked(any(), anyBoolean()); - verify(mMockedDisplayNotificationManager, never()) - .onHighTemperatureExternalDisplayNotAllowed(); - } - - @Test public void testTryDisableExternalDisplay_criticalThermalCondition() throws RemoteException { // Disallow external displays due to thermals. setTemperature(registerThermalListener(), List.of(CRITICAL_TEMPERATURE)); @@ -278,21 +265,6 @@ public class ExternalDisplayPolicyTest { } @Test - public void testNoThermalListenerRegistered_featureDisabled( - @TestParameter final boolean isConnectedDisplayManagementEnabled, - @TestParameter final boolean isErrorHandlingEnabled) throws RemoteException { - assumeFalse(isConnectedDisplayManagementEnabled && isErrorHandlingEnabled); - when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn( - isConnectedDisplayManagementEnabled); - when(mMockedFlags.isConnectedDisplayErrorHandlingEnabled()).thenReturn( - isErrorHandlingEnabled); - - mExternalDisplayPolicy.onBootCompleted(); - verify(mMockedThermalService, never()).registerThermalEventListenerWithType( - any(), anyInt()); - } - - @Test public void testOnCriticalTemperature_disallowAndAllowExternalDisplay() throws RemoteException { final var thermalListener = registerThermalListener(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java index 0dbb6ba58b3c..7d3cd8a8a9ae 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -222,7 +222,6 @@ public class LogicalDisplayMapperTest { when(mSyntheticModeManagerMock.createAppSupportedModes(any(), any(), anyBoolean())) .thenAnswer(AdditionalAnswers.returnsSecondArg()); - when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(false); mLooper = new TestLooper(); mHandler = new Handler(mLooper.getLooper()); mLogicalDisplayMapper = new LogicalDisplayMapper(mContextMock, mFoldSettingProviderMock, @@ -351,8 +350,7 @@ public class LogicalDisplayMapperTest { } @Test - public void testDisplayDeviceAddAndRemove_withDisplayManagement() { - when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testDisplayDeviceAddAndRemove() { DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800, FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); @@ -390,8 +388,7 @@ public class LogicalDisplayMapperTest { } @Test - public void testDisplayDisableEnable_withDisplayManagement() { - when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testDisplayDisableEnable() { DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800, FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); LogicalDisplay displayAdded = add(device); @@ -1350,9 +1347,14 @@ public class LogicalDisplayMapperTest { ArgumentCaptor<LogicalDisplay> displayCaptor = ArgumentCaptor.forClass(LogicalDisplay.class); verify(mListenerMock).onLogicalDisplayEventLocked( - displayCaptor.capture(), eq(LOGICAL_DISPLAY_EVENT_ADDED)); + displayCaptor.capture(), eq(LOGICAL_DISPLAY_EVENT_CONNECTED)); + LogicalDisplay display = displayCaptor.getValue(); + if (display.isEnabledLocked()) { + verify(mListenerMock).onLogicalDisplayEventLocked( + eq(display), eq(LOGICAL_DISPLAY_EVENT_ADDED)); + } clearInvocations(mListenerMock); - return displayCaptor.getValue(); + return display; } private void testDisplayDeviceAddAndRemove_NonInternal(int type) { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 4a09802fc822..fe7cc923d3d1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -743,6 +743,43 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testUpdateOomAdjFreezeState_receivers() { + final ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true); + + updateOomAdj(app); + assertNoCpuTime(app); + + app.mReceivers.incrementCurReceivers(); + updateOomAdj(app); + assertCpuTime(app); + + app.mReceivers.decrementCurReceivers(); + updateOomAdj(app); + assertNoCpuTime(app); + } + + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testUpdateOomAdjFreezeState_activeInstrumentation() { + ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, + MOCKAPP_PACKAGENAME, true); + updateOomAdj(app); + assertNoCpuTime(app); + + mProcessStateController.setActiveInstrumentation(app, mock(ActiveInstrumentation.class)); + updateOomAdj(app); + assertCpuTime(app); + + mProcessStateController.setActiveInstrumentation(app, null); + updateOomAdj(app); + assertNoCpuTime(app); + } + + @SuppressWarnings("GuardedBy") + @Test public void testUpdateOomAdj_DoOne_OverlayUi() { ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java index 89b48bad2358..27eada013642 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java @@ -16,6 +16,8 @@ package com.android.server.am; +import static android.os.PowerWhitelistManager.REASON_NOTIFICATION_SERVICE; +import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED; import static android.os.Process.INVALID_UID; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -27,9 +29,11 @@ import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_OWNER_CANC import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_OWNER_FORCE_STOPPED; import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_SUPERSEDED; import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_USER_STOPPED; +import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.cancelReasonToString; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -39,9 +43,11 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AppGlobals; +import android.app.BackgroundStartPrivileges; import android.app.PendingIntent; import android.content.Intent; import android.content.pm.IPackageManager; +import android.os.Binder; import android.os.Looper; import android.os.UserHandle; @@ -179,6 +185,34 @@ public class PendingIntentControllerTest { } } + @Test + public void testClearAllowBgActivityStartsClearsToken() { + final PendingIntentRecord pir = createPendingIntentRecord(0); + Binder token = new Binder(); + pir.setAllowBgActivityStarts(token, FLAG_ACTIVITY_SENDER); + assertEquals(BackgroundStartPrivileges.allowBackgroundActivityStarts(token), + pir.getBackgroundStartPrivilegesForActivitySender(token)); + pir.clearAllowBgActivityStarts(token); + assertEquals(BackgroundStartPrivileges.NONE, + pir.getBackgroundStartPrivilegesForActivitySender(token)); + } + + @Test + public void testClearAllowBgActivityStartsClearsDuration() { + final PendingIntentRecord pir = createPendingIntentRecord(0); + Binder token = new Binder(); + pir.setAllowlistDurationLocked(token, 1000, + TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, REASON_NOTIFICATION_SERVICE, + "NotificationManagerService"); + PendingIntentRecord.TempAllowListDuration allowlistDurationLocked = + pir.getAllowlistDurationLocked(token); + assertEquals(1000, allowlistDurationLocked.duration); + pir.clearAllowBgActivityStarts(token); + PendingIntentRecord.TempAllowListDuration allowlistDurationLockedAfterClear = + pir.getAllowlistDurationLocked(token); + assertNull(allowlistDurationLockedAfterClear); + } + private void assertCancelReason(int expectedReason, int actualReason) { final String errMsg = "Expected: " + cancelReasonToString(expectedReason) + "; Actual: " + cancelReasonToString(actualReason); diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java index c4a042370de5..f1f4a0e83a79 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java @@ -39,6 +39,7 @@ import static org.mockito.Mockito.when; import android.Manifest; import android.app.backup.BackupManager; +import android.app.backup.BackupManagerInternal; import android.app.backup.ISelectBackupTransportCallback; import android.app.job.JobScheduler; import android.content.ComponentName; @@ -59,6 +60,7 @@ import androidx.test.filters.SmallTest; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.internal.util.DumpUtils; +import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.backup.utils.RandomAccessFileUtils; @@ -716,7 +718,7 @@ public class BackupManagerServiceTest { // Create BMS *before* setting a main user to simulate the main user being created after // BMS, which can happen for the first ever boot of a new device. mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER)); assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER)); @@ -730,7 +732,7 @@ public class BackupManagerServiceTest { // Create BMS *before* setting a main user to simulate the main user being created after // BMS, which can happen for the first ever boot of a new device. mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER)); assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER)); @@ -754,7 +756,7 @@ public class BackupManagerServiceTest { private void createBackupManagerServiceAndUnlockSystemUser() { mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); simulateUserUnlocked(UserHandle.USER_SYSTEM); } @@ -765,7 +767,15 @@ public class BackupManagerServiceTest { private void setMockMainUserAndCreateBackupManagerService(int userId) { when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(userId)); mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); + } + + private void createBackupServiceLifecycle(Context context, BackupManagerService service) { + // Anytime we manually create the Lifecycle, we need to remove the internal BMS because + // it would've been added already at boot time and LocalServices does not allow + // overriding an existing service. + LocalServices.removeServiceForTest(BackupManagerInternal.class); + mServiceLifecycle = new BackupManagerService.Lifecycle(context, service); } private void simulateUserUnlocked(int userId) { diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java index f1072da4161f..6d91bee6d3f6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java @@ -573,4 +573,14 @@ public class ThermalManagerServiceMockingTest { assertNotNull(ret); assertEquals(0, ret.size()); } + + @Test + public void forecastSkinTemperature() throws RemoteException { + Mockito.when(mAidlHalMock.forecastSkinTemperature(Mockito.anyInt())).thenReturn( + 0.55f + ); + float forecast = mAidlWrapper.forecastSkinTemperature(10); + Mockito.verify(mAidlHalMock, Mockito.times(1)).forecastSkinTemperature(10); + assertEquals(0.55f, forecast, 0.01f); + } } diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java index cd94c0f6e245..e61571288ade 100644 --- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java +++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java @@ -70,8 +70,6 @@ import android.os.PerformanceHintManager; import android.os.Process; import android.os.RemoteException; import android.os.SessionCreationConfig; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -1388,7 +1386,6 @@ public class HintManagerServiceTest { @Test - @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK}) public void testCpuHeadroomCache() throws Exception { CpuHeadroomParamsInternal params1 = new CpuHeadroomParamsInternal(); CpuHeadroomParams halParams1 = new CpuHeadroomParams(); @@ -1476,8 +1473,7 @@ public class HintManagerServiceTest { } @Test - @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK}) - public void testGetCpuHeadroomDifferentAffinity_flagOn() throws Exception { + public void testGetCpuHeadroomDifferentAffinity() throws Exception { CountDownLatch latch = new CountDownLatch(2); int[] tids = createThreads(2, latch); CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal(); @@ -1497,28 +1493,6 @@ public class HintManagerServiceTest { verify(mIPowerMock, times(0)).getCpuHeadroom(any()); } - @Test - @DisableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK}) - public void testGetCpuHeadroomDifferentAffinity_flagOff() throws Exception { - CountDownLatch latch = new CountDownLatch(2); - int[] tids = createThreads(2, latch); - CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal(); - params.tids = tids; - CpuHeadroomParams halParams = new CpuHeadroomParams(); - halParams.tids = tids; - float headroom = 0.1f; - CpuHeadroomResult halRet = CpuHeadroomResult.globalHeadroom(headroom); - String ret1 = runAndWaitForCommand("taskset -p 1 " + tids[0]); - String ret2 = runAndWaitForCommand("taskset -p 3 " + tids[1]); - - HintManagerService service = createService(); - clearInvocations(mIPowerMock); - when(mIPowerMock.getCpuHeadroom(eq(halParams))).thenReturn(halRet); - assertEquals("taskset cmd return: " + ret1 + "\n" + ret2, halRet, - service.getBinderServiceInstance().getCpuHeadroom(params)); - verify(mIPowerMock, times(1)).getCpuHeadroom(any()); - } - private String runAndWaitForCommand(String command) throws Exception { java.lang.Process process = Runtime.getRuntime().exec(command); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp index d6ca10a23fb9..07b18db59960 100644 --- a/services/tests/powerstatstests/Android.bp +++ b/services/tests/powerstatstests/Android.bp @@ -27,6 +27,7 @@ android_test { "servicestests-utils", "platform-test-annotations", "flag-junit", + "apct-perftests-utils", ], libs: [ @@ -64,10 +65,12 @@ android_ravenwood_test { "ravenwood-junit", "truth", "androidx.annotation_annotation", + "androidx.test.ext.junit", "androidx.test.rules", "androidx.test.uiautomator_uiautomator", "modules-utils-binary-xml", "flag-junit", + "apct-perftests-utils", ], srcs: [ "src/com/android/server/power/stats/*.java", diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java new file mode 100644 index 000000000000..cc75e9e3114f --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2025 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.power.stats; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.BatteryStatsManager; +import android.os.BatteryUsageStats; +import android.os.BatteryUsageStatsQuery; +import android.os.ParcelFileDescriptor; +import android.perftests.utils.TraceMarkParser; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.uiautomator.UiDevice; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@LargeTest +@android.platform.test.annotations.DisabledOnRavenwood(reason = "Atrace event test") +public class BatteryStatsHistoryTraceTest { + private static final String ATRACE_START = "atrace --async_start -b 1024 -c ss"; + private static final String ATRACE_STOP = "atrace --async_stop"; + private static final String ATRACE_DUMP = "atrace --async_dump"; + + @Before + public void before() throws Exception { + runShellCommand(ATRACE_START); + } + + @After + public void after() throws Exception { + runShellCommand(ATRACE_STOP); + } + + @Test + public void dumpsys() throws Exception { + runShellCommand("dumpsys batterystats --history"); + + Set<String> slices = readAtraceSlices(); + assertThat(slices).contains("BatteryStatsHistory.copy"); + assertThat(slices).contains("BatteryStatsHistory.iterate"); + } + + @Test + public void getBatteryUsageStats() throws Exception { + BatteryStatsManager batteryStatsManager = + getInstrumentation().getTargetContext().getSystemService(BatteryStatsManager.class); + BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder() + .includeBatteryHistory().build(); + BatteryUsageStats batteryUsageStats = batteryStatsManager.getBatteryUsageStats(query); + assertThat(batteryUsageStats).isNotNull(); + + Set<String> slices = readAtraceSlices(); + assertThat(slices).contains("BatteryStatsHistory.copy"); + assertThat(slices).contains("BatteryStatsHistory.iterate"); + assertThat(slices).contains("BatteryStatsHistory.writeToParcel"); + } + + private String runShellCommand(String cmd) throws Exception { + return UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd); + } + + private Set<String> readAtraceSlices() throws Exception { + Set<String> keys = new HashSet<>(); + + TraceMarkParser parser = new TraceMarkParser( + line -> line.name.startsWith("BatteryStatsHistory.")); + ParcelFileDescriptor pfd = + getInstrumentation().getUiAutomation().executeShellCommand(ATRACE_DUMP); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(new ParcelFileDescriptor.AutoCloseInputStream(pfd)))) { + String line; + while ((line = reader.readLine()) != null) { + parser.visit(line); + } + } + parser.forAllSlices((key, slices) -> keys.add(key)); + return keys; + } +} diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java index e86108d84538..ede61a5a0269 100644 --- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java +++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java @@ -15,18 +15,14 @@ */ package com.android.server.selinux; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import static com.android.server.selinux.SelinuxAuditLogBuilder.toCategories; import static com.google.common.truth.Truth.assertThat; -import android.provider.DeviceConfig; - import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,24 +41,12 @@ public class SelinuxAuditLogsBuilderTest { @Before public void setUp() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - TEST_DOMAIN)); - - mAuditLogBuilder = new SelinuxAuditLogBuilder(); + mAuditLogBuilder = new SelinuxAuditLogBuilder(TEST_DOMAIN); mScontextMatcher = mAuditLogBuilder.mScontextMatcher; mTcontextMatcher = mAuditLogBuilder.mTcontextMatcher; mPathMatcher = mAuditLogBuilder.mPathMatcher; } - @After - public void tearDown() { - runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides()); - } - @Test public void testMatcher_scontext() { assertThat(mScontextMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isTrue(); @@ -109,13 +93,9 @@ public class SelinuxAuditLogsBuilderTest { @Test public void testMatcher_scontextDefaultConfig() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.clearLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN)); - - Matcher scontexMatcher = new SelinuxAuditLogBuilder().mScontextMatcher; + Matcher scontexMatcher = + new SelinuxAuditLogBuilder(SelinuxAuditLogsCollector.DEFAULT_SELINUX_AUDIT_DOMAIN) + .mScontextMatcher; assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isFalse(); assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0:c123,c456").matches()) @@ -221,13 +201,7 @@ public class SelinuxAuditLogsBuilderTest { @Test public void testSelinuxAuditLogsBuilder_wrongConfig() { String notARegexDomain = "not]a[regex"; - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - notARegexDomain)); - SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder(); + SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder(notARegexDomain); noOpBuilder.reset( "granted { p } scontext=u:r:" diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java index b6ccf5e0ad80..db58c74e8431 100644 --- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java +++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java @@ -15,7 +15,6 @@ */ package com.android.server.selinux; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -28,7 +27,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; -import android.provider.DeviceConfig; import android.util.EventLog; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -59,6 +57,7 @@ public class SelinuxAuditLogsCollectorTest { private final SelinuxAuditLogsCollector mSelinuxAutidLogsCollector = // Ignore rate limiting for tests new SelinuxAuditLogsCollector( + () -> TEST_DOMAIN, new RateLimiter(mClock, /* window= */ Duration.ofMillis(0)), new QuotaLimiter( mClock, /* windowSize= */ Duration.ofHours(1), /* maxPermits= */ 5)); @@ -67,13 +66,6 @@ public class SelinuxAuditLogsCollectorTest { @Before public void setUp() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - TEST_DOMAIN)); - mSelinuxAutidLogsCollector.setStopRequested(false); // move the clock forward for the limiters. mClock.currentTimeMillis += Duration.ofHours(1).toMillis(); @@ -85,7 +77,6 @@ public class SelinuxAuditLogsCollectorTest { @After public void tearDown() { - runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides()); mMockitoSession.finishMocking(); } diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java index 82efae45e1a4..92c6db5b7b96 100644 --- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java @@ -21,6 +21,9 @@ import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_VIA_SYS import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap; import static android.service.quickaccesswallet.Flags.launchWalletViaSysuiCallbacks; +import static com.android.server.GestureLauncherService.DOUBLE_TAP_POWER_DISABLED_MODE; +import static com.android.server.GestureLauncherService.DOUBLE_TAP_POWER_LAUNCH_CAMERA_MODE; +import static com.android.server.GestureLauncherService.DOUBLE_TAP_POWER_MULTI_TARGET_MODE; import static com.android.server.GestureLauncherService.LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER; import static com.android.server.GestureLauncherService.LAUNCH_WALLET_ON_DOUBLE_TAP_POWER; import static com.android.server.GestureLauncherService.POWER_DOUBLE_TAP_MAX_TIME_MS; @@ -163,7 +166,7 @@ public class GestureLauncherServiceTest { new GestureLauncherService( mContext, mMetricsLogger, mQuickAccessWalletClient, mUiEventLogger); - withDoubleTapPowerGestureEnableSettingValue(true); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(true); withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); } @@ -215,68 +218,117 @@ public class GestureLauncherServiceTest { } @Test - public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingDisabled() { - if (launchWalletOptionOnPowerDoubleTap()) { - withDoubleTapPowerEnabledConfigValue(false); - withDoubleTapPowerGestureEnableSettingValue(false); - withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); - } else { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); - } + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_flagEnabled_configFalseSettingDisabled() { + withDoubleTapPowerModeConfigValue( + DOUBLE_TAP_POWER_DISABLED_MODE); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(false); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test - public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingEnabled() { - if (launchWalletOptionOnPowerDoubleTap()) { - withDoubleTapPowerEnabledConfigValue(false); - withDoubleTapPowerGestureEnableSettingValue(true); - withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); - assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( - mContext, FAKE_USER_ID)); - } else { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(0); - assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( - mContext, FAKE_USER_ID)); - } + @RequiresFlagsDisabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_flagDisabled_configFalseSettingDisabled() { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + + assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); } @Test - public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingDisabled() { - if (launchWalletOptionOnPowerDoubleTap()) { - withDoubleTapPowerEnabledConfigValue(true); - withDoubleTapPowerGestureEnableSettingValue(false); - withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); - } else { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(1); - } + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_flagEnabled_configFalseSettingEnabled() { + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_DISABLED_MODE); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test - public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingEnabled() { - if (launchWalletOptionOnPowerDoubleTap()) { - withDoubleTapPowerEnabledConfigValue(true); - withDoubleTapPowerGestureEnableSettingValue(true); - withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); - } else { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - } + @RequiresFlagsDisabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_flagDisabled_configFalseSettingEnabled() { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(0); + + assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_flagEnabled_configTrueSettingDisabled() { + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(false); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + + assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsDisabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_flagDisabled_configTrueSettingDisabled() { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(1); + + assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_flagEnabled_configTrueSettingEnabled() { + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + + assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsDisabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_flagDisabled_configTrueSettingEnabled() { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(0); + assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_launchCameraMode_settingEnabled() { + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_LAUNCH_CAMERA_MODE); + withCameraDoubleTapPowerDisableSettingValue(0); + + assertTrue( + mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_launchCameraMode_settingDisabled() { + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_LAUNCH_CAMERA_MODE); + withCameraDoubleTapPowerDisableSettingValue(1); + + assertFalse( + mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) public void testIsCameraDoubleTapPowerSettingEnabled_actionWallet() { - withDoubleTapPowerEnabledConfigValue(true); - withDoubleTapPowerGestureEnableSettingValue(true); + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(true); withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); assertFalse( @@ -287,8 +339,8 @@ public class GestureLauncherServiceTest { @Test @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) public void testIsWalletDoubleTapPowerSettingEnabled() { - withDoubleTapPowerEnabledConfigValue(true); - withDoubleTapPowerGestureEnableSettingValue(true); + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(true); withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); assertTrue( @@ -299,11 +351,11 @@ public class GestureLauncherServiceTest { @Test @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) public void testIsWalletDoubleTapPowerSettingEnabled_configDisabled() { - withDoubleTapPowerEnabledConfigValue(false); - withDoubleTapPowerGestureEnableSettingValue(true); + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_DISABLED_MODE); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(true); withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); - assertTrue( + assertFalse( mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @@ -311,8 +363,8 @@ public class GestureLauncherServiceTest { @Test @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) public void testIsWalletDoubleTapPowerSettingEnabled_settingDisabled() { - withDoubleTapPowerEnabledConfigValue(true); - withDoubleTapPowerGestureEnableSettingValue(false); + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(false); withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); assertFalse( @@ -323,8 +375,8 @@ public class GestureLauncherServiceTest { @Test @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) public void testIsWalletDoubleTapPowerSettingEnabled_actionCamera() { - withDoubleTapPowerEnabledConfigValue(true); - withDoubleTapPowerGestureEnableSettingValue(true); + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(true); withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); assertFalse( @@ -449,13 +501,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffInteractive() { - if (launchWalletOptionOnPowerDoubleTap()) { - withDoubleTapPowerGestureEnableSettingValue(false); - } else { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); - } - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + disableDoubleTapPowerGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -498,13 +544,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOffInteractive() { - if (launchWalletOptionOnPowerDoubleTap()) { - withDoubleTapPowerGestureEnableSettingValue(false); - } else { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); - } - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + disableDoubleTapPowerGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -549,9 +589,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOffInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + disableDoubleTapPowerGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1031,9 +1069,7 @@ public class GestureLauncherServiceTest { public void testInterceptPowerKeyDown_triggerEmergency_cameraGestureEnabled_doubleTap_cooldownTriggered() { // Enable camera double tap gesture - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); // Enable power button cooldown withEmergencyGesturePowerButtonCooldownPeriodMsValue(3000); @@ -1220,10 +1256,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_longpress() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); - withUserSetupCompleteValue(true); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1400,13 +1433,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffNotInteractive() { - if (launchWalletOptionOnPowerDoubleTap()) { - withDoubleTapPowerGestureEnableSettingValue(false); - } else { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); - } - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + disableDoubleTapPowerGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1449,9 +1476,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOffNotInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + disableDoubleTapPowerGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1495,9 +1520,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOffNotInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + disableDoubleTapPowerGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1630,9 +1653,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOnNotInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1823,12 +1844,13 @@ public class GestureLauncherServiceTest { .thenReturn(enableConfigValue); } - private void withDoubleTapPowerEnabledConfigValue(boolean enable) { - when(mResources.getBoolean(com.android.internal.R.bool.config_doubleTapPowerGestureEnabled)) - .thenReturn(enable); + private void withDoubleTapPowerModeConfigValue( + int modeConfigValue) { + when(mResources.getInteger(com.android.internal.R.integer.config_doubleTapPowerGestureMode)) + .thenReturn(modeConfigValue); } - private void withDoubleTapPowerGestureEnableSettingValue(boolean enable) { + private void withMultiTargetDoubleTapPowerGestureEnableSettingValue(boolean enable) { Settings.Secure.putIntForUser( mContentResolver, Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED, @@ -1910,8 +1932,8 @@ public class GestureLauncherServiceTest { private void enableWalletGesture() { withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); - withDoubleTapPowerGestureEnableSettingValue(true); - withDoubleTapPowerEnabledConfigValue(true); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(true); + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE); mGestureLauncherService.updateWalletDoubleTapPowerEnabled(); withUserSetupCompleteValue(true); @@ -1926,8 +1948,9 @@ public class GestureLauncherServiceTest { private void enableCameraGesture() { if (launchWalletOptionOnPowerDoubleTap()) { - withDoubleTapPowerEnabledConfigValue(true); - withDoubleTapPowerGestureEnableSettingValue(true); + withDoubleTapPowerModeConfigValue( + DOUBLE_TAP_POWER_MULTI_TARGET_MODE); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(true); withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); } else { withCameraDoubleTapPowerEnableConfigValue(true); @@ -1937,6 +1960,18 @@ public class GestureLauncherServiceTest { withUserSetupCompleteValue(true); } + private void disableDoubleTapPowerGesture() { + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_DISABLED_MODE); + withMultiTargetDoubleTapPowerGestureEnableSettingValue(false); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } + mGestureLauncherService.updateWalletDoubleTapPowerEnabled(); + withUserSetupCompleteValue(true); + } + private void sendPowerKeyDownToGestureLauncherServiceAndAssertValues( long eventTime, boolean expectedIntercept, boolean expectedOutLaunchedValue) { KeyEvent keyEvent = diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index fa78dfce0a17..dafe4827b2fe 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -220,6 +220,9 @@ public class AccessibilityManagerServiceTest { @Mock private ProxyManager mProxyManager; @Mock private StatusBarManagerInternal mStatusBarManagerInternal; @Mock private DevicePolicyManager mDevicePolicyManager; + @Mock + private HearingDevicePhoneCallNotificationController + mMockHearingDevicePhoneCallNotificationController; @Spy private IUserInitializationCompleteCallback mUserInitializationCompleteCallback; @Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor; private IAccessibilityManager mA11yManagerServiceOnDevice; @@ -289,7 +292,8 @@ public class AccessibilityManagerServiceTest { mMockMagnificationController, mInputFilter, mProxyManager, - mFakePermissionEnforcer); + mFakePermissionEnforcer, + mMockHearingDevicePhoneCallNotificationController); mA11yms.switchUser(mTestableContext.getUserId()); mTestableLooper.processAllMessages(); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java new file mode 100644 index 000000000000..efea21428937 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java @@ -0,0 +1,187 @@ +/* + * 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.accessibility; + +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Application; +import android.app.Instrumentation; +import android.app.NotificationManager; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.media.AudioDeviceInfo; +import android.media.AudioDevicePort; +import android.media.AudioManager; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; + +import androidx.annotation.NonNull; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.messages.nano.SystemMessageProto; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Tests for the {@link HearingDevicePhoneCallNotificationController}. + */ +@RunWith(AndroidJUnit4.class) +public class HearingDevicePhoneCallNotificationControllerTest { + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + private static final String TEST_ADDRESS = "55:66:77:88:99:AA"; + + private final Application mApplication = ApplicationProvider.getApplicationContext(); + @Spy + private final Context mContext = mApplication.getApplicationContext(); + private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); + + @Mock + private TelephonyManager mTelephonyManager; + @Mock + private NotificationManager mNotificationManager; + @Mock + private AudioManager mAudioManager; + private HearingDevicePhoneCallNotificationController mController; + private TestCallStateListener mTestCallStateListener; + + @Before + public void setUp() { + mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED); + when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager); + when(mContext.getSystemService(NotificationManager.class)).thenReturn(mNotificationManager); + when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager); + + mTestCallStateListener = new TestCallStateListener(mContext); + mController = new HearingDevicePhoneCallNotificationController(mContext, + mTestCallStateListener); + mController.startListenForCallState(); + } + + @Test + public void startListenForCallState_callbackNotNull() { + Mockito.reset(mTelephonyManager); + mController = new HearingDevicePhoneCallNotificationController(mContext); + ArgumentCaptor<TelephonyCallback> listenerCaptor = ArgumentCaptor.forClass( + TelephonyCallback.class); + + mController.startListenForCallState(); + + verify(mTelephonyManager).registerTelephonyCallback(any(Executor.class), + listenerCaptor.capture()); + TelephonyCallback callback = listenerCaptor.getValue(); + assertThat(callback).isNotNull(); + } + + @Test + public void onCallStateChanged_stateOffHook_hapDevice_showNotification() { + AudioDeviceInfo hapDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS, + AudioManager.DEVICE_OUT_BLE_HEADSET); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[]{hapDeviceInfo}); + when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo)); + + mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK); + + verify(mNotificationManager).notify( + eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH), any()); + } + + @Test + public void onCallStateChanged_stateOffHook_a2dpDevice_noNotification() { + AudioDeviceInfo a2dpDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS, + AudioManager.DEVICE_OUT_BLUETOOTH_A2DP); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[]{a2dpDeviceInfo}); + when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(a2dpDeviceInfo)); + + mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK); + + verify(mNotificationManager, never()).notify( + eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH), any()); + } + + @Test + public void onCallStateChanged_stateOffHookThenIdle_hapDeviceInfo_cancelNotification() { + AudioDeviceInfo hapDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS, + AudioManager.DEVICE_OUT_BLE_HEADSET); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[]{hapDeviceInfo}); + when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo)); + + mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK); + mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_IDLE); + + verify(mNotificationManager).cancel( + eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH)); + } + + private AudioDeviceInfo createAudioDeviceInfo(String address, int type) { + AudioDevicePort audioDevicePort = mock(AudioDevicePort.class); + doReturn(type).when(audioDevicePort).type(); + doReturn(address).when(audioDevicePort).address(); + doReturn("testDevice").when(audioDevicePort).name(); + + return new AudioDeviceInfo(audioDevicePort); + } + + /** + * For easier testing for CallStateListener, override methods that contain final object. + */ + private static class TestCallStateListener extends + HearingDevicePhoneCallNotificationController.CallStateListener { + + TestCallStateListener(@NonNull Context context) { + super(context); + } + + @Override + boolean isHapClientSupported() { + return true; + } + + @Override + boolean isHapClientDevice(BluetoothAdapter bluetoothAdapter, AudioDeviceInfo info) { + return TEST_ADDRESS.equals(info.getAddress()); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS index b74281edbf52..c824c3948e2d 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS +++ b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS @@ -1 +1,3 @@ +# Bug component: 44215 + include /core/java/android/view/accessibility/OWNERS diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java index 0bf419ec242c..998c1d1a23b1 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -35,6 +36,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; +import android.content.pm.UserInfo; import android.os.Binder; import android.os.Bundle; import android.os.DropBoxManager; @@ -48,6 +50,7 @@ import android.os.Parcel; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; +import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.provider.DeviceConfig; import android.provider.Settings; @@ -68,6 +71,7 @@ import org.junit.Test; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -150,6 +154,25 @@ public class ActivityManagerTest { } @Test + public void testRemovedUserShouldNotBeRunning() throws Exception { + final UserManager userManager = mContext.getSystemService(UserManager.class); + assertNotNull("UserManager should not be null", userManager); + final UserInfo user = userManager.createUser( + "TestUser", UserManager.USER_TYPE_FULL_SECONDARY, 0); + + mService.startUserInBackground(user.id); + assertTrue("User should be running", mService.isUserRunning(user.id, 0)); + assertTrue("User should be in running users", + Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id)); + + userManager.removeUser(user.id); + mService.startUserInBackground(user.id); + assertFalse("Removed user should not be running", mService.isUserRunning(user.id, 0)); + assertFalse("Removed user should not be in running users", + Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id)); + } + + @Test public void testServiceUnbindAndKilling() { for (int i = TEST_LOOPS; i > 0; i--) { runOnce(i); diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 6411463fe0d9..06958b81d846 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -490,29 +490,6 @@ public class UserControllerTest { mInjector.mHandler.clearAllRecordedMessages(); // Verify that continueUserSwitch worked as expected continueAndCompleteUserSwitch(userState, oldUserId, newUserId); - verify(mInjector, times(0)).dismissKeyguard(any()); - verify(mInjector, times(1)).dismissUserSwitchingDialog(any()); - continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false); - verifySystemUserVisibilityChangesNeverNotified(); - } - - @Test - public void testContinueUserSwitchDismissKeyguard() { - when(mInjector.mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(false); - mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, - /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false, - /* backgroundUserScheduledStopTimeSecs= */ -1); - // Start user -- this will update state of mUserController - mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND); - Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); - assertNotNull(reportMsg); - UserState userState = (UserState) reportMsg.obj; - int oldUserId = reportMsg.arg1; - int newUserId = reportMsg.arg2; - mInjector.mHandler.clearAllRecordedMessages(); - // Verify that continueUserSwitch worked as expected - continueAndCompleteUserSwitch(userState, oldUserId, newUserId); - verify(mInjector, times(1)).dismissKeyguard(any()); verify(mInjector, times(1)).dismissUserSwitchingDialog(any()); continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false); verifySystemUserVisibilityChangesNeverNotified(); @@ -1923,11 +1900,6 @@ public class UserControllerTest { } @Override - protected void dismissKeyguard(Runnable runnable) { - runnable.run(); - } - - @Override void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser, String switchingFromSystemUserMessage, String switchingToSystemUserMessage, Runnable onShown) { diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java new file mode 100644 index 000000000000..84713079c9d3 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java @@ -0,0 +1,309 @@ +/* + * 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.appop; + +import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR; +import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER; +import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED; +import static android.app.AppOpsManager.UID_STATE_FOREGROUND; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import android.app.AppOpsManager; +import android.content.Context; +import android.os.Process; +import android.util.ArraySet; +import android.util.Log; +import android.util.LongSparseArray; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.appop.DiscreteOpsSqlRegistry.DiscreteOp; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class DiscreteAppOpSqlPersistenceTest { + private static final String DATABASE_NAME = "test_app_ops.db"; + private DiscreteOpsSqlRegistry mDiscreteRegistry; + private final Context mContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); + + @Before + public void setUp() { + mDiscreteRegistry = new DiscreteOpsSqlRegistry(mContext, + mContext.getDatabasePath(DATABASE_NAME)); + mDiscreteRegistry.systemReady(); + } + + @After + public void cleanUp() { + mContext.deleteDatabase(DATABASE_NAME); + } + + @Test + public void discreteOpEventIsRecorded() { + DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build(); + mDiscreteRegistry.recordDiscreteAccess(opEvent); + List<DiscreteOp> discreteOps = mDiscreteRegistry.getCachedDiscreteOps(); + assertThat(discreteOps.size()).isEqualTo(1); + assertThat(discreteOps).contains(opEvent); + } + + @Test + public void discreteOpEventIsPersistedToDisk() { + DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build(); + mDiscreteRegistry.recordDiscreteAccess(opEvent); + flushDiscreteOpsToDatabase(); + assertThat(mDiscreteRegistry.getCachedDiscreteOps()).isEmpty(); + List<DiscreteOp> discreteOps = mDiscreteRegistry.getAllDiscreteOps(); + assertThat(discreteOps.size()).isEqualTo(1); + assertThat(discreteOps).contains(opEvent); + } + + @Test + public void discreteOpEventInSameMinuteIsNotRecorded() { + long oneMinuteMillis = Duration.ofMinutes(1).toMillis(); + // round timestamp at minute level and add 5 seconds + long accessTime = System.currentTimeMillis() / oneMinuteMillis * oneMinuteMillis + 5000; + DiscreteOp opEvent = new DiscreteOpBuilder(mContext).setAccessTime(accessTime).build(); + mDiscreteRegistry.recordDiscreteAccess(opEvent); + // create duplicate event in same minute, with added 30 seconds + DiscreteOp opEvent2 = + new DiscreteOpBuilder(mContext).setAccessTime(accessTime + 30000).build(); + mDiscreteRegistry.recordDiscreteAccess(opEvent2); + List<DiscreteOp> discreteOps = mDiscreteRegistry.getAllDiscreteOps(); + + assertThat(discreteOps.size()).isEqualTo(1); + assertThat(discreteOps).contains(opEvent); + } + + @Test + public void multipleDiscreteOpEventAreRecorded() { + DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build(); + DiscreteOp opEvent2 = new DiscreteOpBuilder(mContext).setPackageName( + "test.package").build(); + mDiscreteRegistry.recordDiscreteAccess(opEvent); + mDiscreteRegistry.recordDiscreteAccess(opEvent2); + + List<DiscreteOp> discreteOps = mDiscreteRegistry.getAllDiscreteOps(); + assertThat(discreteOps).contains(opEvent); + assertThat(discreteOps).contains(opEvent2); + assertThat(discreteOps.size()).isEqualTo(2); + } + + @Test + public void clearDiscreteOps() { + DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build(); + mDiscreteRegistry.recordDiscreteAccess(opEvent); + flushDiscreteOpsToDatabase(); + DiscreteOp opEvent2 = new DiscreteOpBuilder(mContext).setUid(12345).setPackageName( + "abc").build(); + mDiscreteRegistry.recordDiscreteAccess(opEvent2); + mDiscreteRegistry.clearHistory(); + assertThat(mDiscreteRegistry.getAllDiscreteOps()).isEmpty(); + } + + @Test + public void clearDiscreteOpsForPackage() { + DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build(); + mDiscreteRegistry.recordDiscreteAccess(opEvent); + flushDiscreteOpsToDatabase(); + mDiscreteRegistry.recordDiscreteAccess(new DiscreteOpBuilder(mContext).build()); + mDiscreteRegistry.clearHistory(Process.myUid(), mContext.getPackageName()); + + assertThat(mDiscreteRegistry.getAllDiscreteOps()).isEmpty(); + } + + @Test + public void offsetDiscreteOps() { + DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build(); + long event2AccessTime = System.currentTimeMillis() - 300000; + DiscreteOp opEvent2 = new DiscreteOpBuilder(mContext).setAccessTime( + event2AccessTime).build(); + mDiscreteRegistry.recordDiscreteAccess(opEvent); + flushDiscreteOpsToDatabase(); + mDiscreteRegistry.recordDiscreteAccess(opEvent2); + long offset = Duration.ofMinutes(2).toMillis(); + + mDiscreteRegistry.offsetHistory(offset); + + // adjust input for assertion + DiscreteOp e1 = new DiscreteOpBuilder(opEvent) + .setAccessTime(opEvent.getAccessTime() - offset).build(); + DiscreteOp e2 = new DiscreteOpBuilder(opEvent2) + .setAccessTime(event2AccessTime - offset).build(); + + List<DiscreteOp> results = mDiscreteRegistry.getAllDiscreteOps(); + assertThat(results.size()).isEqualTo(2); + assertThat(results).contains(e1); + assertThat(results).contains(e2); + } + + @Test + public void completeAttributionChain() { + long chainId = 100; + DiscreteOp event1 = new DiscreteOpBuilder(mContext) + .setChainId(chainId) + .setAttributionFlags(ATTRIBUTION_FLAG_RECEIVER | ATTRIBUTION_FLAG_TRUSTED) + .build(); + DiscreteOp event2 = new DiscreteOpBuilder(mContext) + .setChainId(chainId) + .setAttributionFlags(ATTRIBUTION_FLAG_ACCESSOR | ATTRIBUTION_FLAG_TRUSTED) + .build(); + List<DiscreteOp> events = new ArrayList<>(); + events.add(event1); + events.add(event2); + + LongSparseArray<DiscreteOpsSqlRegistry.AttributionChain> chains = + mDiscreteRegistry.createAttributionChains(events, new ArraySet<>()); + + assertThat(chains.size()).isGreaterThan(0); + DiscreteOpsSqlRegistry.AttributionChain chain = chains.get(chainId); + assertThat(chain).isNotNull(); + assertThat(chain.isComplete()).isTrue(); + assertThat(chain.getStart()).isEqualTo(event1); + assertThat(chain.getLastVisible()).isEqualTo(event2); + } + + @Test + public void addToHistoricalOps() { + long beginTimeMillis = System.currentTimeMillis(); + DiscreteOp event1 = new DiscreteOpBuilder(mContext) + .build(); + DiscreteOp event2 = new DiscreteOpBuilder(mContext) + .setUid(123457) + .build(); + mDiscreteRegistry.recordDiscreteAccess(event1); + flushDiscreteOpsToDatabase(); + mDiscreteRegistry.recordDiscreteAccess(event2); + + long endTimeMillis = System.currentTimeMillis() + 500; + AppOpsManager.HistoricalOps results = new AppOpsManager.HistoricalOps(beginTimeMillis, + endTimeMillis); + + mDiscreteRegistry.addFilteredDiscreteOpsToHistoricalOps(results, beginTimeMillis, + endTimeMillis, 0, 0, null, null, null, 0, new ArraySet<>()); + Log.i("Manjeet", "TEST read " + results); + assertWithMessage("results shouldn't be empty").that(results.isEmpty()).isFalse(); + } + + @Test + public void dump() { + DiscreteOp event1 = new DiscreteOpBuilder(mContext) + .setAccessTime(1732221340628L) + .setUid(12345) + .build(); + DiscreteOp event2 = new DiscreteOpBuilder(mContext) + .setAccessTime(1732227340628L) + .setUid(123457) + .build(); + mDiscreteRegistry.recordDiscreteAccess(event1); + flushDiscreteOpsToDatabase(); + mDiscreteRegistry.recordDiscreteAccess(event2); + } + + /** This clears in-memory cache and push records into the database. */ + private void flushDiscreteOpsToDatabase() { + mDiscreteRegistry.writeAndClearOldAccessHistory(); + } + + /** + * Creates default op event for CAMERA app op with current time as access time + * and 1 minute duration + */ + private static class DiscreteOpBuilder { + private int mUid; + private String mPackageName; + private String mAttributionTag; + private String mDeviceId; + private int mOpCode; + private int mOpFlags; + private int mAttributionFlags; + private int mUidState; + private long mChainId; + private long mAccessTime; + private long mDuration; + + DiscreteOpBuilder(Context context) { + mUid = Process.myUid(); + mPackageName = context.getPackageName(); + mAttributionTag = null; + mDeviceId = String.valueOf(context.getDeviceId()); + mOpCode = AppOpsManager.OP_CAMERA; + mOpFlags = AppOpsManager.OP_FLAG_SELF; + mAttributionFlags = ATTRIBUTION_FLAG_ACCESSOR; + mUidState = UID_STATE_FOREGROUND; + mChainId = AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE; + mAccessTime = System.currentTimeMillis(); + mDuration = Duration.ofMinutes(1).toMillis(); + } + + DiscreteOpBuilder(DiscreteOp discreteOp) { + this.mUid = discreteOp.getUid(); + this.mPackageName = discreteOp.getPackageName(); + this.mAttributionTag = discreteOp.getAttributionTag(); + this.mDeviceId = discreteOp.getDeviceId(); + this.mOpCode = discreteOp.getOpCode(); + this.mOpFlags = discreteOp.getOpFlags(); + this.mAttributionFlags = discreteOp.getAttributionFlags(); + this.mUidState = discreteOp.getUidState(); + this.mChainId = discreteOp.getChainId(); + this.mAccessTime = discreteOp.getAccessTime(); + this.mDuration = discreteOp.getDuration(); + } + + public DiscreteOpBuilder setUid(int uid) { + this.mUid = uid; + return this; + } + + public DiscreteOpBuilder setPackageName(String packageName) { + this.mPackageName = packageName; + return this; + } + + public DiscreteOpBuilder setAttributionFlags(int attributionFlags) { + this.mAttributionFlags = attributionFlags; + return this; + } + + public DiscreteOpBuilder setChainId(long chainId) { + this.mChainId = chainId; + return this; + } + + public DiscreteOpBuilder setAccessTime(long accessTime) { + this.mAccessTime = accessTime; + return this; + } + + public DiscreteOp build() { + return new DiscreteOp(mUid, mPackageName, mAttributionTag, mDeviceId, mOpCode, mOpFlags, + mAttributionFlags, mUidState, mChainId, mAccessTime, mDuration); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java index 2ff0c6288ece..ae973be17904 100644 --- a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpPersistenceTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java @@ -47,9 +47,12 @@ import org.junit.runner.RunWith; import java.io.File; import java.util.List; +/** + * Test xml persistence implementation for discrete ops. + */ @RunWith(AndroidJUnit4.class) -public class DiscreteAppOpPersistenceTest { - private DiscreteRegistry mDiscreteRegistry; +public class DiscreteAppOpXmlPersistenceTest { + private DiscreteOpsXmlRegistry mDiscreteRegistry; private final Object mLock = new Object(); private File mMockDataDirectory; private final Context mContext = @@ -61,13 +64,13 @@ public class DiscreteAppOpPersistenceTest { @Before public void setUp() { mMockDataDirectory = mContext.getDir("mock_data", Context.MODE_PRIVATE); - mDiscreteRegistry = new DiscreteRegistry(mLock, mMockDataDirectory); + mDiscreteRegistry = new DiscreteOpsXmlRegistry(mLock, mMockDataDirectory); mDiscreteRegistry.systemReady(); } @After public void cleanUp() { - mDiscreteRegistry.writeAndClearAccessHistory(); + mDiscreteRegistry.writeAndClearOldAccessHistory(); FileUtils.deleteContents(mMockDataDirectory); } @@ -87,14 +90,14 @@ public class DiscreteAppOpPersistenceTest { mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags, uidState, accessTime, duration, attributionFlags, attributionChainId, - DiscreteRegistry.ACCESS_TYPE_FINISH_OP); + DiscreteOpsXmlRegistry.ACCESS_TYPE_FINISH_OP); // Verify in-memory object is correct fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime, duration, uidState, opFlags, attributionFlags, attributionChainId); // Write to disk and clear the in-memory object - mDiscreteRegistry.writeAndClearAccessHistory(); + mDiscreteRegistry.writeAndClearOldAccessHistory(); // Verify the storage file is created and then verify its content is correct File[] files = FileUtils.listFilesOrEmpty(mMockDataDirectory); @@ -119,12 +122,12 @@ public class DiscreteAppOpPersistenceTest { mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags, uidState, accessTime, duration, attributionFlags, attributionChainId, - DiscreteRegistry.ACCESS_TYPE_START_OP); + DiscreteOpsXmlRegistry.ACCESS_TYPE_START_OP); fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime, duration, uidState, opFlags, attributionFlags, attributionChainId); - mDiscreteRegistry.writeAndClearAccessHistory(); + mDiscreteRegistry.writeAndClearOldAccessHistory(); File[] files = FileUtils.listFilesOrEmpty(mMockDataDirectory); assertThat(files.length).isEqualTo(1); @@ -136,30 +139,31 @@ public class DiscreteAppOpPersistenceTest { int expectedOp, String expectedDeviceId, String expectedAttrTag, long expectedAccessTime, long expectedAccessDuration, int expectedUidState, int expectedOpFlags, int expectedAttrFlags, int expectedAttrChainId) { - DiscreteRegistry.DiscreteOps discreteOps = mDiscreteRegistry.getAllDiscreteOps(); + DiscreteOpsXmlRegistry.DiscreteOps discreteOps = mDiscreteRegistry.getAllDiscreteOps(); assertThat(discreteOps.isEmpty()).isFalse(); assertThat(discreteOps.mUids.size()).isEqualTo(1); - DiscreteRegistry.DiscreteUidOps discreteUidOps = discreteOps.mUids.get(expectedUid); + DiscreteOpsXmlRegistry.DiscreteUidOps discreteUidOps = discreteOps.mUids.get(expectedUid); assertThat(discreteUidOps.mPackages.size()).isEqualTo(1); - DiscreteRegistry.DiscretePackageOps discretePackageOps = + DiscreteOpsXmlRegistry.DiscretePackageOps discretePackageOps = discreteUidOps.mPackages.get(expectedPackageName); assertThat(discretePackageOps.mPackageOps.size()).isEqualTo(1); - DiscreteRegistry.DiscreteOp discreteOp = discretePackageOps.mPackageOps.get(expectedOp); + DiscreteOpsXmlRegistry.DiscreteOp discreteOp = + discretePackageOps.mPackageOps.get(expectedOp); assertThat(discreteOp.mDeviceAttributedOps.size()).isEqualTo(1); - DiscreteRegistry.DiscreteDeviceOp discreteDeviceOp = + DiscreteOpsXmlRegistry.DiscreteDeviceOp discreteDeviceOp = discreteOp.mDeviceAttributedOps.get(expectedDeviceId); assertThat(discreteDeviceOp.mAttributedOps.size()).isEqualTo(1); - List<DiscreteRegistry.DiscreteOpEvent> discreteOpEvents = + List<DiscreteOpsXmlRegistry.DiscreteOpEvent> discreteOpEvents = discreteDeviceOp.mAttributedOps.get(expectedAttrTag); assertThat(discreteOpEvents.size()).isEqualTo(1); - DiscreteRegistry.DiscreteOpEvent discreteOpEvent = discreteOpEvents.get(0); + DiscreteOpsXmlRegistry.DiscreteOpEvent discreteOpEvent = discreteOpEvents.get(0); assertThat(discreteOpEvent.mNoteTime).isEqualTo(expectedAccessTime); assertThat(discreteOpEvent.mNoteDuration).isEqualTo(expectedAccessDuration); assertThat(discreteOpEvent.mUidState).isEqualTo(expectedUidState); diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java new file mode 100644 index 000000000000..21cc3bac3938 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java @@ -0,0 +1,168 @@ +/* + * 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.appop; + +import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR; +import static android.app.AppOpsManager.UID_STATE_FOREGROUND; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.AppOpsManager; +import android.companion.virtual.VirtualDeviceManager; +import android.content.Context; +import android.os.FileUtils; +import android.os.Process; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.time.Duration; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class DiscreteOpsMigrationAndRollbackTest { + private final Context mContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); + private static final String DATABASE_NAME = "test_app_ops.db"; + private static final int RECORD_COUNT = 500; + private final File mMockDataDirectory = mContext.getDir("mock_data", Context.MODE_PRIVATE); + final Object mLock = new Object(); + + @After + @Before + public void clean() { + mContext.deleteDatabase(DATABASE_NAME); + FileUtils.deleteContents(mMockDataDirectory); + } + + @Test + public void migrateFromXmlToSqlite() { + // write records to xml registry + DiscreteOpsXmlRegistry xmlRegistry = new DiscreteOpsXmlRegistry(mLock, mMockDataDirectory); + xmlRegistry.systemReady(); + for (int i = 1; i <= RECORD_COUNT; i++) { + DiscreteOpsSqlRegistry.DiscreteOp opEvent = + new DiscreteOpBuilder(mContext) + .setChainId(i) + .setUid(10000 + i) // make all records unique + .build(); + xmlRegistry.recordDiscreteAccess(opEvent.getUid(), opEvent.getPackageName(), + opEvent.getDeviceId(), opEvent.getOpCode(), opEvent.getAttributionTag(), + opEvent.getOpFlags(), opEvent.getUidState(), opEvent.getAccessTime(), + opEvent.getDuration(), opEvent.getAttributionFlags(), + (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP); + } + xmlRegistry.writeAndClearOldAccessHistory(); + assertThat(xmlRegistry.readLargestChainIdFromDiskLocked()).isEqualTo(RECORD_COUNT); + assertThat(xmlRegistry.getAllDiscreteOps().mUids.size()).isEqualTo(RECORD_COUNT); + + // migration to sql registry + DiscreteOpsSqlRegistry sqlRegistry = new DiscreteOpsSqlRegistry(mContext, + mContext.getDatabasePath(DATABASE_NAME)); + sqlRegistry.systemReady(); + DiscreteOpsMigrationHelper.migrateDiscreteOpsToSqlite(xmlRegistry, sqlRegistry); + List<DiscreteOpsSqlRegistry.DiscreteOp> sqlOps = sqlRegistry.getAllDiscreteOps(); + + assertThat(xmlRegistry.getAllDiscreteOps().mUids).isEmpty(); + assertThat(sqlOps.size()).isEqualTo(RECORD_COUNT); + assertThat(sqlRegistry.getLargestAttributionChainId()).isEqualTo(RECORD_COUNT); + } + + @Test + public void migrateFromSqliteToXml() { + // write to sql registry + DiscreteOpsSqlRegistry sqlRegistry = new DiscreteOpsSqlRegistry(mContext, + mContext.getDatabasePath(DATABASE_NAME)); + sqlRegistry.systemReady(); + for (int i = 1; i <= RECORD_COUNT; i++) { + DiscreteOpsSqlRegistry.DiscreteOp opEvent = + new DiscreteOpBuilder(mContext) + .setChainId(i) + .setUid(RECORD_COUNT + i) // make all records unique + .build(); + sqlRegistry.recordDiscreteAccess(opEvent.getUid(), opEvent.getPackageName(), + opEvent.getDeviceId(), opEvent.getOpCode(), opEvent.getAttributionTag(), + opEvent.getOpFlags(), opEvent.getUidState(), opEvent.getAccessTime(), + opEvent.getDuration(), opEvent.getAttributionFlags(), + (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP); + } + sqlRegistry.writeAndClearOldAccessHistory(); + assertThat(sqlRegistry.getAllDiscreteOps().size()).isEqualTo(RECORD_COUNT); + assertThat(sqlRegistry.getLargestAttributionChainId()).isEqualTo(RECORD_COUNT); + + // migration to xml registry + DiscreteOpsXmlRegistry xmlRegistry = new DiscreteOpsXmlRegistry(mLock, mMockDataDirectory); + xmlRegistry.systemReady(); + DiscreteOpsMigrationHelper.migrateDiscreteOpsToXml(sqlRegistry, xmlRegistry); + DiscreteOpsXmlRegistry.DiscreteOps xmlOps = xmlRegistry.getAllDiscreteOps(); + + assertThat(sqlRegistry.getAllDiscreteOps()).isEmpty(); + assertThat(xmlOps.mLargestChainId).isEqualTo(RECORD_COUNT); + assertThat(xmlOps.mUids.size()).isEqualTo(RECORD_COUNT); + } + + private static class DiscreteOpBuilder { + private int mUid; + private String mPackageName; + private String mAttributionTag; + private String mDeviceId; + private int mOpCode; + private int mOpFlags; + private int mAttributionFlags; + private int mUidState; + private int mChainId; + private long mAccessTime; + private long mDuration; + + DiscreteOpBuilder(Context context) { + mUid = Process.myUid(); + mPackageName = context.getPackageName(); + mAttributionTag = null; + mDeviceId = VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT; + mOpCode = AppOpsManager.OP_CAMERA; + mOpFlags = AppOpsManager.OP_FLAG_SELF; + mAttributionFlags = ATTRIBUTION_FLAG_ACCESSOR; + mUidState = UID_STATE_FOREGROUND; + mChainId = AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE; + mAccessTime = System.currentTimeMillis(); + mDuration = Duration.ofMinutes(1).toMillis(); + } + + public DiscreteOpBuilder setUid(int uid) { + this.mUid = uid; + return this; + } + + public DiscreteOpBuilder setChainId(int chainId) { + this.mChainId = chainId; + return this; + } + + public DiscreteOpsSqlRegistry.DiscreteOp build() { + return new DiscreteOpsSqlRegistry.DiscreteOp(mUid, mPackageName, mAttributionTag, + mDeviceId, + mOpCode, mOpFlags, mAttributionFlags, mUidState, mChainId, mAccessTime, + mDuration); + } + } +} 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 32578a7dc10f..bdbb495db841 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 @@ -340,8 +340,7 @@ public class VirtualDeviceManagerServiceTest { LocalServices.removeServiceForTest(DisplayManagerInternal.class); LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); - doNothing().when(mInputManagerInternalMock) - .setMousePointerAccelerationEnabled(anyBoolean(), anyInt()); + doNothing().when(mInputManagerInternalMock).setMouseScalingEnabled(anyBoolean(), anyInt()); doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt()); LocalServices.removeServiceForTest(InputManagerInternal.class); LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock); diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt index 1352adef783f..ad6e467efeef 100644 --- a/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt +++ b/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt @@ -76,12 +76,10 @@ class OverlayReferenceMapperTests { val overlay1 = mockOverlay(1) mapper = mapper( overlayToTargetToOverlayables = mapOf( - overlay0.packageName to mapOf( - target.packageName to target.overlayables.keys - ), - overlay1.packageName to mapOf( - target.packageName to target.overlayables.keys - ) + overlay0.packageName to android.util.Pair(target.packageName, + target.overlayables.keys.first()), + overlay1.packageName to android.util.Pair(target.packageName, + target.overlayables.keys.first()) ) ) val existing = mapper.addInOrder(overlay0, overlay1) { @@ -134,42 +132,38 @@ class OverlayReferenceMapperTests { } @Test - fun overlayWithMultipleTargets() { - val target0 = mockTarget(0) - val target1 = mockTarget(1) + fun overlayWithoutTarget() { val overlay = mockOverlay() - mapper = mapper( - overlayToTargetToOverlayables = mapOf( - overlay.packageName to mapOf( - target0.packageName to target0.overlayables.keys, - target1.packageName to target1.overlayables.keys - ) - ) - ) - mapper.addInOrder(target0, target1, overlay) { - assertThat(it).containsExactly(ACTOR_PACKAGE_NAME) - } - assertMapping(ACTOR_PACKAGE_NAME to setOf(target0, target1, overlay)) - mapper.remove(target0) { - assertThat(it).containsExactly(ACTOR_PACKAGE_NAME) + mapper.addInOrder(overlay) { + assertThat(it).isEmpty() } - assertMapping(ACTOR_PACKAGE_NAME to setOf(target1, overlay)) - mapper.remove(target1) { - assertThat(it).containsExactly(ACTOR_PACKAGE_NAME) + // An overlay can only have visibility exposed through its target + assertEmpty() + mapper.remove(overlay) { + assertThat(it).isEmpty() } assertEmpty() } @Test - fun overlayWithoutTarget() { + fun targetWithNullOverlayable() { + val target = mockTarget() val overlay = mockOverlay() - mapper.addInOrder(overlay) { + mapper = mapper( + overlayToTargetToOverlayables = mapOf( + overlay.packageName to android.util.Pair(target.packageName, null) + ) + ) + val existing = mapper.addInOrder(overlay) { assertThat(it).isEmpty() } - // An overlay can only have visibility exposed through its target assertEmpty() - mapper.remove(overlay) { - assertThat(it).isEmpty() + mapper.addInOrder(target, existing = existing) { + assertThat(it).containsExactly(ACTOR_PACKAGE_NAME) + } + assertMapping(ACTOR_PACKAGE_NAME to setOf(target)) + mapper.remove(target) { + assertThat(it).containsExactly(ACTOR_PACKAGE_NAME) } assertEmpty() } @@ -219,17 +213,15 @@ class OverlayReferenceMapperTests { namedActors: Map<String, Map<String, String>> = Uri.parse(ACTOR_NAME).run { mapOf(authority!! to mapOf(pathSegments.first() to ACTOR_PACKAGE_NAME)) }, - overlayToTargetToOverlayables: Map<String, Map<String, Set<String>>> = mapOf( - mockOverlay().packageName to mapOf( - mockTarget().run { packageName to overlayables.keys } - ) - ) + overlayToTargetToOverlayables: Map<String, android.util.Pair<String, String>> = mapOf( + mockOverlay().packageName to mockTarget().run { android.util.Pair(packageName!!, + overlayables.keys.first()) }) ) = OverlayReferenceMapper(deferRebuild, object : OverlayReferenceMapper.Provider { override fun getActorPkg(actor: String) = OverlayActorEnforcer.getPackageNameForActor(actor, namedActors).first override fun getTargetToOverlayables(pkg: AndroidPackage) = - overlayToTargetToOverlayables[pkg.packageName] ?: emptyMap() + overlayToTargetToOverlayables[pkg.packageName] }) private fun mockTarget(increment: Int = 0) = mockThrowOnUnmocked<AndroidPackage> { diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 4e030d499c25..3ef360a752f6 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -112,6 +112,7 @@ import org.mockito.stubbing.Answer; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; @@ -556,6 +557,12 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { } @Override + void injectFinishWrite(@NonNull ResilientAtomicFile file, + @NonNull FileOutputStream os) throws IOException { + file.finishWrite(os, false /* doFsVerity */); + } + + @Override void wtf(String message, Throwable th) { // During tests, WTF is fatal. fail(message + " exception: " + th + "\n" + Log.getStackTraceString(th)); diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index c01283a236c4..776f05dfb6ea 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -159,7 +159,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /** * Test for the first launch path, no settings file available. */ - public void FirstInitialize() { + public void testFirstInitialize() { assertResetTimes(START_TIME, START_TIME + INTERVAL); } @@ -167,7 +167,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { * Test for {@link ShortcutService#getLastResetTimeLocked()} and * {@link ShortcutService#getNextResetTimeLocked()}. */ - public void UpdateAndGetNextResetTimeLocked() { + public void testUpdateAndGetNextResetTimeLocked() { assertResetTimes(START_TIME, START_TIME + INTERVAL); // Advance clock. @@ -196,7 +196,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /** * Test for the restoration from saved file. */ - public void InitializeFromSavedFile() { + public void testInitializeFromSavedFile() { mInjectedCurrentTimeMillis = START_TIME + 4 * INTERVAL + 50; assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL); @@ -220,7 +220,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // TODO Add various broken cases. } - public void LoadConfig() { + public void testLoadConfig() { mService.updateConfigurationLocked( ConfigConstants.KEY_RESET_INTERVAL_SEC + "=123," + ConfigConstants.KEY_MAX_SHORTCUTS + "=4," @@ -261,22 +261,22 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // === Test for app side APIs === /** Test for {@link android.content.pm.ShortcutManager#getMaxShortcutCountForActivity()} */ - public void GetMaxDynamicShortcutCount() { + public void testGetMaxDynamicShortcutCount() { assertEquals(MAX_SHORTCUTS, mManager.getMaxShortcutCountForActivity()); } /** Test for {@link android.content.pm.ShortcutManager#getRemainingCallCount()} */ - public void GetRemainingCallCount() { + public void testGetRemainingCallCount() { assertEquals(MAX_UPDATES_PER_INTERVAL, mManager.getRemainingCallCount()); } - public void GetIconMaxDimensions() { + public void testGetIconMaxDimensions() { assertEquals(MAX_ICON_DIMENSION, mManager.getIconMaxWidth()); assertEquals(MAX_ICON_DIMENSION, mManager.getIconMaxHeight()); } /** Test for {@link android.content.pm.ShortcutManager#getRateLimitResetTime()} */ - public void GetRateLimitResetTime() { + public void testGetRateLimitResetTime() { assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime()); mInjectedCurrentTimeMillis = START_TIME + 4 * INTERVAL + 50; @@ -284,7 +284,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(START_TIME + 5 * INTERVAL, mManager.getRateLimitResetTime()); } - public void SetDynamicShortcuts() { + public void testSetDynamicShortcuts() { setCaller(CALLING_PACKAGE_1, USER_10); final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.icon1); @@ -354,7 +354,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void AddDynamicShortcuts() { + public void testAddDynamicShortcuts() { setCaller(CALLING_PACKAGE_1, USER_10); final ShortcutInfo si1 = makeShortcut("shortcut1"); @@ -402,7 +402,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void PushDynamicShortcut() { + public void disabled_testPushDynamicShortcut() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5," + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=1"); @@ -544,7 +544,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { eq(CALLING_PACKAGE_1), eq("s9"), eq(USER_10)); } - public void PushDynamicShortcut_CallsToUsageStatsManagerAreThrottled() + public void disabled_testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled() throws InterruptedException { mService.updateConfigurationLocked( ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=500"); @@ -576,6 +576,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { Mockito.reset(mMockUsageStatsManagerInternal); for (int i = 2; i <= 10; i++) { final ShortcutInfo si = makeShortcut("s" + i); + setCaller(CALLING_PACKAGE_2, USER_10); mManager.pushDynamicShortcut(si); } verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage( @@ -595,7 +596,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { eq(CALLING_PACKAGE_2), any(), eq(USER_10)); } - public void UnlimitedCalls() { + public void testUnlimitedCalls() { setCaller(CALLING_PACKAGE_1, USER_10); final ShortcutInfo si1 = makeShortcut("shortcut1"); @@ -626,7 +627,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(3, mManager.getRemainingCallCount()); } - public void PublishWithNoActivity() { + public void disbabledTestPublishWithNoActivity() { // If activity is not explicitly set, use the default one. mRunningUsers.put(USER_11, true); @@ -732,7 +733,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void PublishWithNoActivity_noMainActivityInPackage() { + public void disabled_testPublishWithNoActivity_noMainActivityInPackage() { mRunningUsers.put(USER_11, true); runWithCaller(CALLING_PACKAGE_2, USER_11, () -> { @@ -751,7 +752,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void DeleteDynamicShortcuts() { + public void testDeleteDynamicShortcuts() { final ShortcutInfo si1 = makeShortcut("shortcut1"); final ShortcutInfo si2 = makeShortcut("shortcut2"); final ShortcutInfo si3 = makeShortcut("shortcut3"); @@ -792,7 +793,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(2, mManager.getRemainingCallCount()); } - public void DeleteAllDynamicShortcuts() { + public void testDeleteAllDynamicShortcuts() { final ShortcutInfo si1 = makeShortcut("shortcut1"); final ShortcutInfo si2 = makeShortcut("shortcut2"); final ShortcutInfo si3 = makeShortcut("shortcut3"); @@ -821,7 +822,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(1, mManager.getRemainingCallCount()); } - public void Icons() throws IOException { + public void testIcons() throws IOException { final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32); final Icon res64x64 = Icon.createWithResource(getTestContext(), R.drawable.black_64x64); final Icon res512x512 = Icon.createWithResource(getTestContext(), R.drawable.black_512x512); @@ -1035,7 +1036,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { */ } - public void CleanupDanglingBitmaps() throws Exception { + public void testCleanupDanglingBitmaps() throws Exception { assertBitmapDirectories(USER_10, EMPTY_STRINGS); assertBitmapDirectories(USER_11, EMPTY_STRINGS); @@ -1204,7 +1205,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { maxSize)); } - public void ShrinkBitmap() { + public void testShrinkBitmap() { checkShrinkBitmap(32, 32, R.drawable.black_512x512, 32); checkShrinkBitmap(511, 511, R.drawable.black_512x512, 511); checkShrinkBitmap(512, 512, R.drawable.black_512x512, 512); @@ -1227,7 +1228,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { return out.getFile(); } - public void OpenIconFileForWrite() throws IOException { + public void testOpenIconFileForWrite() throws IOException { mInjectedCurrentTimeMillis = 1000; final File p10_1_1 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1); @@ -1301,7 +1302,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertFalse(p11_1_3.getName().contains("_")); } - public void UpdateShortcuts() { + public void testUpdateShortcuts() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( makeShortcut("s1"), @@ -1432,7 +1433,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void UpdateShortcuts_icons() { + public void testUpdateShortcuts_icons() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( makeShortcut("s1") @@ -1526,7 +1527,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void ShortcutManagerGetShortcuts_shortcutTypes() { + public void testShortcutManagerGetShortcuts_shortcutTypes() { // Create 3 manifest and 3 dynamic shortcuts addManifestShortcutResource( @@ -1617,7 +1618,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertShortcutIds(mManager.getShortcuts(ShortcutManager.FLAG_MATCH_CACHED), "s1", "s2"); } - public void CachedShortcuts() { + public void testCachedShortcuts() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"), makeLongLivedShortcut("s2"), makeLongLivedShortcut("s3"), @@ -1701,7 +1702,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { "s2"); } - public void CachedShortcuts_accessShortcutsPermission() { + public void testCachedShortcuts_accessShortcutsPermission() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"), makeLongLivedShortcut("s2"), makeLongLivedShortcut("s3"), @@ -1743,7 +1744,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertShortcutIds(mManager.getShortcuts(ShortcutManager.FLAG_MATCH_CACHED), "s3"); } - public void CachedShortcuts_canPassShortcutLimit() { + public void testCachedShortcuts_canPassShortcutLimit() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=4"); @@ -1781,7 +1782,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // === Test for launcher side APIs === - public void GetShortcuts() { + public void testGetShortcuts() { // Set up shortcuts. @@ -1998,7 +1999,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { "s1", "s3"); } - public void GetShortcuts_shortcutKinds() throws Exception { + public void testGetShortcuts_shortcutKinds() throws Exception { // Create 3 manifest and 3 dynamic shortcuts addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), @@ -2109,7 +2110,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void GetShortcuts_resolveStrings() throws Exception { + public void testGetShortcuts_resolveStrings() throws Exception { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { ShortcutInfo si = new ShortcutInfo.Builder(mClientContext) .setId("id") @@ -2157,7 +2158,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void GetShortcuts_personsFlag() { + public void testGetShortcuts_personsFlag() { ShortcutInfo s = new ShortcutInfo.Builder(mClientContext, "id") .setShortLabel("label") .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class)) @@ -2205,7 +2206,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { } // TODO resource - public void GetShortcutInfo() { + public void testGetShortcutInfo() { // Create shortcuts. setCaller(CALLING_PACKAGE_1); final ShortcutInfo s1_1 = makeShortcut( @@ -2280,7 +2281,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals("ABC", findById(list, "s1").getTitle()); } - public void PinShortcutAndGetPinnedShortcuts() { + public void testPinShortcutAndGetPinnedShortcuts() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000); final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000); @@ -2361,7 +2362,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { * This is similar to the above test, except it used "disable" instead of "remove". It also * does "enable". */ - public void DisableAndEnableShortcuts() { + public void testDisableAndEnableShortcuts() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000); final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000); @@ -2486,7 +2487,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void DisableShortcuts_thenRepublish() { + public void testDisableShortcuts_thenRepublish() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3")))); @@ -2556,7 +2557,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void PinShortcutAndGetPinnedShortcuts_multi() { + public void disabled_testPinShortcutAndGetPinnedShortcuts_multi() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( @@ -2832,7 +2833,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void PinShortcutAndGetPinnedShortcuts_assistant() { + public void testPinShortcutAndGetPinnedShortcuts_assistant() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( @@ -2888,7 +2889,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void PinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() { + public void disabled_testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( @@ -3477,7 +3478,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void StartShortcut() { + public void testStartShortcut() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final ShortcutInfo s1_1 = makeShortcut( @@ -3612,7 +3613,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // TODO Check extra, etc } - public void LauncherCallback() throws Throwable { + public void testLauncherCallback() throws Throwable { // Disable throttling for this test. mService.updateConfigurationLocked( ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL + "=99999999," @@ -3778,7 +3779,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { .isEmpty(); } - public void LauncherCallback_crossProfile() throws Throwable { + public void testLauncherCallback_crossProfile() throws Throwable { prepareCrossProfileDataSet(); final Handler h = new Handler(Looper.getMainLooper()); @@ -3901,7 +3902,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // === Test for persisting === - public void SaveAndLoadUser_empty() { + public void testSaveAndLoadUser_empty() { assertTrue(mManager.setDynamicShortcuts(list())); Log.i(TAG, "Saved state"); @@ -3918,7 +3919,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /** * Try save and load, also stop/start the user. */ - public void SaveAndLoadUser() { + public void disabled_testSaveAndLoadUser() { // First, create some shortcuts and save. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x16); @@ -4059,7 +4060,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // TODO Check all other fields } - public void LoadCorruptedShortcuts() throws Exception { + public void testLoadCorruptedShortcuts() throws Exception { initService(); addPackage("com.android.chrome", 0, 0); @@ -4073,7 +4074,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertNull(ShortcutPackage.loadFromFile(mService, user, corruptedShortcutPackage, false)); } - public void SaveCorruptAndLoadUser() throws Exception { + public void testSaveCorruptAndLoadUser() throws Exception { // First, create some shortcuts and save. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x16); @@ -4229,7 +4230,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // TODO Check all other fields } - public void CleanupPackage() { + public void testCleanupPackage() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( makeShortcut("s0_1")))); @@ -4506,7 +4507,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { mService.saveDirtyInfo(); } - public void CleanupPackage_republishManifests() { + public void testCleanupPackage_republishManifests() { addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), R.xml.shortcut_2); @@ -4574,7 +4575,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void HandleGonePackage_crossProfile() { + public void testHandleGonePackage_crossProfile() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( @@ -4846,7 +4847,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(expected, spi.canRestoreTo(mService, pi, true)); } - public void CanRestoreTo() { + public void testCanRestoreTo() { addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 10, "sig1"); addPackage(CALLING_PACKAGE_2, CALLING_UID_2, 10, "sig1", "sig2"); addPackage(CALLING_PACKAGE_3, CALLING_UID_3, 10, "sig1"); @@ -4909,7 +4910,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { checkCanRestoreTo(DISABLED_REASON_BACKUP_NOT_SUPPORTED, spi3, true, 10, true, "sig1"); } - public void HandlePackageDelete() { + public void testHandlePackageDelete() { checkHandlePackageDeleteInner((userId, packageName) -> { uninstallPackage(userId, packageName); mService.mPackageMonitor.onReceive(getTestContext(), @@ -4917,7 +4918,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void HandlePackageDisable() { + public void testHandlePackageDisable() { checkHandlePackageDeleteInner((userId, packageName) -> { disablePackage(userId, packageName); mService.mPackageMonitor.onReceive(getTestContext(), @@ -5049,7 +5050,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { } /** Almost ame as testHandlePackageDelete, except it doesn't uninstall packages. */ - public void HandlePackageClearData() { + public void testHandlePackageClearData() { final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource( getTestContext().getResources(), R.drawable.black_32x32)); setCaller(CALLING_PACKAGE_1, USER_10); @@ -5125,7 +5126,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_11)); } - public void HandlePackageClearData_manifestRepublished() { + public void testHandlePackageClearData_manifestRepublished() { mRunningUsers.put(USER_11, true); @@ -5167,7 +5168,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void HandlePackageUpdate() throws Throwable { + public void testHandlePackageUpdate() throws Throwable { // Set up shortcuts and launchers. final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32); @@ -5341,7 +5342,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /** * Test the case where an updated app has resource IDs changed. */ - public void HandlePackageUpdate_resIdChanged() throws Exception { + public void testHandlePackageUpdate_resIdChanged() throws Exception { final Icon icon1 = Icon.createWithResource(getTestContext(), /* res ID */ 1000); final Icon icon2 = Icon.createWithResource(getTestContext(), /* res ID */ 1001); @@ -5416,7 +5417,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void HandlePackageUpdate_systemAppUpdate() { + public void testHandlePackageUpdate_systemAppUpdate() { // Package1 is a system app. Package 2 is not a system app, so it's not scanned // in this test at all. @@ -5522,7 +5523,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { mService.getUserShortcutsLocked(USER_10).getLastAppScanOsFingerprint()); } - public void HandlePackageChanged() { + public void testHandlePackageChanged() { final ComponentName ACTIVITY1 = new ComponentName(CALLING_PACKAGE_1, "act1"); final ComponentName ACTIVITY2 = new ComponentName(CALLING_PACKAGE_1, "act2"); @@ -5652,7 +5653,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void HandlePackageUpdate_activityNoLongerMain() throws Throwable { + public void testHandlePackageUpdate_activityNoLongerMain() throws Throwable { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( makeShortcutWithActivity("s1a", @@ -5738,7 +5739,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { * - Unpinned dynamic shortcuts * - Bitmaps */ - public void BackupAndRestore() { + public void testBackupAndRestore() { assertFileNotExists("user-0/shortcut_dump/restore-0-start.txt"); assertFileNotExists("user-0/shortcut_dump/restore-1-payload.xml"); @@ -5759,7 +5760,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { checkBackupAndRestore_success(/*firstRestore=*/ true); } - public void BackupAndRestore_backupRestoreTwice() { + public void testBackupAndRestore_backupRestoreTwice() { prepareForBackupTest(); checkBackupAndRestore_success(/*firstRestore=*/ true); @@ -5775,7 +5776,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { checkBackupAndRestore_success(/*firstRestore=*/ false); } - public void BackupAndRestore_restoreToNewVersion() { + public void testBackupAndRestore_restoreToNewVersion() { prepareForBackupTest(); addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 2); @@ -5784,7 +5785,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { checkBackupAndRestore_success(/*firstRestore=*/ true); } - public void BackupAndRestore_restoreToSuperSetSignatures() { + public void testBackupAndRestore_restoreToSuperSetSignatures() { prepareForBackupTest(); // Change package signatures. @@ -5981,7 +5982,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void BackupAndRestore_publisherWrongSignature() { + public void testBackupAndRestore_publisherWrongSignature() { prepareForBackupTest(); addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 10, "sigx"); // different signature @@ -5989,7 +5990,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { checkBackupAndRestore_publisherNotRestored(ShortcutInfo.DISABLED_REASON_SIGNATURE_MISMATCH); } - public void BackupAndRestore_publisherNoLongerBackupTarget() { + public void testBackupAndRestore_publisherNoLongerBackupTarget() { prepareForBackupTest(); updatePackageInfo(CALLING_PACKAGE_1, @@ -6118,7 +6119,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void BackupAndRestore_launcherLowerVersion() { + public void testBackupAndRestore_launcherLowerVersion() { prepareForBackupTest(); addPackage(LAUNCHER_1, LAUNCHER_UID_1, 0); // Lower version @@ -6127,7 +6128,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { checkBackupAndRestore_success(/*firstRestore=*/ true); } - public void BackupAndRestore_launcherWrongSignature() { + public void testBackupAndRestore_launcherWrongSignature() { prepareForBackupTest(); addPackage(LAUNCHER_1, LAUNCHER_UID_1, 10, "sigx"); // different signature @@ -6135,7 +6136,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { checkBackupAndRestore_launcherNotRestored(true); } - public void BackupAndRestore_launcherNoLongerBackupTarget() { + public void testBackupAndRestore_launcherNoLongerBackupTarget() { prepareForBackupTest(); updatePackageInfo(LAUNCHER_1, @@ -6240,7 +6241,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void BackupAndRestore_launcherAndPackageNoLongerBackupTarget() { + public void testBackupAndRestore_launcherAndPackageNoLongerBackupTarget() { prepareForBackupTest(); updatePackageInfo(CALLING_PACKAGE_1, @@ -6338,7 +6339,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void BackupAndRestore_disabled() { + public void testBackupAndRestore_disabled() { prepareCrossProfileDataSet(); // Before doing backup & restore, disable s1. @@ -6403,7 +6404,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { } - public void BackupAndRestore_manifestRePublished() { + public void testBackupAndRestore_manifestRePublished() { // Publish two manifest shortcuts. addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), @@ -6494,7 +6495,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { * logcat. * - if it has allowBackup=false, we don't touch any of the existing shortcuts. */ - public void BackupAndRestore_appAlreadyInstalledWhenRestored() { + public void testBackupAndRestore_appAlreadyInstalledWhenRestored() { // Pre-backup. Same as testBackupAndRestore_manifestRePublished(). // Publish two manifest shortcuts. @@ -6619,7 +6620,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /** * Test for restoring the pre-P backup format. */ - public void BackupAndRestore_api27format() throws Exception { + public void testBackupAndRestore_api27format() throws Exception { final byte[] payload = readTestAsset("shortcut/shortcut_api27_backup.xml").getBytes(); addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 10, "22222"); @@ -6657,7 +6658,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { } - public void SaveAndLoad_crossProfile() { + public void testSaveAndLoad_crossProfile() { prepareCrossProfileDataSet(); dumpsysOnLogcat("Before save & load"); @@ -6860,7 +6861,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { .getPackageUserId()); } - public void OnApplicationActive_permission() { + public void testOnApplicationActive_permission() { assertExpectException(SecurityException.class, "Missing permission", () -> mManager.onApplicationActive(CALLING_PACKAGE_1, USER_10)); @@ -6869,7 +6870,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { mManager.onApplicationActive(CALLING_PACKAGE_1, USER_10); } - public void GetShareTargets_permission() { + public void testGetShareTargets_permission() { addPackage(CHOOSER_ACTIVITY_PACKAGE, CHOOSER_ACTIVITY_UID, 10, "sig1"); mInjectedChooserActivity = ComponentName.createRelative(CHOOSER_ACTIVITY_PACKAGE, ".ChooserActivity"); @@ -6888,7 +6889,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void HasShareTargets_permission() { + public void testHasShareTargets_permission() { assertExpectException(SecurityException.class, "Missing permission", () -> mManager.hasShareTargets(CALLING_PACKAGE_1)); @@ -6897,7 +6898,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { mManager.hasShareTargets(CALLING_PACKAGE_1); } - public void isSharingShortcut_permission() throws IntentFilter.MalformedMimeTypeException { + public void testisSharingShortcut_permission() throws IntentFilter.MalformedMimeTypeException { setCaller(LAUNCHER_1, USER_10); IntentFilter filter_any = new IntentFilter(); @@ -6912,18 +6913,18 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { mManager.hasShareTargets(CALLING_PACKAGE_1); } - public void Dumpsys_crossProfile() { + public void testDumpsys_crossProfile() { prepareCrossProfileDataSet(); dumpsysOnLogcat("test1", /* force= */ true); } - public void Dumpsys_withIcons() throws IOException { - Icons(); + public void testDumpsys_withIcons() throws IOException { + testIcons(); // Dump after having some icons. dumpsysOnLogcat("test1", /* force= */ true); } - public void ManifestShortcut_publishOnUnlockUser() { + public void testManifestShortcut_publishOnUnlockUser() { addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), R.xml.shortcut_1); @@ -7137,7 +7138,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertNull(mService.getPackageShortcutForTest(LAUNCHER_1, USER_10)); } - public void ManifestShortcut_publishOnBroadcast() { + public void testManifestShortcut_publishOnBroadcast() { // First, no packages are installed. uninstallPackage(USER_10, CALLING_PACKAGE_1); uninstallPackage(USER_10, CALLING_PACKAGE_2); @@ -7393,7 +7394,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void ManifestShortcuts_missingMandatoryFields() { + public void testManifestShortcuts_missingMandatoryFields() { // Start with no apps installed. uninstallPackage(USER_10, CALLING_PACKAGE_1); uninstallPackage(USER_10, CALLING_PACKAGE_2); @@ -7462,7 +7463,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void ManifestShortcuts_intentDefinitions() { + public void testManifestShortcuts_intentDefinitions() { addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), R.xml.shortcut_error_4); @@ -7604,7 +7605,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void ManifestShortcuts_checkAllFields() { + public void testManifestShortcuts_checkAllFields() { mService.handleUnlockUser(USER_10); // Package 1 updated, which has one valid manifest shortcut and one invalid. @@ -7709,7 +7710,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void ManifestShortcuts_localeChange() throws InterruptedException { + public void testManifestShortcuts_localeChange() throws InterruptedException { mService.handleUnlockUser(USER_10); // Package 1 updated, which has one valid manifest shortcut and one invalid. @@ -7813,7 +7814,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void ManifestShortcuts_updateAndDisabled_notPinned() { + public void testManifestShortcuts_updateAndDisabled_notPinned() { mService.handleUnlockUser(USER_10); // First, just publish a manifest shortcut. @@ -7853,7 +7854,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void ManifestShortcuts_updateAndDisabled_pinned() { + public void testManifestShortcuts_updateAndDisabled_pinned() { mService.handleUnlockUser(USER_10); // First, just publish a manifest shortcut. @@ -7909,7 +7910,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void ManifestShortcuts_duplicateInSingleActivity() { + public void testManifestShortcuts_duplicateInSingleActivity() { mService.handleUnlockUser(USER_10); // The XML has two shortcuts with the same ID. @@ -7934,7 +7935,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void ManifestShortcuts_duplicateInTwoActivities() { + public void testManifestShortcuts_duplicateInTwoActivities() { mService.handleUnlockUser(USER_10); // ShortcutActivity has shortcut ms1 @@ -7986,7 +7987,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /** * Manifest shortcuts cannot override shortcuts that were published via the APIs. */ - public void ManifestShortcuts_cannotOverrideNonManifest() { + public void testManifestShortcuts_cannotOverrideNonManifest() { mService.handleUnlockUser(USER_10); // Create a non-pinned dynamic shortcut and a non-dynamic pinned shortcut. @@ -8059,7 +8060,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /** * Make sure the APIs won't work on manifest shortcuts. */ - public void ManifestShortcuts_immutable() { + public void testManifestShortcuts_immutable() { mService.handleUnlockUser(USER_10); // Create a non-pinned manifest shortcut, a pinned shortcut that was originally @@ -8152,7 +8153,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /** * Make sure the APIs won't work on manifest shortcuts. */ - public void ManifestShortcuts_tooMany() { + public void testManifestShortcuts_tooMany() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3"); @@ -8171,7 +8172,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void MaxShortcutCount_set() { + public void testMaxShortcutCount_set() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3"); @@ -8252,7 +8253,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void MaxShortcutCount_add() { + public void testMaxShortcutCount_add() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3"); @@ -8379,7 +8380,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void MaxShortcutCount_update() { + public void testMaxShortcutCount_update() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3"); @@ -8470,7 +8471,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void ShortcutsPushedOutByManifest() { + public void testShortcutsPushedOutByManifest() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3"); @@ -8578,7 +8579,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void ReturnedByServer() { + public void disabled_testReturnedByServer() { // Package 1 updated, with manifest shortcuts. addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), @@ -8624,7 +8625,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void IsForegroundDefaultLauncher_true() { + public void testIsForegroundDefaultLauncher_true() { // random uid in the USER_10 range. final int uid = 1000024; @@ -8635,7 +8636,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { } - public void IsForegroundDefaultLauncher_defaultButNotForeground() { + public void testIsForegroundDefaultLauncher_defaultButNotForeground() { // random uid in the USER_10 range. final int uid = 1000024; @@ -8645,7 +8646,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertFalse(mInternal.isForegroundDefaultLauncher("default", uid)); } - public void IsForegroundDefaultLauncher_foregroundButNotDefault() { + public void testIsForegroundDefaultLauncher_foregroundButNotDefault() { // random uid in the USER_10 range. final int uid = 1000024; @@ -8655,7 +8656,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertFalse(mInternal.isForegroundDefaultLauncher("another", uid)); } - public void ParseShareTargetsFromManifest() { + public void testParseShareTargetsFromManifest() { // These values must exactly match the content of shortcuts_share_targets.xml resource List<ShareTargetInfo> expectedValues = new ArrayList<>(); expectedValues.add(new ShareTargetInfo( @@ -8707,7 +8708,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { } } - public void ShareTargetInfo_saveToXml() throws IOException, XmlPullParserException { + public void testShareTargetInfo_saveToXml() throws IOException, XmlPullParserException { List<ShareTargetInfo> expectedValues = new ArrayList<>(); expectedValues.add(new ShareTargetInfo( new ShareTargetInfo.TargetData[]{new ShareTargetInfo.TargetData( @@ -8773,7 +8774,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { } } - public void IsSharingShortcut() throws IntentFilter.MalformedMimeTypeException { + public void testIsSharingShortcut() throws IntentFilter.MalformedMimeTypeException { addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), R.xml.shortcut_share_targets); @@ -8823,7 +8824,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { filter_any)); } - public void IsSharingShortcut_PinnedAndCachedOnlyShortcuts() + public void testIsSharingShortcut_PinnedAndCachedOnlyShortcuts() throws IntentFilter.MalformedMimeTypeException { addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), @@ -8880,7 +8881,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { filter_any)); } - public void AddingShortcuts_ExcludesHiddenFromLauncherShortcuts() { + public void testAddingShortcuts_ExcludesHiddenFromLauncherShortcuts() { final ShortcutInfo s1 = makeShortcutExcludedFromLauncher("s1"); final ShortcutInfo s2 = makeShortcutExcludedFromLauncher("s2"); final ShortcutInfo s3 = makeShortcutExcludedFromLauncher("s3"); @@ -8901,7 +8902,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void UpdateShortcuts_ExcludesHiddenFromLauncherShortcuts() { + public void testUpdateShortcuts_ExcludesHiddenFromLauncherShortcuts() { final ShortcutInfo s1 = makeShortcut("s1"); final ShortcutInfo s2 = makeShortcut("s2"); final ShortcutInfo s3 = makeShortcut("s3"); @@ -8914,7 +8915,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void PinHiddenShortcuts_ThrowsException() { + public void testPinHiddenShortcuts_ThrowsException() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertThrown(IllegalArgumentException.class, () -> { mManager.requestPinShortcut(makeShortcutExcludedFromLauncher("s1"), null); diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java index 2ed71cecd79d..952d8fa47a34 100644 --- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java @@ -118,7 +118,6 @@ public class ThermalManagerServiceTest { private class ThermalHalFake extends ThermalHalWrapper { private static final int INIT_STATUS = Temperature.THROTTLING_NONE; private List<Temperature> mTemperatureList = new ArrayList<>(); - private List<Temperature> mOverrideTemperatures = null; private List<CoolingDevice> mCoolingDeviceList = new ArrayList<>(); private List<TemperatureThreshold> mTemperatureThresholdList = initializeThresholds(); @@ -132,6 +131,9 @@ public class ThermalManagerServiceTest { INIT_STATUS); private CoolingDevice mCpu = new CoolingDevice(40, CoolingDevice.TYPE_BATTERY, "cpu"); private CoolingDevice mGpu = new CoolingDevice(43, CoolingDevice.TYPE_BATTERY, "gpu"); + private Map<Integer, Float> mForecastSkinTemperatures = null; + private int mForecastSkinTemperaturesCalled = 0; + private boolean mForecastSkinTemperaturesError = false; private List<TemperatureThreshold> initializeThresholds() { ArrayList<TemperatureThreshold> thresholds = new ArrayList<>(); @@ -173,12 +175,17 @@ public class ThermalManagerServiceTest { mCoolingDeviceList.add(mGpu); } - void setOverrideTemperatures(List<Temperature> temperatures) { - mOverrideTemperatures = temperatures; + void enableForecastSkinTemperature() { + mForecastSkinTemperatures = Map.of(0, 22.0f, 10, 25.0f, 20, 28.0f, + 30, 31.0f, 40, 34.0f, 50, 37.0f, 60, 40.0f); } - void resetOverrideTemperatures() { - mOverrideTemperatures = null; + void disableForecastSkinTemperature() { + mForecastSkinTemperatures = null; + } + + void failForecastSkinTemperature() { + mForecastSkinTemperaturesError = true; } @Override @@ -219,6 +226,18 @@ public class ThermalManagerServiceTest { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + mForecastSkinTemperaturesCalled++; + if (mForecastSkinTemperaturesError) { + throw new RuntimeException(); + } + if (mForecastSkinTemperatures == null) { + throw new UnsupportedOperationException(); + } + return mForecastSkinTemperatures.get(forecastSeconds); + } + + @Override protected boolean connectToHal() { return true; } @@ -388,7 +407,7 @@ public class ThermalManagerServiceTest { Thread.sleep(CALLBACK_TIMEOUT_MILLI_SEC); resetListenerMock(); int status = Temperature.THROTTLING_SEVERE; - mFakeHal.setOverrideTemperatures(new ArrayList<>()); + mFakeHal.mTemperatureList = new ArrayList<>(); // Should not notify on non-skin type Temperature newBattery = new Temperature(37, Temperature.TYPE_BATTERY, "batt", status); @@ -518,6 +537,99 @@ public class ThermalManagerServiceTest { } @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast() throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + assertEquals(1.0f, mService.mService.getThermalHeadroom(60), 0.01f); + assertEquals(0.9f, mService.mService.getThermalHeadroom(50), 0.01f); + assertEquals(0.8f, mService.mService.getThermalHeadroom(40), 0.01f); + assertEquals(0.7f, mService.mService.getThermalHeadroom(30), 0.01f); + assertEquals(0.6f, mService.mService.getThermalHeadroom(20), 0.01f); + assertEquals(0.5f, mService.mService.getThermalHeadroom(10), 0.01f); + assertEquals(0.4f, mService.mService.getThermalHeadroom(0), 0.01f); + assertEquals(7, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholds() + throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + List<TemperatureThreshold> thresholds = mFakeHal.initializeThresholds(); + TemperatureThreshold skinThreshold = new TemperatureThreshold(); + skinThreshold.type = Temperature.TYPE_SKIN; + skinThreshold.name = "skin2"; + skinThreshold.hotThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/]; + skinThreshold.coldThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/]; + for (int i = 0; i < skinThreshold.hotThrottlingThresholds.length; ++i) { + // Sets NONE to 45.0f, SEVERE to 60.0f, and SHUTDOWN to 75.0f + skinThreshold.hotThrottlingThresholds[i] = 45.0f + 5.0f * i; + } + thresholds.add(skinThreshold); + mFakeHal.mTemperatureThresholdList = thresholds; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertFalse("HAL skin forecast should be disabled on multiple SKIN thresholds", + mService.mIsHalSkinForecastSupported.get()); + mService.mService.getThermalHeadroom(10); + assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST, + Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK}) + public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholdsCallback() + throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + TemperatureThreshold newThreshold = new TemperatureThreshold(); + newThreshold.name = "skin2"; + newThreshold.type = Temperature.TYPE_SKIN; + newThreshold.hotThrottlingThresholds = new float[]{ + Float.NaN, 43.0f, 46.0f, 49.0f, Float.NaN, Float.NaN, Float.NaN + }; + mFakeHal.mCallback.onThresholdChanged(newThreshold); + mService.mService.getThermalHeadroom(10); + assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast_errorOnHal() throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + mFakeHal.disableForecastSkinTemperature(); + assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.enableForecastSkinTemperature(); + assertFalse(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(2, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.failForecastSkinTemperature(); + assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(3, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test @EnableFlags({Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK, Flags.FLAG_ALLOW_THERMAL_HEADROOM_THRESHOLDS}) public void testTemperatureWatcherUpdateSevereThresholds() throws Exception { diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java b/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java new file mode 100644 index 000000000000..47e3dc85f6d0 --- /dev/null +++ b/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import android.annotation.UserIdInt; + +public final class ConfigInternalForTests { + + static final @UserIdInt int USER_ID = 9876; + + static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_DISABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(false) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_ENABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(false) + .setAutoDetectionEnabledSetting(true) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(true) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_DETECT_NOT_SUPPORTED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(false) + .setGeoDetectionFeatureSupported(false) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_DISABLED_GEO_DISABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_DISABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(true) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_ENABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(true) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(true) + .build(); +} diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java b/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java index fc6afe486187..aeb4d9a19ff0 100644 --- a/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java +++ b/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java @@ -31,7 +31,7 @@ import java.util.Optional; /** * A partially implemented, fake implementation of ServiceConfigAccessor for tests. * - * <p>This class has rudamentary support for multiple users, but unlike the real thing, it doesn't + * <p>This class has rudimentary support for multiple users, but unlike the real thing, it doesn't * simulate that some settings are global and shared between users. It also delivers config updates * synchronously. */ diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java index e52e8b60a61d..47a9b2c47173 100644 --- a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java +++ b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java @@ -35,6 +35,12 @@ import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_U import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_HIGH; import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_DETECT_NOT_SUPPORTED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_DISABLED_GEO_DISABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_DISABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_ENABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_USER_RESTRICTED_AUTO_ENABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.USER_ID; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGH; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGHEST; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_LOW; @@ -68,6 +74,7 @@ import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.MatchType; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.Quality; import android.service.timezone.TimeZoneProviderStatus; +import android.util.IndentingPrintWriter; import com.android.server.SystemTimeZone.TimeZoneConfidence; import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedTelephonyTimeZoneSuggestion; @@ -82,6 +89,7 @@ import org.junit.runner.RunWith; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -92,7 +100,6 @@ import java.util.function.Function; @RunWith(JUnitParamsRunner.class) public class TimeZoneDetectorStrategyImplTest { - private static final @UserIdInt int USER_ID = 9876; private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234; /** A time zone used for initialization that does not occur elsewhere in tests. */ private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC"; @@ -101,7 +108,7 @@ public class TimeZoneDetectorStrategyImplTest { // Telephony test cases are ordered so that each successive one is of the same or higher score // than the previous. - private static final TelephonyTestCase[] TELEPHONY_TEST_CASES = new TelephonyTestCase[] { + private static final TelephonyTestCase[] TELEPHONY_TEST_CASES = new TelephonyTestCase[]{ newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, TELEPHONY_SCORE_LOW), newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, @@ -118,90 +125,6 @@ public class TimeZoneDetectorStrategyImplTest { TELEPHONY_SCORE_HIGHEST), }; - private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_DISABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(false) - .setAutoDetectionEnabledSetting(false) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_ENABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(false) - .setAutoDetectionEnabledSetting(true) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(true) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_DETECT_NOT_SUPPORTED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(false) - .setGeoDetectionFeatureSupported(false) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(false) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_DISABLED_GEO_DISABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(false) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_DISABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(true) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_ENABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(true) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(true) - .build(); - private static final TelephonyTimeZoneAlgorithmStatus TELEPHONY_ALGORITHM_RUNNING_STATUS = new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING); @@ -421,7 +344,7 @@ public class TimeZoneDetectorStrategyImplTest { new QualifiedTelephonyTimeZoneSuggestion(slotIndex1TimeZoneSuggestion, TELEPHONY_SCORE_NONE); script.verifyLatestQualifiedTelephonySuggestionReceived( - SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion) + SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion) .verifyLatestQualifiedTelephonySuggestionReceived(SLOT_INDEX2, null); assertEquals(expectedSlotIndex1ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); @@ -629,7 +552,7 @@ public class TimeZoneDetectorStrategyImplTest { */ @Test public void testTelephonySuggestionMultipleSlotIndexSuggestionScoringAndSlotIndexBias() { - String[] zoneIds = { "Europe/London", "Europe/Paris" }; + String[] zoneIds = {"Europe/London", "Europe/Paris"}; TelephonyTimeZoneSuggestion emptySlotIndex1Suggestion = createEmptySlotIndex1Suggestion(); TelephonyTimeZoneSuggestion emptySlotIndex2Suggestion = createEmptySlotIndex2Suggestion(); QualifiedTelephonyTimeZoneSuggestion expectedEmptySlotIndex1ScoredSuggestion = @@ -672,7 +595,7 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyLatestQualifiedTelephonySuggestionReceived( - SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion) + SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion) .verifyLatestQualifiedTelephonySuggestionReceived( SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion); assertEquals(expectedZoneSlotIndex1ScoredSuggestion, @@ -805,14 +728,14 @@ public class TimeZoneDetectorStrategyImplTest { boolean bypassUserPolicyChecks = false; boolean expectedResult = true; script.simulateManualTimeZoneSuggestion( - USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) + USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) .verifyTimeZoneChangedAndReset(manualSuggestion); assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion()); } @Test - @Parameters({ "true,true", "true,false", "false,true", "false,false" }) + @Parameters({"true,true", "true,false", "false,true", "false,false"}) public void testManualSuggestion_autoTimeEnabled_userRestrictions( boolean userConfigAllowed, boolean bypassUserPolicyChecks) { ConfigurationInternal config = @@ -834,7 +757,7 @@ public class TimeZoneDetectorStrategyImplTest { } @Test - @Parameters({ "true,true", "true,false", "false,true", "false,false" }) + @Parameters({"true,true", "true,false", "false,true", "false,false"}) public void testManualSuggestion_autoTimeDisabled_userRestrictions( boolean userConfigAllowed, boolean bypassUserPolicyChecks) { ConfigurationInternal config = @@ -849,7 +772,7 @@ public class TimeZoneDetectorStrategyImplTest { ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Europe/Paris"); boolean expectedResult = userConfigAllowed || bypassUserPolicyChecks; script.simulateManualTimeZoneSuggestion( - USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult); + USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult); if (expectedResult) { script.verifyTimeZoneChangedAndReset(manualSuggestion); assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion()); @@ -1258,7 +1181,6 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); - } // Demonstrate what happens when geolocation is uncertain when telephony fallback is @@ -1569,7 +1491,7 @@ public class TimeZoneDetectorStrategyImplTest { boolean bypassUserPolicyChecks = false; boolean expectedResult = true; script.simulateManualTimeZoneSuggestion( - USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) + USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) .verifyTimeZoneChangedAndReset(manualSuggestion); expectedDeviceTimeZoneId = manualSuggestion.getZoneId(); assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId, @@ -1880,6 +1802,7 @@ public class TimeZoneDetectorStrategyImplTest { boolean actualResult = mTimeZoneDetectorStrategy.suggestManualTimeZone( userId, manualTimeZoneSuggestion, bypassUserPolicyChecks); assertEquals(expectedResult, actualResult); + return this; } @@ -2001,4 +1924,34 @@ public class TimeZoneDetectorStrategyImplTest { return new TelephonyTestCase(matchType, quality, expectedScore); } + static class FakeTimeZoneChangeEventListener implements TimeZoneChangeListener { + private final List<TimeZoneChangeEvent> mEvents = new ArrayList<>(); + + FakeTimeZoneChangeEventListener() { + } + + @Override + public void process(TimeZoneChangeEvent event) { + mEvents.add(event); + } + + public List<TimeZoneChangeEvent> getTimeZoneChangeEvents() { + return mEvents; + } + + @Override + public void dump(IndentingPrintWriter ipw) { + // No-op for tests + } + } + + private static void assertEmpty(Collection<?> collection) { + assertTrue( + "Expected empty, but contains (" + collection.size() + ") elements: " + collection, + collection.isEmpty()); + } + + private static void assertNotEmpty(Collection<?> collection) { + assertFalse("Expected not empty: " + collection, collection.isEmpty()); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java index af7f703e9c31..b33233107766 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java @@ -101,9 +101,12 @@ public class ConditionProvidersTest extends UiServiceTestCase { mProviders.notifyConditions("package", msi, conditionsToNotify); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0])); - verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[1])); - verify(mCallback).onConditionChanged(eq(Uri.parse("c")), eq(conditionsToNotify[2])); + verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]), + eq(100)); + verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[1]), + eq(100)); + verify(mCallback).onConditionChanged(eq(Uri.parse("c")), eq(conditionsToNotify[2]), + eq(100)); verifyNoMoreInteractions(mCallback); } @@ -121,8 +124,10 @@ public class ConditionProvidersTest extends UiServiceTestCase { mProviders.notifyConditions("package", msi, conditionsToNotify); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0])); - verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[1])); + verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]), + eq(100)); + verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[1]), + eq(100)); verifyNoMoreInteractions(mCallback); } @@ -141,8 +146,10 @@ public class ConditionProvidersTest extends UiServiceTestCase { mProviders.notifyConditions("package", msi, conditionsToNotify); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0])); - verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[3])); + verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]), + eq(100)); + verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[3]), + eq(100)); verifyNoMoreInteractions(mCallback); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java index 6cb24293a7d5..fa733e85c89c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java @@ -2509,6 +2509,134 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS, + android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) + public void testRepostWithNewChannel_afterAutogrouping_isRegrouped() { + final String pkg = "package"; + final List<NotificationRecord> notificationList = new ArrayList<>(); + // Post ungrouped notifications => will be autogrouped + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 42, + String.valueOf(i + 42), UserHandle.SYSTEM, null, false); + notificationList.add(notification); + mGroupHelper.onNotificationPosted(notification, false); + } + + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + + // Post ungrouped notifications to a different section, below autogroup limit + Mockito.reset(mCallback); + // Post ungrouped notifications => will be autogrouped + final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1", + "TEST_CHANNEL_ID1", IMPORTANCE_LOW); + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 4242, + String.valueOf(i + 4242), UserHandle.SYSTEM, null, false, silentChannel); + notificationList.add(notification); + mGroupHelper.onNotificationPosted(notification, false); + } + + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean()); + + // Update a notification to a different channel that moves it to a different section + Mockito.reset(mCallback); + final NotificationRecord notifToInvalidate = notificationList.get(0); + final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate); + final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2", + "TEST_CHANNEL_ID2", IMPORTANCE_LOW); + notifToInvalidate.updateNotificationChannel(updatedChannel); + assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection); + boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false); + assertThat(needsAutogrouping).isTrue(); + + // Check that the silent section was autogrouped + final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(silentSectionGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), + eq(silentSectionGroupKey), eq(true)); + verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey())); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), + eq(expectedGroupKey), any()); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS, + android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) + public void testRepostWithNewChannel_afterForceGrouping_isRegrouped() { + final String pkg = "package"; + final String groupName = "testGroup"; + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post valid section summary notifications without children => force group + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 42, + String.valueOf(i + 42), UserHandle.SYSTEM, groupName, false); + notificationList.add(notification); + mGroupHelper.onNotificationPostedWithDelay(notification, notificationList, + summaryByGroup); + } + + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + + // Update a notification to a different channel that moves it to a different section + Mockito.reset(mCallback); + final NotificationRecord notifToInvalidate = notificationList.get(0); + final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate); + final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2", + "TEST_CHANNEL_ID2", IMPORTANCE_LOW); + notifToInvalidate.updateNotificationChannel(updatedChannel); + assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection); + boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false); + + mGroupHelper.onNotificationPostedWithDelay(notifToInvalidate, notificationList, + summaryByGroup); + + // Check that the updated notification is removed from the autogroup + assertThat(needsAutogrouping).isFalse(); + verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey())); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), + eq(expectedGroupKey), any()); + + // Post child notifications for the silent sectin => will be autogrouped + Mockito.reset(mCallback); + final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1", + "TEST_CHANNEL_ID1", IMPORTANCE_LOW); + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 4242, + String.valueOf(i + 4242), UserHandle.SYSTEM, "aGroup", false, silentChannel); + notificationList.add(notification); + needsAutogrouping = mGroupHelper.onNotificationPosted(notification, false); + assertThat(needsAutogrouping).isFalse(); + mGroupHelper.onNotificationPostedWithDelay(notification, notificationList, + summaryByGroup); + } + + // Check that the silent section was autogrouped + final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(silentSectionGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(silentSectionGroupKey), eq(true)); + } + + @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testMoveAggregateGroups_updateChannel() { final String pkg = "package"; 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 301165f8151d..e43b28bb9404 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -6341,6 +6341,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testOnlyAutogroupIfNeeded_channelChanged_ghUpdate() { + NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, + "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false); + mService.addNotification(r); + + NotificationRecord update = generateNotificationRecord(mSilentChannel, 0, + "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false); + mService.addEnqueuedNotification(update); + + NotificationManagerService.PostNotificationRunnable runnable = + mService.new PostNotificationRunnable(update.getKey(), + update.getSbn().getPackageName(), update.getUid(), + mPostNotificationTrackerFactory.newTracker(null)); + runnable.run(); + waitForIdle(); + + verify(mGroupHelper, times(1)).onNotificationPosted(any(), anyBoolean()); + } + + @Test public void testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate() { NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, "testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate", null, false); @@ -11213,7 +11233,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Representative used to verify getCallingZenUser(). mBinderService.getAutomaticZenRules(); - verify(zenModeHelper).getAutomaticZenRules(eq(UserHandle.CURRENT)); + verify(zenModeHelper).getAutomaticZenRules(eq(UserHandle.CURRENT), anyInt()); } @Test @@ -11225,7 +11245,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Representative used to verify getCallingZenUser(). mBinderService.getAutomaticZenRules(); - verify(zenModeHelper).getAutomaticZenRules(eq(Binder.getCallingUserHandle())); + verify(zenModeHelper).getAutomaticZenRules(eq(Binder.getCallingUserHandle()), anyInt()); } /** Prepares for a zen-related test that uses a mocked {@link ZenModeHelper}. */ @@ -17901,4 +17921,63 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r1), eq(hasOriginalSummary)); } + @Test + @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION, + FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) + public void testRebundleNotification_restoresBundleChannel() throws Exception { + NotificationManagerService.WorkerHandler handler = mock( + NotificationManagerService.WorkerHandler.class); + mService.setHandler(handler); + when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); + when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true); + when(mAssistants.isTypeAdjustmentAllowedForPackage(anyString(), anyInt())).thenReturn(true); + + // Post a single notification + final boolean hasOriginalSummary = false; + final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); + final String keyToUnbundle = r.getKey(); + mService.addNotification(r); + + // Classify notification into the NEWS bundle + Bundle signals = new Bundle(); + signals.putInt(Adjustment.KEY_TYPE, Adjustment.TYPE_NEWS); + Adjustment adjustment = new Adjustment( + r.getSbn().getPackageName(), r.getKey(), signals, "", r.getUser().getIdentifier()); + mBinderService.applyAdjustmentFromAssistant(null, adjustment); + waitForIdle(); + r.applyAdjustments(); + // Check that the NotificationRecord channel is updated + assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID); + assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS); + + // Unbundle the notification + mService.mNotificationDelegate.unbundleNotification(keyToUnbundle); + + // Check that the original channel was restored + assertThat(r.getChannel().getId()).isEqualTo(TEST_CHANNEL_ID); + assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS); + verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r), eq(hasOriginalSummary)); + + Mockito.reset(mRankingHandler); + Mockito.reset(mGroupHelper); + + // Rebundle the notification + mService.mNotificationDelegate.rebundleNotification(keyToUnbundle); + + // Actually apply the adjustments + doAnswer(invocationOnMock -> { + ((NotificationRecord) invocationOnMock.getArguments()[0]).applyAdjustments(); + ((NotificationRecord) invocationOnMock.getArguments()[0]).calculateImportance(); + return null; + }).when(mRankingHelper).extractSignals(any(NotificationRecord.class)); + mService.handleRankingSort(); + verify(handler, times(1)).scheduleSendRankingUpdate(); + + // Check that the bundle channel was restored + verify(mRankingHandler, times(1)).requestSort(); + assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID); + } + } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 8e79514c875e..f41805d40b0d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -66,7 +66,6 @@ import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.No import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED; -import static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI; import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA; import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER; import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE; @@ -164,6 +163,7 @@ import com.android.os.AtomsProto.PackageNotificationChannelPreferences; import com.android.os.AtomsProto.PackageNotificationPreferences; import com.android.server.UiServiceTestCase; import com.android.server.notification.PermissionHelper.PackagePermission; +import com.android.server.uri.UriGrantsManagerInternal; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -179,6 +179,9 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -199,9 +202,6 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadLocalRandom; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @EnableFlags(FLAG_PERSIST_INCOMPLETE_RESTORE_DATA) @@ -239,9 +239,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { private NotificationManager.Policy mTestNotificationPolicy; - private PreferencesHelper mHelper; - // fresh object for testing xml reading - private PreferencesHelper mXmlHelper; + private TestPreferencesHelper mHelper; + // fresh object for testing xml reading; also TestPreferenceHelper in order to avoid interacting + // with real IpcDataCaches + private TestPreferencesHelper mXmlHelper; private AudioAttributes mAudioAttributes; private NotificationChannelLoggerFake mLogger = new NotificationChannelLoggerFake(); @@ -378,10 +379,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(mUserProfiles.getCurrentProfileIds()).thenReturn(currentProfileIds); when(mClock.millis()).thenReturn(System.currentTimeMillis()); - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); resetZenModeHelper(); @@ -793,7 +794,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_oldXml_migrates() throws Exception { - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -929,7 +930,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception { - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -988,7 +989,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_permissionNotificationOff() throws Exception { - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock); @@ -1047,7 +1048,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception { - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -1641,7 +1642,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { serializer.flush(); // simulate load after reboot - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); loadByteArrayXml(baos.toByteArray(), false, USER_ALL); @@ -1696,7 +1697,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { Duration.ofDays(2).toMillis() + System.currentTimeMillis()); // simulate load after reboot - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); loadByteArrayXml(xml.getBytes(), false, USER_ALL); @@ -1774,10 +1775,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(contentResolver.getResourceId(ANDROID_RES_SOUND_URI)).thenReturn(resId).thenThrow( new FileNotFoundException("")).thenReturn(resId); - mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); - mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); @@ -3190,7 +3191,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() { final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri"); @@ -3210,7 +3210,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() { final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound"); @@ -3229,7 +3228,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() { final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound"); @@ -6573,4 +6571,223 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.setCanBePromoted(PKG_P, UID_P, false, false); assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue(); } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_invalidateOnCreationAndChange() { + mHelper.resetCacheInvalidation(); + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // new channel should invalidate the cache. + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // when the channel data is updated, should invalidate the cache again after that. + mHelper.resetCacheInvalidation(); + NotificationChannel newChannel = channel.copy(); + newChannel.setName("new name"); + newChannel.setImportance(IMPORTANCE_HIGH); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // also for conversations + mHelper.resetCacheInvalidation(); + String parentId = "id"; + String convId = "conversation"; + NotificationChannel conv = new NotificationChannel( + String.format(CONVERSATION_CHANNEL_ID_FORMAT, parentId, convId), "conversation", + IMPORTANCE_DEFAULT); + conv.setConversationId(parentId, convId); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, conv, true, false, UID_N_MR1, + false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + mHelper.resetCacheInvalidation(); + NotificationChannel newConv = conv.copy(); + newConv.setName("changed"); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newConv, true, UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_invalidateOnDelete() { + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // ignore any invalidations up until now + mHelper.resetCacheInvalidation(); + + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // recreate channel and now permanently delete + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + mHelper.resetCacheInvalidation(); + mHelper.permanentlyDeleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id"); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_noInvalidationWhenNoChange() { + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // ignore any invalidations up until now + mHelper.resetCacheInvalidation(); + + // newChannel, same as the old channel + NotificationChannel newChannel = channel.copy(); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, false, UID_N_MR1, + false); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false); + + // because there were no effective changes, we should not see any cache invalidations + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // deletions of a nonexistent channel also don't change anything + mHelper.resetCacheInvalidation(); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "nonexistent", UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_multipleUsersAndPackages() { + // Setup: create channels for: + // pkg O, user + // pkg O, work (same channel ID, different user) + // pkg N_MR1, user + // pkg N_MR1, user, conversation child of above + String p2u1ConvId = String.format(CONVERSATION_CHANNEL_ID_FORMAT, "p2", "conv"); + NotificationChannel p1u1 = new NotificationChannel("p1", "p1u1", IMPORTANCE_DEFAULT); + NotificationChannel p1u2 = new NotificationChannel("p1", "p1u2", IMPORTANCE_DEFAULT); + NotificationChannel p2u1 = new NotificationChannel("p2", "p2u1", IMPORTANCE_DEFAULT); + NotificationChannel p2u1Conv = new NotificationChannel(p2u1ConvId, "p2u1 conv", + IMPORTANCE_DEFAULT); + p2u1Conv.setConversationId("p2", "conv"); + + mHelper.createNotificationChannel(PKG_O, UID_O, p1u1, true, + false, UID_O, false); + mHelper.createNotificationChannel(PKG_O, UID_O + UserHandle.PER_USER_RANGE, p1u2, true, + false, UID_O + UserHandle.PER_USER_RANGE, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1, true, + false, UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1Conv, true, + false, UID_N_MR1, false); + mHelper.resetCacheInvalidation(); + + // Update to an existent channel, with a change: should invalidate + NotificationChannel p1u1New = p1u1.copy(); + p1u1New.setName("p1u1 new"); + mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New, true, UID_O, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // Do it again, but no change for this user + mHelper.resetCacheInvalidation(); + mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New.copy(), true, UID_O, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // Delete conversations, but for a package without those conversations + mHelper.resetCacheInvalidation(); + mHelper.deleteConversations(PKG_O, UID_O, Set.of(p2u1Conv.getConversationId()), UID_O, + false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // Now delete conversations for the right package + mHelper.resetCacheInvalidation(); + mHelper.deleteConversations(PKG_N_MR1, UID_N_MR1, Set.of(p2u1Conv.getConversationId()), + UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_userRemoved() throws Exception { + NotificationChannel c1 = new NotificationChannel("id1", "name1", IMPORTANCE_DEFAULT); + int uid1 = UserHandle.getUid(1, 1); + setUpPackageWithUid("pkg1", uid1); + mHelper.createNotificationChannel("pkg1", uid1, c1, true, false, uid1, false); + mHelper.resetCacheInvalidation(); + + // delete user 1; should invalidate cache + mHelper.onUserRemoved(1); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_packagesChanged() { + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + + // package deleted: expect cache invalidation + mHelper.resetCacheInvalidation(); + mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1}, + new int[]{UID_N_MR1}); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // re-created: expect cache invalidation again + mHelper.resetCacheInvalidation(); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + mHelper.onPackagesChanged(false, USER_SYSTEM, new String[]{PKG_N_MR1}, + new int[]{UID_N_MR1}); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @DisableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_flagOff_neverTouchesCache() { + // Do a bunch of channel-changing operations. + NotificationChannel channel = + new NotificationChannel("id", "name1", NotificationManager.IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); + + NotificationChannel copy = channel.copy(); + copy.setName("name2"); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, copy, true, UID_N_MR1, false); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false); + + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + } + + // Test version of PreferencesHelper whose only functional difference is that it does not + // interact with the real IpcDataCache, and instead tracks whether or not the cache has been + // invalidated since creation or the last reset. + private static class TestPreferencesHelper extends PreferencesHelper { + private boolean mCacheInvalidated = false; + + TestPreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler, + ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager, + NotificationChannelLogger notificationChannelLogger, + AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles, + UriGrantsManagerInternal ugmInternal, + boolean showReviewPermissionsNotification, Clock clock) { + super(context, pm, rankingHandler, zenHelper, permHelper, permManager, + notificationChannelLogger, appOpsManager, userProfiles, ugmInternal, + showReviewPermissionsNotification, clock); + } + + @Override + protected void invalidateNotificationChannelCache() { + mCacheInvalidated = true; + } + + boolean hasCacheBeenInvalidated() { + return mCacheInvalidated; + } + + void resetCacheInvalidation() { + mCacheInvalidated = false; + } + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 1884bbd39bb9..6ef078b6da8a 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -291,7 +291,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return FlagsParameterization.allCombinationsOf(FLAG_MODES_UI, FLAG_BACKUP_RESTORE_LOGGING); + return FlagsParameterization.allCombinationsOf(FLAG_MODES_UI, FLAG_BACKUP_RESTORE_LOGGING, + com.android.server.notification.Flags.FLAG_FIX_CALLING_UID_FROM_CPS); } public ZenModeHelperTest(FlagsParameterization flags) { @@ -2617,7 +2618,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testSetAutomaticZenRuleState_nullPkg() { + public void testSetAutomaticZenRuleStateFromConditionProvider_nullPkg() { AutomaticZenRule zenRule = new AutomaticZenRule("name", null, new ComponentName(mContext.getPackageName(), "ScheduleConditionProvider"), @@ -2627,10 +2628,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, null, zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, zenRule.getConditionId(), - new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleStateFromConditionProvider(UserHandle.CURRENT, + zenRule.getConditionId(), new Condition(zenRule.getConditionId(), "", STATE_TRUE), + ORIGIN_APP, CUSTOM_PKG_UID); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); assertEquals(STATE_TRUE, ruleInConfig.condition.state); @@ -2726,8 +2726,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ORIGIN_SYSTEM, "test", SYSTEM_UID); Condition condition = new Condition(sharedUri, "", STATE_TRUE); - mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, sharedUri, condition, - ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleStateFromConditionProvider(UserHandle.CURRENT, sharedUri, + condition, ORIGIN_SYSTEM, SYSTEM_UID); for (ZenModeConfig.ZenRule rule : mZenModeHelper.mConfig.automaticRules.values()) { if (rule.id.equals(id)) { @@ -2741,8 +2741,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { } condition = new Condition(sharedUri, "", STATE_FALSE); - mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, sharedUri, condition, - ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleStateFromConditionProvider(UserHandle.CURRENT, sharedUri, + condition, ORIGIN_SYSTEM, SYSTEM_UID); for (ZenModeConfig.ZenRule rule : mZenModeHelper.mConfig.automaticRules.values()) { if (rule.id.equals(id)) { @@ -2780,9 +2780,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(zde) .build(), - ORIGIN_APP, "reasons", 0); + ORIGIN_APP, "reasons", CUSTOM_PKG_UID); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); assertThat(savedRule.getDeviceEffects()).isEqualTo( new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) @@ -2814,9 +2815,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(zde) .build(), - ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", SYSTEM_UID); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + SYSTEM_UID); assertThat(savedRule.getDeviceEffects()).isEqualTo(zde); } @@ -2845,7 +2847,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ORIGIN_USER_IN_SYSTEMUI, "reasons", 0); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + SYSTEM_UID); assertThat(savedRule.getDeviceEffects()).isEqualTo(zde); } @@ -2863,7 +2866,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(original) .build(), - ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", SYSTEM_UID); ZenDeviceEffects updateFromApp = new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) // Good @@ -2875,9 +2878,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(updateFromApp) .build(), - ORIGIN_APP, "reasons", 0); + ORIGIN_APP, "reasons", CUSTOM_PKG_UID); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); assertThat(savedRule.getDeviceEffects()).isEqualTo( new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) // From update. @@ -2898,7 +2902,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(original) .build(), - ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", SYSTEM_UID); ZenDeviceEffects updateFromSystem = new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) // Good @@ -2908,9 +2912,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setDeviceEffects(updateFromSystem) .build(), - ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", SYSTEM_UID); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + SYSTEM_UID); assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromSystem); } @@ -2926,7 +2931,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(original) .build(), - ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", SYSTEM_UID); ZenDeviceEffects updateFromUser = new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) @@ -2939,9 +2944,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setDeviceEffects(updateFromUser) .build(), - ORIGIN_USER_IN_SYSTEMUI, "reasons", 0); + ORIGIN_USER_IN_SYSTEMUI, "reasons", SYSTEM_UID); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + SYSTEM_UID); assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromUser); } @@ -2959,15 +2965,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // default is stars .build()) .build(), - ORIGIN_APP, "reasons", 0); + ORIGIN_APP, "reasons", CUSTOM_PKG_UID); mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, new AutomaticZenRule.Builder("Rule", CONDITION_ID) // no zen policy .build(), - ORIGIN_APP, "reasons", 0); + ORIGIN_APP, "reasons", CUSTOM_PKG_UID); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls()) .isEqualTo(STATE_DISALLOW); } @@ -2988,7 +2995,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowReminders(true) .build()) .build(), - ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", SYSTEM_UID); mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, new AutomaticZenRule.Builder("Rule", CONDITION_ID) @@ -2996,9 +3003,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS) .build()) .build(), - ORIGIN_APP, "reasons", 0); + ORIGIN_APP, "reasons", CUSTOM_PKG_UID); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls()) .isEqualTo(STATE_ALLOW); // from update assertThat(savedRule.getZenPolicy().getPriorityCallSenders()) @@ -4441,7 +4449,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.triggerDescription = TRIGGER_DESC; mZenModeHelper.mConfig.automaticRules.put(rule.id, rule); - AutomaticZenRule actual = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, rule.id); + AutomaticZenRule actual = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, rule.id, + SYSTEM_UID); assertEquals(NAME, actual.getName()); assertEquals(OWNER, actual.getOwner()); @@ -4508,16 +4517,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); // Checks the name can be changed by the app because the user has not modified it. AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) .setName("NewName") .build(); mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP, - "reason", SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + "reason", CUSTOM_PKG_UID); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID); assertThat(rule.getName()).isEqualTo("NewName"); // The user modifies some other field in the rule, which makes the rule as a whole not @@ -4534,8 +4544,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setName("NewAppName") .build(); mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP, - "reason", SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + "reason", CUSTOM_PKG_UID); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID); assertThat(rule.getName()).isEqualTo("NewAppName"); // The user modifies the name. @@ -4544,7 +4554,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID); assertThat(rule.getName()).isEqualTo("UserProvidedName"); // The app is no longer able to modify the name. @@ -4552,8 +4562,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setName("NewAppName") .build(); mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP, - "reason", SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + "reason", CUSTOM_PKG_UID); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID); assertThat(rule.getName()).isEqualTo("UserProvidedName"); } @@ -4568,8 +4578,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); // Modifies the filter, icon, zen policy, and device effects ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy()) @@ -4589,7 +4600,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Update the rule with the AZR from origin user. mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID); // UPDATE_ORIGIN_USER should change the bitmask and change the values. assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); @@ -4625,8 +4636,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); // Modifies the icon, zen policy and device effects ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy()) @@ -4646,7 +4658,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Update the rule with the AZR from origin systemUI. mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_SYSTEM, "reason", SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID); // UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI should change the value but NOT update the bitmask. assertThat(rule.getIconResId()).isEqualTo(ICON_RES_ID); @@ -4675,8 +4687,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); ZenPolicy policy = new ZenPolicy.Builder() .allowReminders(true) @@ -4693,7 +4706,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Since the rule is not already user modified, UPDATE_ORIGIN_APP can modify the rule. // The bitmask is not modified. mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP, - "reason", SYSTEM_UID); + "reason", CUSTOM_PKG_UID); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(storedRule.userModifiedFields).isEqualTo(0); @@ -4717,9 +4730,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Zen rule update coming from the app again. This cannot fully update the rule, because // the rule is already considered user modified. mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleIdUser, azrUpdate, ORIGIN_APP, - "reason", SYSTEM_UID); + "reason", CUSTOM_PKG_UID); AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, - ruleIdUser); + ruleIdUser, CUSTOM_PKG_UID); // The app can only change the value if the rule is not already user modified, // so the rule is not changed, and neither is the bitmask. @@ -4749,8 +4762,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build()) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); // The values are modified but the bitmask is not. assertThat(rule.getZenPolicy().getPriorityCategoryReminders()) @@ -4771,7 +4785,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID); AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase) // Sets Device Effects to null @@ -4781,8 +4795,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Zen rule update coming from app, but since the rule isn't already // user modified, it can be updated. mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr, ORIGIN_APP, "reason", - SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + CUSTOM_PKG_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); // When AZR's ZenDeviceEffects is null, the updated rule's device effects are kept. assertThat(rule.getDeviceEffects()).isEqualTo(zde); @@ -4797,8 +4812,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID); AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase) // Set zen policy to null @@ -4808,8 +4822,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Zen rule update coming from app, but since the rule isn't already // user modified, it can be updated. mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr, ORIGIN_APP, "reason", - SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + CUSTOM_PKG_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); // When AZR's ZenPolicy is null, we expect the updated rule's policy to be unchanged // (equivalent to the provided policy, with additional fields filled in with defaults). @@ -4829,8 +4844,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID); // Create a fully populated ZenPolicy. ZenPolicy policy = new ZenPolicy.Builder() @@ -4860,7 +4874,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Default config defined in getDefaultConfigParser() is used as the original rule. mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr, ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); // New ZenPolicy differs from the default config assertThat(rule.getZenPolicy()).isNotNull(); @@ -4890,8 +4905,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) @@ -4903,7 +4919,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Applies the update to the rule. mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr, ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID); // New ZenDeviceEffects is used; all fields considered set, since previously were null. assertThat(rule.getDeviceEffects()).isNotNull(); @@ -5286,7 +5302,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, update, ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); - AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + SYSTEM_UID); assertThat(result).isNotNull(); assertThat(result.getOwner().getClassName()).isEqualTo("brand.new.cps"); } @@ -5306,7 +5323,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, update, ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); - AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID); assertThat(result).isNotNull(); assertThat(result.getOwner().getClassName()).isEqualTo("old.third.party.cps"); } @@ -5518,8 +5536,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime()) - .isEqualTo(1000); + assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID).getCreationTime()).isEqualTo(1000); // User customizes it. AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) @@ -5546,7 +5564,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // - ZenPolicy is the one that the user had set. // - rule still has the user-modified fields. AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, - newRuleId); + newRuleId, CUSTOM_PKG_UID); assertThat(finalRule.getCreationTime()).isEqualTo(1000); // And not 3000. assertThat(newRuleId).isEqualTo(ruleId); assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); @@ -5575,8 +5593,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime()) - .isEqualTo(1000); + assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID).getCreationTime()).isEqualTo(1000); // App deletes it. mTestClock.advanceByMillis(1000); @@ -5592,7 +5610,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Verify that the rule was recreated. This means id and creation time are new. AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, - newRuleId); + newRuleId, CUSTOM_PKG_UID); assertThat(finalRule.getCreationTime()).isEqualTo(3000); assertThat(newRuleId).isNotEqualTo(ruleId); } @@ -5609,8 +5627,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime()) - .isEqualTo(1000); + assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID) + .getCreationTime()).isEqualTo(1000); // User customizes it. mTestClock.advanceByMillis(1000); @@ -5637,7 +5655,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Verify that the rule was recreated. This means id and creation time are new, and the rule // matches the latest data supplied to addAZR. AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, - newRuleId); + newRuleId, CUSTOM_PKG_UID); assertThat(finalRule.getCreationTime()).isEqualTo(4000); assertThat(newRuleId).isNotEqualTo(ruleId); assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); @@ -5660,8 +5678,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime()) - .isEqualTo(1000); + assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + CUSTOM_PKG_UID).getCreationTime()).isEqualTo(1000); // User customizes it. mTestClock.advanceByMillis(1000); @@ -5686,7 +5704,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Verify that the rule was recreated. This means id and creation time are new. AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, - newRuleId); + newRuleId, CUSTOM_PKG_UID); assertThat(finalRule.getCreationTime()).isEqualTo(4000); assertThat(newRuleId).isNotEqualTo(ruleId); } @@ -5728,7 +5746,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Verify that the rule was NOT restored: assertThat(newRuleId).isNotEqualTo(ruleId); AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, - newRuleId); + newRuleId, CUSTOM_PKG_UID); assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); assertThat(finalRule.getOwner()).isEqualTo(new ComponentName("second", "owner")); @@ -5869,7 +5887,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // The rule is restored... assertThat(newRuleId).isEqualTo(ruleId); AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, - newRuleId); + newRuleId, CUSTOM_PKG_UID); assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); // ... but it is NOT active @@ -5923,7 +5941,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // The rule is restored... assertThat(newRuleId).isEqualTo(ruleId); AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, - newRuleId); + newRuleId, CUSTOM_PKG_UID); assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); // ... but it is NEITHER active NOR snoozed. @@ -6005,22 +6023,22 @@ public class ZenModeHelperTest extends UiServiceTestCase { ORIGIN_APP, "reasons", CUSTOM_PKG_UID); // Null condition -> STATE_FALSE - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id)) + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id, CUSTOM_PKG_UID)) .isEqualTo(Condition.STATE_FALSE); mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id)) + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id, CUSTOM_PKG_UID)) .isEqualTo(Condition.STATE_TRUE); mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, CONDITION_FALSE, ORIGIN_APP, CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id)) + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id, CUSTOM_PKG_UID)) .isEqualTo(Condition.STATE_FALSE); mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, id, ORIGIN_APP, "", CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id)) + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id, CUSTOM_PKG_UID)) .isEqualTo(Condition.STATE_UNKNOWN); } @@ -6036,8 +6054,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, "systemRule")) - .isEqualTo(Condition.STATE_UNKNOWN); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, "systemRule", + CUSTOM_PKG_UID)).isEqualTo(Condition.STATE_UNKNOWN); } @Test @@ -6063,7 +6081,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags(FLAG_MODES_API) - public void setAutomaticZenRuleState_conditionForNotOwnedRule_ignored() { + public void setAutomaticZenRuleStateFromConditionProvider_conditionForNotOwnedRule_ignored() { // Assume existence of an other-package-owned rule that is currently ACTIVE. assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); ZenRule otherRule = newZenRule("another.package", Instant.now(), null); @@ -6075,7 +6093,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); // Should be ignored. - mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, otherRule.conditionId, + mZenModeHelper.setAutomaticZenRuleStateFromConditionProvider(UserHandle.CURRENT, + otherRule.conditionId, new Condition(otherRule.conditionId, "off", Condition.STATE_FALSE), ORIGIN_APP, CUSTOM_PKG_UID); @@ -6182,7 +6201,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); // From user, update that rule's interruption filter. - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + SYSTEM_UID); AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); @@ -6214,7 +6234,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); // From user, update something in that rule, but not the interruption filter. - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + SYSTEM_UID); AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) .setName("Renamed") .build(); @@ -6315,7 +6336,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = ZenModeConfig.implicitRuleId(mContext.getPackageName()); // User chooses a new name. - AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + SYSTEM_UID); mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, new AutomaticZenRule.Builder(azr).setName("User chose this").build(), ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); @@ -6414,7 +6436,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.getZenPolicy()).allowMedia(true).build(); // From user, update that rule's policy. - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + SYSTEM_UID); ZenPolicy userUpdateZenPolicy = new ZenPolicy.Builder().disallowAllSounds() .allowAlarms(true).build(); AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) @@ -6456,7 +6479,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.getZenPolicy()).allowMedia(true).build(); // From user, update something in that rule, but not the ZenPolicy. - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + SYSTEM_UID); AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) .setName("Rule renamed, not touching policy") .build(); @@ -6509,7 +6533,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = ZenModeConfig.implicitRuleId(mContext.getPackageName()); // User chooses a new name. - AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, + SYSTEM_UID); mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, new AutomaticZenRule.Builder(azr).setName("User chose this").build(), ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); @@ -6645,7 +6670,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { new AutomaticZenRule.Builder("Rule", CONDITION_ID).setIconResId(resourceId).build(), ORIGIN_APP, "reason", CUSTOM_PKG_UID); AutomaticZenRule storedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, - ruleId); + ruleId, CUSTOM_PKG_UID); assertThat(storedRule.getIconResId()).isEqualTo(0); } @@ -7087,8 +7112,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); implicitRule = getZenRule(implicitRuleId(CUSTOM_PKG_NAME)); - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, implicitRule.id)) - .isEqualTo(STATE_TRUE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, implicitRule.id, + CUSTOM_PKG_UID)).isEqualTo(STATE_TRUE); assertThat(implicitRule.isActive()).isTrue(); assertThat(implicitRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); } @@ -7108,8 +7133,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); implicitRule = getZenRule(implicitRuleId(CUSTOM_PKG_NAME)); - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, implicitRule.id)) - .isEqualTo(STATE_FALSE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, implicitRule.id, + CUSTOM_PKG_UID)).isEqualTo(STATE_FALSE); assertThat(implicitRule.isActive()).isFalse(); assertThat(implicitRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); } @@ -7177,7 +7202,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId)) + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId, SYSTEM_UID)) .isEqualTo(STATE_TRUE); ZenRule zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); @@ -7192,14 +7217,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); if (Flags.modesUi()) { - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId)) - .isEqualTo(STATE_TRUE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId, + SYSTEM_UID)).isEqualTo(STATE_TRUE); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); assertThat(zenRule.condition).isNull(); } else { - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId)) - .isEqualTo(STATE_FALSE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId, + SYSTEM_UID)).isEqualTo(STATE_FALSE); } } @@ -7218,7 +7243,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "snooze", STATE_FALSE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId)) + assertThat( + mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID)) .isEqualTo(STATE_FALSE); ZenRule zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); @@ -7232,7 +7258,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { TypedXmlPullParser parser = getParserForByteStream(xmlBytes); mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); - assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId)) + assertThat( + mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID)) .isEqualTo(STATE_TRUE); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java new file mode 100644 index 000000000000..09f573cd1ee0 --- /dev/null +++ b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java @@ -0,0 +1,158 @@ +/* + * 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.vibrator; + +import static com.google.common.truth.Truth.assertThat; + +import android.hardware.vibrator.IVibrator; +import android.os.VibratorInfo; +import android.os.vibrator.BasicPwleSegment; +import android.os.vibrator.Flags; +import android.os.vibrator.PwleSegment; +import android.os.vibrator.StepSegment; +import android.os.vibrator.VibrationEffectSegment; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; + +public class BasicToPwleSegmentAdapterTest { + + private static final float TEST_RESONANT_FREQUENCY = 150; + private static final float[] TEST_FREQUENCIES = + new float[]{90f, 120f, 150f, 60f, 30f, 210f, 270f, 300f, 240f, 180f}; + private static final float[] TEST_OUTPUT_ACCELERATIONS = + new float[]{1.2f, 1.8f, 2.4f, 0.6f, 0.1f, 2.2f, 1.0f, 0.5f, 1.9f, 3.0f}; + + private static final VibratorInfo.FrequencyProfile TEST_FREQUENCY_PROFILE = + new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, TEST_FREQUENCIES, + TEST_OUTPUT_ACCELERATIONS); + + private static final VibratorInfo.FrequencyProfile EMPTY_FREQUENCY_PROFILE = + new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, null, null); + + private BasicToPwleSegmentAdapter mAdapter; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() throws Exception { + mAdapter = new BasicToPwleSegmentAdapter(); + } + + @Test + @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_withFeatureFlagDisabled_returnsOriginalSegments() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20), + new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100), + new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50))); + List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments); + + VibratorInfo vibratorInfo = createVibratorInfo( + TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1)) + .isEqualTo(-1); + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(originalSegments); + } + + @Test + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_noPwleCapability_returnsOriginalSegments() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20), + new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100), + new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50))); + List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments); + + VibratorInfo vibratorInfo = createVibratorInfo(TEST_FREQUENCY_PROFILE); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1)) + .isEqualTo(-1); + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(originalSegments); + } + + @Test + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_invalidFrequencyProfile_returnsOriginalSegments() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20), + new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100), + new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50))); + List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments); + VibratorInfo vibratorInfo = createVibratorInfo( + EMPTY_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1)) + .isEqualTo(-1); + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(originalSegments); + } + + @Test + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_withPwleCapability_adaptSegmentsCorrectly() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100), + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100), + new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100), + new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100))); + List<VibrationEffectSegment> expectedSegments = Arrays.asList( + new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100), + // startAmplitude, endAmplitude, startFrequencyHz, endFrequencyHz, duration + new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100), + new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100), + new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100)); + VibratorInfo vibratorInfo = createVibratorInfo( + TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(expectedSegments); + } + + private static VibratorInfo createVibratorInfo(VibratorInfo.FrequencyProfile frequencyProfile, + int... capabilities) { + return new VibratorInfo.Builder(0) + .setCapabilities(IntStream.of(capabilities).reduce((a, b) -> a | b).orElse(0)) + .setFrequencyProfile(frequencyProfile) + .build(); + } +} diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java index 9d4d94bebfd9..85ef466b2477 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java @@ -758,6 +758,18 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES) + public void testKeyGestureToggleVoiceAccess() { + Assert.assertTrue( + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); + mPhoneWindowManager.assertVoiceAccess(true); + + Assert.assertTrue( + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); + mPhoneWindowManager.assertVoiceAccess(false); + } + + @Test public void testKeyGestureToggleDoNotDisturb() { mPhoneWindowManager.overrideZenMode(Settings.Global.ZEN_MODE_OFF); Assert.assertTrue( diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 6c48ba26a475..4ff3d433632a 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -201,6 +201,8 @@ class TestPhoneWindowManager { private boolean mIsTalkBackEnabled; private boolean mIsTalkBackShortcutGestureEnabled; + private boolean mIsVoiceAccessEnabled; + private Intent mBrowserIntent; private Intent mSmsIntent; @@ -225,6 +227,18 @@ class TestPhoneWindowManager { } } + private class TestVoiceAccessShortcutController extends VoiceAccessShortcutController { + TestVoiceAccessShortcutController(Context context) { + super(context); + } + + @Override + boolean toggleVoiceAccess(int currentUserId) { + mIsVoiceAccessEnabled = !mIsVoiceAccessEnabled; + return mIsVoiceAccessEnabled; + } + } + private class TestInjector extends PhoneWindowManager.Injector { TestInjector(Context context, WindowManagerPolicy.WindowManagerFuncs funcs) { super(context, funcs); @@ -260,6 +274,10 @@ class TestPhoneWindowManager { return new TestTalkbackShortcutController(mContext); } + VoiceAccessShortcutController getVoiceAccessShortcutController() { + return new TestVoiceAccessShortcutController(mContext); + } + WindowWakeUpPolicy getWindowWakeUpPolicy() { return mWindowWakeUpPolicy; } @@ -1024,6 +1042,11 @@ class TestPhoneWindowManager { Assert.assertEquals(expectEnabled, mIsTalkBackEnabled); } + void assertVoiceAccess(boolean expectEnabled) { + mTestLooper.dispatchAll(); + Assert.assertEquals(expectEnabled, mIsVoiceAccessEnabled); + } + void assertKeyGestureEventSentToKeyGestureController(int gestureType) { verify(mInputManagerInternal) .handleKeyGestureInKeyGestureController(anyInt(), any(), anyInt(), eq(gestureType)); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index c9cbe0fa08c5..6fad82b26808 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -210,7 +210,7 @@ public class ActivityRecordTests extends WindowTestsBase { } private TestStartingWindowOrganizer registerTestStartingWindowOrganizer() { - return new TestStartingWindowOrganizer(mAtm); + return new TestStartingWindowOrganizer(mAtm, mDisplayContent); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java index 9d191cea8acb..a0727a7af87b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java @@ -335,7 +335,7 @@ public class AppCompatOrientationOverridesTest extends WindowTestsBase { } private AppCompatOrientationOverrides getTopOrientationOverrides() { - return activity().top().mAppCompatController.getAppCompatOrientationOverrides(); + return activity().top().mAppCompatController.getOrientationOverrides(); } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java index a21ab5de5de2..4faa71451a4d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java @@ -601,7 +601,7 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase { } private AppCompatOrientationOverrides getTopOrientationOverrides() { - return activity().top().mAppCompatController.getAppCompatOrientationOverrides(); + return activity().top().mAppCompatController.getOrientationOverrides(); } private AppCompatOrientationPolicy getTopAppCompatOrientationPolicy() { diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java index 463254caa845..50419d46f48f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java @@ -159,8 +159,8 @@ public class AppCompatReachabilityOverridesTest extends WindowTestsBase { @Override void onPostActivityCreation(@NonNull ActivityRecord activity) { super.onPostActivityCreation(activity); - spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides()); - activity.mAppCompatController.getAppCompatReachabilityPolicy() + spyOn(activity.mAppCompatController.getReachabilityOverrides()); + activity.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier); } @@ -196,7 +196,7 @@ public class AppCompatReachabilityOverridesTest extends WindowTestsBase { @NonNull private AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return activity().top().mAppCompatController.getAppCompatReachabilityOverrides(); + return activity().top().mAppCompatController.getReachabilityOverrides(); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java index ddc4de9cfd8a..09b8bce2c930 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java @@ -246,8 +246,8 @@ public class AppCompatReachabilityPolicyTest extends WindowTestsBase { @Override void onPostActivityCreation(@NonNull ActivityRecord activity) { super.onPostActivityCreation(activity); - spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides()); - activity.mAppCompatController.getAppCompatReachabilityPolicy() + spyOn(activity.mAppCompatController.getReachabilityOverrides()); + activity.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier); } @@ -281,12 +281,12 @@ public class AppCompatReachabilityPolicyTest extends WindowTestsBase { @NonNull private AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return activity().top().mAppCompatController.getAppCompatReachabilityOverrides(); + return activity().top().mAppCompatController.getReachabilityOverrides(); } @NonNull private AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() { - return activity().top().mAppCompatController.getAppCompatReachabilityPolicy(); + return activity().top().mAppCompatController.getReachabilityPolicy(); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java index b8d554b405d1..98a4fb3c473f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java @@ -184,12 +184,12 @@ public class AppCompatResizeOverridesTest extends WindowTestsBase { void checkShouldOverrideForceResizeApp(boolean expected) { Assert.assertEquals(expected, activity().top().mAppCompatController - .getAppCompatResizeOverrides().shouldOverrideForceResizeApp()); + .getResizeOverrides().shouldOverrideForceResizeApp()); } void checkShouldOverrideForceNonResizeApp(boolean expected) { Assert.assertEquals(expected, activity().top().mAppCompatController - .getAppCompatResizeOverrides().shouldOverrideForceNonResizeApp()); + .getResizeOverrides().shouldOverrideForceNonResizeApp()); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 5486aa34b5fa..dfd10ec86a20 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1183,6 +1183,18 @@ public class DisplayContentTests extends WindowTestsBase { assertEquals(prev, mDisplayContent.getLastOrientationSource()); // The top will use the rotation from "prev" with fixed rotation. assertTrue(top.hasFixedRotationTransform()); + + mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp(); + assertFalse(top.hasFixedRotationTransform()); + + // Assume that the requested orientation of "prev" is landscape. And the display is also + // rotated to landscape. The activities from bottom to top are TaskB{"prev, "behindTop"}, + // TaskB{"top"}. Then "behindTop" should also get landscape according to ORIENTATION_BEHIND + // instead of resolving as undefined which causes to unexpected fixed portrait rotation. + final ActivityRecord behindTop = new ActivityBuilder(mAtm).setTask(prev.getTask()) + .setOnTop(false).setScreenOrientation(SCREEN_ORIENTATION_BEHIND).build(); + mDisplayContent.applyFixedRotationForNonTopVisibleActivityIfNeeded(behindTop); + assertFalse(behindTop.hasFixedRotationTransform()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index ea925c019b77..4854f0d948b4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -88,7 +88,7 @@ public class DisplayPolicyTests extends WindowTestsBase { } private WindowState createDreamWindow() { - final WindowState win = createDreamWindow(null, TYPE_BASE_APPLICATION, "dream"); + final WindowState win = createDreamWindow("dream", TYPE_BASE_APPLICATION); final WindowManager.LayoutParams attrs = win.mAttrs; attrs.width = MATCH_PARENT; attrs.height = MATCH_PARENT; diff --git a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java index 3e87f1f96fcd..ee9673f5ee77 100644 --- a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java @@ -177,15 +177,16 @@ public class PersisterQueueTests { assertTrue("Target didn't call callback enough times.", mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE)); + // Wait until writing thread is waiting, which indicates the thread is waiting for new tasks + // to appear. + assertTrue("Failed to wait until the writing thread is waiting.", + mTarget.waitUntilWritingThreadIsWaiting(TIMEOUT_ALLOWANCE)); + // Second item mFactory.setExpectedProcessedItemNumber(1); mListener.setExpectedOnPreProcessItemCallbackTimes(1); dispatchTime = SystemClock.uptimeMillis(); - // Synchronize on the instance to make sure we schedule the item after it starts to wait for - // task indefinitely. - synchronized (mTarget) { - mTarget.addItem(mFactory.createItem(), false); - } + mTarget.addItem(mFactory.createItem(), false); assertTrue("Target didn't process item enough times.", mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE)); assertEquals("Target didn't process all items.", 2, mFactory.getTotalProcessedItemCount()); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 9d9f24cb50f2..07ee09a350d9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -185,31 +185,46 @@ public class SizeCompatTests extends WindowTestsBase { } private ActivityRecord setUpApp(DisplayContent display) { - return setUpApp(display, null /* appBuilder */); + return setUpApp(display, null /* appBuilder */, null /* taskBuilder */); } private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder appBuilder) { + return setUpApp(display, appBuilder, null /* taskBuilder */); + } + + private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder aBuilder, + TaskBuilder tBuilder) { // Use the real package name (com.android.frameworks.wmtests) so that // EnableCompatChanges/DisableCompatChanges can take effect. // Otherwise the fake WindowTestsBase.DEFAULT_COMPONENT_PACKAGE_NAME will make // PlatformCompat#isChangeEnabledByPackageName always return default value. final ComponentName componentName = ComponentName.createRelative( mContext, SizeCompatTests.class.getName()); - mTask = new TaskBuilder(mSupervisor).setDisplay(display).setComponent(componentName) + final TaskBuilder taskBuilder = tBuilder != null ? tBuilder : new TaskBuilder(mSupervisor); + mTask = taskBuilder.setDisplay(display).setComponent(componentName) .build(); - final ActivityBuilder builder = appBuilder != null ? appBuilder : new ActivityBuilder(mAtm); - mActivity = builder.setTask(mTask).setComponent(componentName).build(); + final ActivityBuilder appBuilder = aBuilder != null ? aBuilder : new ActivityBuilder(mAtm); + mActivity = appBuilder.setTask(mTask).setComponent(componentName).build(); doReturn(false).when(mActivity).isImmersiveMode(any()); return mActivity; } private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh) { - return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */); + return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, null /* taskBuilder */); } private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder) { + return setUpDisplaySizeWithApp(dw, dh, appBuilder, null /* taskBuilder */); + } + + private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, TaskBuilder taskBuilder) { + return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, taskBuilder); + } + + private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder, + TaskBuilder taskBuilder) { final TestDisplayContent.Builder builder = new TestDisplayContent.Builder(mAtm, dw, dh); - return setUpApp(builder.build(), appBuilder); + return setUpApp(builder.build(), appBuilder, taskBuilder); } private void setUpLargeScreenDisplayWithApp(int dw, int dh) { @@ -330,7 +345,7 @@ public class SizeCompatTests extends WindowTestsBase { if (horizontalReachability) { final Consumer<Integer> doubleClick = (Integer x) -> { - mActivity.mAppCompatController.getAppCompatReachabilityPolicy() + mActivity.mAppCompatController.getReachabilityPolicy() .handleDoubleTap(x, displayHeight / 2); mActivity.mRootWindowContainer.performSurfacePlacement(); }; @@ -360,7 +375,7 @@ public class SizeCompatTests extends WindowTestsBase { } else { final Consumer<Integer> doubleClick = (Integer y) -> { - mActivity.mAppCompatController.getAppCompatReachabilityPolicy() + mActivity.mAppCompatController.getReachabilityPolicy() .handleDoubleTap(displayWidth / 2, y); mActivity.mRootWindowContainer.performSurfacePlacement(); }; @@ -421,7 +436,7 @@ public class SizeCompatTests extends WindowTestsBase { final Consumer<Integer> doubleClick = (Integer y) -> { - activity.mAppCompatController.getAppCompatReachabilityPolicy() + activity.mAppCompatController.getReachabilityPolicy() .handleDoubleTap(dw / 2, y); activity.mRootWindowContainer.performSurfacePlacement(); }; @@ -834,7 +849,7 @@ public class SizeCompatTests extends WindowTestsBase { // Change the fixed orientation. mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); assertTrue(mActivity.isRelaunching()); - assertTrue(mActivity.mAppCompatController.getAppCompatOrientationOverrides() + assertTrue(mActivity.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged()); assertFitted(); @@ -3427,7 +3442,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ false); final AppCompatReachabilityOverrides reachabilityOverrides = - mActivity.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivity.mAppCompatController.getReachabilityOverrides(); assertFalse(reachabilityOverrides.isVerticalReachabilityEnabled()); assertFalse(reachabilityOverrides.isHorizontalReachabilityEnabled()); } @@ -3451,7 +3466,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // Horizontal reachability is disabled because the app is in split screen. - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3475,7 +3490,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // Vertical reachability is disabled because the app is in split screen. - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3498,7 +3513,7 @@ public class SizeCompatTests extends WindowTestsBase { // Vertical reachability is disabled because the app does not match parent width assertNotEquals(mActivity.getScreenResolvedBounds().width(), mActivity.mDisplayContent.getBounds().width()); - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3516,7 +3531,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(new Rect(0, 0, 0, 0), mActivity.getBounds()); // Vertical reachability is still enabled as resolved bounds is not empty - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3533,7 +3548,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(new Rect(0, 0, 0, 0), mActivity.getBounds()); // Horizontal reachability is still enabled as resolved bounds is not empty - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3548,7 +3563,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE, SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3563,7 +3578,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE, SCREEN_ORIENTATION_LANDSCAPE); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3585,7 +3600,7 @@ public class SizeCompatTests extends WindowTestsBase { // Horizontal reachability is disabled because the app does not match parent height assertNotEquals(mActivity.getScreenResolvedBounds().height(), mActivity.mDisplayContent.getBounds().height()); - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3608,7 +3623,7 @@ public class SizeCompatTests extends WindowTestsBase { // Horizontal reachability is enabled because the app matches parent height assertEquals(mActivity.getScreenResolvedBounds().height(), mActivity.mDisplayContent.getBounds().height()); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3631,7 +3646,7 @@ public class SizeCompatTests extends WindowTestsBase { // Vertical reachability is enabled because the app matches parent width assertEquals(mActivity.getScreenResolvedBounds().width(), mActivity.mDisplayContent.getBounds().width()); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -4315,7 +4330,7 @@ public class SizeCompatTests extends WindowTestsBase { // Make sure app doesn't jump to top (default tabletop position) when unfolding. assertEquals(1.0f, mActivity.mAppCompatController - .getAppCompatReachabilityOverrides().getVerticalPositionMultiplier(mActivity + .getReachabilityOverrides().getVerticalPositionMultiplier(mActivity .getParent().getConfiguration()), 0); // Simulate display fully open after unfolding. @@ -4323,7 +4338,7 @@ public class SizeCompatTests extends WindowTestsBase { doReturn(false).when(mActivity.mDisplayContent).inTransition(); assertEquals(1.0f, mActivity.mAppCompatController - .getAppCompatReachabilityOverrides().getVerticalPositionMultiplier(mActivity + .getReachabilityOverrides().getVerticalPositionMultiplier(mActivity .getParent().getConfiguration()), 0); } @@ -4469,6 +4484,80 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(new Rect(0, 0, 1000, 2800), bounds); } + @Test + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testUserAspectRatioOverridesNotAppliedToResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + spyOn(mActivity.mWmService.mAppCompatConfiguration); + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) + .isUserAppAspectRatioSettingsEnabled(); + final AppCompatController appCompatController = mActivity.mAppCompatController; + final AppCompatAspectRatioOverrides aspectRatioOverrides = + appCompatController.getAppCompatAspectRatioOverrides(); + spyOn(aspectRatioOverrides); + // Set user aspect ratio override. + doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides) + .getUserMinAspectRatioOverrideCode(); + + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false); + assertFalse(appCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied()); + } + + @Test + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testUserAspectRatioOverridesAppliedToNonResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + spyOn(mActivity.mWmService.mAppCompatConfiguration); + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) + .isUserAppAspectRatioSettingsEnabled(); + final AppCompatController appCompatController = mActivity.mAppCompatController; + final AppCompatAspectRatioOverrides aspectRatioOverrides = + appCompatController.getAppCompatAspectRatioOverrides(); + spyOn(aspectRatioOverrides); + // Set user aspect ratio override. + doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides) + .getUserMinAspectRatioOverrideCode(); + + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true); + assertTrue(appCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied()); + } + + @Test + @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO, + ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE}) + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testSystemAspectRatioOverridesNotAppliedToResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false); + + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isAspectRatioApplied()); + } + + @Test + @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO, + ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE}) + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testSystemAspectRatioOverridesAppliedToNonResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true); + + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isAspectRatioApplied()); + } + private void assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity( float letterboxVerticalPositionMultiplier, Rect fixedOrientationLetterbox, Rect sizeCompatUnscaled, Rect sizeCompatScaled) { @@ -5028,7 +5117,7 @@ public class SizeCompatTests extends WindowTestsBase { private void setUpAllowThinLetterboxed(boolean thinLetterboxAllowed) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivity.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivity.mAppCompatController.getReachabilityOverrides(); spyOn(reachabilityOverrides); doReturn(thinLetterboxAllowed).when(reachabilityOverrides) .allowVerticalReachabilityForThinLetterbox(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index d1f5d157560b..be79160c3a09 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -76,6 +76,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityManager; import android.app.ActivityThread; import android.app.IApplicationThread; import android.content.pm.ActivityInfo; @@ -1522,7 +1523,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_CONDENSE_CONFIGURATION_CHANGE_FOR_SIMPLE_MODE) public void setConfigurationChangeSettingsForUser_createsFromParcel_callsSettingImpl() throws Settings.SettingNotFoundException { - final int userId = 0; + final int currentUserId = ActivityManager.getCurrentUser(); final int forcedDensity = 400; final float forcedFontScaleFactor = 1.15f; final Parcelable.Creator<ConfigurationChangeSetting> creator = @@ -1536,10 +1537,10 @@ public class WindowManagerServiceTests extends WindowTestsBase { mWm.setConfigurationChangeSettingsForUser(settings, UserHandle.USER_CURRENT); - verify(mDisplayContent).setForcedDensity(forcedDensity, userId); + verify(mDisplayContent).setForcedDensity(forcedDensity, currentUserId); assertEquals(forcedFontScaleFactor, Settings.System.getFloat( mContext.getContentResolver(), Settings.System.FONT_SCALE), 0.1f /* delta */); - verify(mAtm).updateFontScaleIfNeeded(userId); + verify(mAtm).updateFontScaleIfNeeded(currentUserId); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 513ba1d49258..ab9abfc4a876 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -1001,7 +1001,6 @@ public class WindowStateTests extends WindowTestsBase { assertTrue(handleWrapper.isChanged()); assertTrue(testFlag(handle.inputConfig, InputConfig.WATCH_OUTSIDE_TOUCH)); - assertFalse(testFlag(handle.inputConfig, InputConfig.PREVENT_SPLITTING)); assertTrue(testFlag(handle.inputConfig, InputConfig.DISABLE_USER_ACTIVITY)); // The window of standard resizable task should not use surface crop as touchable region. assertFalse(handle.replaceTouchableRegionWithCrop); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index ce0d91264063..37d2a7511d98 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -478,7 +478,7 @@ public class WindowTestsBase extends SystemServiceTestsBase { } private WindowState createCommonWindow(WindowState parent, int type, String name) { - final WindowState win = createWindow(parent, type, name); + final WindowState win = newWindowBuilder(name, type).setParent(parent).build(); // Prevent common windows from been IME targets. win.mAttrs.flags |= FLAG_NOT_FOCUSABLE; return win; @@ -502,7 +502,8 @@ public class WindowTestsBase extends SystemServiceTestsBase { } WindowState createNavBarWithProvidedInsets(DisplayContent dc) { - final WindowState navbar = createWindow(null, TYPE_NAVIGATION_BAR, dc, "navbar"); + final WindowState navbar = newWindowBuilder("navbar", TYPE_NAVIGATION_BAR).setDisplay( + dc).build(); final Binder owner = new Binder(); navbar.mAttrs.providedInsets = new InsetsFrameProvider[] { new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars()) @@ -513,7 +514,8 @@ public class WindowTestsBase extends SystemServiceTestsBase { } WindowState createStatusBarWithProvidedInsets(DisplayContent dc) { - final WindowState statusBar = createWindow(null, TYPE_STATUS_BAR, dc, "statusBar"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_STATUS_BAR).setDisplay( + dc).build(); final Binder owner = new Binder(); statusBar.mAttrs.providedInsets = new InsetsFrameProvider[] { new InsetsFrameProvider(owner, 0, WindowInsets.Type.statusBars()) @@ -575,92 +577,13 @@ public class WindowTestsBase extends SystemServiceTestsBase { WindowState createAppWindow(Task task, int type, String name) { final ActivityRecord activity = createNonAttachedActivityRecord(task.getDisplayContent()); task.addChild(activity, 0); - return createWindow(null, type, activity, name); + return newWindowBuilder(name, type).setWindowToken(activity).build(); } - WindowState createDreamWindow(WindowState parent, int type, String name) { + WindowState createDreamWindow(String name, int type) { final WindowToken token = createWindowToken( mDisplayContent, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_DREAM, type); - return createWindow(parent, type, token, name); - } - - // TODO: Move these calls to a builder? - WindowState createWindow(WindowState parent, int type, String name) { - return (parent == null) - ? createWindow(parent, type, mDisplayContent, name) - : createWindow(parent, type, parent.mToken, name); - } - - WindowState createWindow(WindowState parent, int type, String name, int ownerId) { - return (parent == null) - ? createWindow(parent, type, mDisplayContent, name, ownerId) - : createWindow(parent, type, parent.mToken, name, ownerId); - } - - WindowState createWindow(WindowState parent, int windowingMode, int activityType, - int type, DisplayContent dc, String name) { - final WindowToken token = createWindowToken(dc, windowingMode, activityType, type); - return createWindow(parent, type, token, name); - } - - WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name) { - return createWindow( - parent, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type, dc, name); - } - - WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name, - int ownerId) { - final WindowToken token = createWindowToken( - dc, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type); - return createWindow(parent, type, token, name, ownerId); - } - - WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name, - boolean ownerCanAddInternalSystemWindow) { - final WindowToken token = createWindowToken( - dc, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type); - return createWindow(parent, type, token, name, 0 /* ownerId */, - ownerCanAddInternalSystemWindow); - } - - WindowState createWindow(WindowState parent, int type, WindowToken token, String name) { - return createWindow(parent, type, token, name, 0 /* ownerId */, - false /* ownerCanAddInternalSystemWindow */); - } - - WindowState createWindow(WindowState parent, int type, WindowToken token, String name, - int ownerId) { - return createWindow(parent, type, token, name, ownerId, - false /* ownerCanAddInternalSystemWindow */); - } - - WindowState createWindow(WindowState parent, int type, WindowToken token, String name, - int ownerId, boolean ownerCanAddInternalSystemWindow) { - return createWindow(parent, type, token, name, ownerId, ownerCanAddInternalSystemWindow, - mIWindow); - } - - WindowState createWindow(WindowState parent, int type, WindowToken token, String name, - int ownerId, boolean ownerCanAddInternalSystemWindow, IWindow iwindow) { - return createWindow(parent, type, token, name, ownerId, UserHandle.getUserId(ownerId), - ownerCanAddInternalSystemWindow, mWm, getTestSession(token), iwindow); - } - - static WindowState createWindow(WindowState parent, int type, WindowToken token, - String name, int ownerId, int userId, boolean ownerCanAddInternalSystemWindow, - WindowManagerService service, Session session, IWindow iWindow) { - SystemServicesTestRule.checkHoldsLock(service.mGlobalLock); - - final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(type); - attrs.setTitle(name); - attrs.packageName = "test"; - - final WindowState w = new WindowState(service, session, iWindow, token, parent, - OP_NONE, attrs, VISIBLE, ownerId, userId, ownerCanAddInternalSystemWindow); - // TODO: Probably better to make this call in the WindowState ctor to avoid errors with - // adding it to the token... - token.addWindow(w); - return w; + return newWindowBuilder(name, type).setWindowToken(token).build(); } static void makeWindowVisible(WindowState... windows) { @@ -1920,11 +1843,14 @@ public class WindowTestsBase extends SystemServiceTestsBase { private final WindowManagerService mWMService; private final SparseArray<IBinder> mTaskAppMap = new SparseArray<>(); private final HashMap<IBinder, WindowState> mAppWindowMap = new HashMap<>(); + private final DisplayContent mDisplayContent; - TestStartingWindowOrganizer(ActivityTaskManagerService service) { + TestStartingWindowOrganizer(ActivityTaskManagerService service, + DisplayContent displayContent) { mAtm = service; mWMService = mAtm.mWindowManager; mAtm.mTaskOrganizerController.registerTaskOrganizer(this); + mDisplayContent = displayContent; } @Override @@ -1933,10 +1859,11 @@ public class WindowTestsBase extends SystemServiceTestsBase { final ActivityRecord activity = ActivityRecord.forTokenLocked(info.appToken); IWindow iWindow = mock(IWindow.class); doReturn(mock(IBinder.class)).when(iWindow).asBinder(); - final WindowState window = WindowTestsBase.createWindow(null, - TYPE_APPLICATION_STARTING, activity, - "Starting window", 0 /* ownerId */, 0 /* userId*/, - false /* internalWindows */, mWMService, createTestSession(mAtm), iWindow); + // WindowToken is already passed, windowTokenCreator is not needed here. + final WindowState window = new WindowTestsBase.WindowStateBuilder("Starting window", + TYPE_APPLICATION_STARTING, mWMService, mDisplayContent, iWindow, + (unused) -> createTestSession(mAtm), + null /* windowTokenCreator */).setWindowToken(activity).build(); activity.mStartingWindow = window; mAppWindowMap.put(info.appToken, window); mTaskAppMap.put(info.taskInfo.taskId, info.appToken); diff --git a/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java b/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java index d49214ab718b..a9ae5f7dfc3f 100644 --- a/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java +++ b/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java @@ -16,6 +16,10 @@ package com.android.server.texttospeech; +import static android.content.Context.BIND_AUTO_CREATE; +import static android.content.Context.BIND_FOREGROUND_SERVICE; +import static android.content.Context.BIND_SCHEDULE_LIKE_TOP_APP; + import static com.android.internal.infra.AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS; import android.annotation.NonNull; @@ -95,7 +99,7 @@ final class TextToSpeechManagerPerUserService extends ITextToSpeechSessionCallback callback) { super(context, new Intent(TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE).setPackage(engine), - Context.BIND_AUTO_CREATE | Context.BIND_SCHEDULE_LIKE_TOP_APP, + BIND_AUTO_CREATE | BIND_SCHEDULE_LIKE_TOP_APP | BIND_FOREGROUND_SERVICE, userId, ITextToSpeechService.Stub::asInterface); mEngine = engine; diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index 7082f0028a5e..e65e4b05ef98 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -29,6 +29,7 @@ import android.annotation.SuppressAutoDoc; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; @@ -1886,6 +1887,34 @@ public class TelecomManager { } /** + * This test API determines the foreground service delegation state for a VoIP app that adds + * calls via {@link TelecomManager#addCall(CallAttributes, Executor, OutcomeReceiver, + * CallControlCallback, CallEventCallback)}. Foreground Service Delegation allows applications + * to operate in the background starting in Android 14 and is granted by Telecom via a request + * to the ActivityManager. + * + * @param handle of the voip app that is being checked + * @return true if the app has foreground service delegation. Otherwise, false. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_VOIP_CALL_MONITOR_REFACTOR) + @TestApi + public boolean hasForegroundServiceDelegation(@Nullable PhoneAccountHandle handle) { + ITelecomService service = getTelecomService(); + if (service != null) { + try { + return service.hasForegroundServiceDelegation(handle, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, + "RemoteException calling ITelecomService#hasForegroundServiceDelegation.", + e); + } + } + return false; + } + + /** * Return the line 1 phone number for given phone account. * * <p>Requires Permission: diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl index c85374e0b660..b32379ae4b1e 100644 --- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl +++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl @@ -409,4 +409,10 @@ interface ITelecomService { */ void addCall(in CallAttributes callAttributes, in ICallEventCallback callback, String callId, String callingPackage); + + /** + * @see TelecomServiceImpl#hasForegroundServiceDelegation + */ + boolean hasForegroundServiceDelegation(in PhoneAccountHandle phoneAccountHandle, + String callingPackage); } diff --git a/telephony/java/android/telephony/CellularIdentifierDisclosure.java b/telephony/java/android/telephony/CellularIdentifierDisclosure.java index 0b6a70feac9d..92c51ec9e84f 100644 --- a/telephony/java/android/telephony/CellularIdentifierDisclosure.java +++ b/telephony/java/android/telephony/CellularIdentifierDisclosure.java @@ -74,6 +74,14 @@ public final class CellularIdentifierDisclosure implements Parcelable { /** IMEI DETATCH INDICATION. Reference: 3GPP TS 24.008 9.2.14. * Applies to 2g and 3g networks. Used for circuit-switched detach. */ public static final int NAS_PROTOCOL_MESSAGE_IMSI_DETACH_INDICATION = 11; + /** Vendor-specific enumeration to identify a disclosure as potentially benign. + * Enables vendors to semantically classify disclosures based on their own logic. */ + @FlaggedApi(Flags.FLAG_VENDOR_SPECIFIC_CELLULAR_IDENTIFIER_DISCLOSURE_INDICATIONS) + public static final int NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_FALSE = 12; + /** Vendor-specific enumeration to identify a disclosure as potentially harmful. + * Enables vendors to semantically classify disclosures based on their own logic. */ + @FlaggedApi(Flags.FLAG_VENDOR_SPECIFIC_CELLULAR_IDENTIFIER_DISCLOSURE_INDICATIONS) + public static final int NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_TRUE = 13; /** @hide */ @Retention(RetentionPolicy.SOURCE) @@ -84,7 +92,9 @@ public final class CellularIdentifierDisclosure implements Parcelable { NAS_PROTOCOL_MESSAGE_AUTHENTICATION_AND_CIPHERING_RESPONSE, NAS_PROTOCOL_MESSAGE_REGISTRATION_REQUEST, NAS_PROTOCOL_MESSAGE_DEREGISTRATION_REQUEST, NAS_PROTOCOL_MESSAGE_CM_REESTABLISHMENT_REQUEST, - NAS_PROTOCOL_MESSAGE_CM_SERVICE_REQUEST, NAS_PROTOCOL_MESSAGE_IMSI_DETACH_INDICATION}) + NAS_PROTOCOL_MESSAGE_CM_SERVICE_REQUEST, NAS_PROTOCOL_MESSAGE_IMSI_DETACH_INDICATION, + NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_FALSE, + NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_TRUE}) public @interface NasProtocolMessage { } @@ -156,6 +166,14 @@ public final class CellularIdentifierDisclosure implements Parcelable { return mIsEmergency; } + /** + * @return if the modem vendor classifies the disclosure as benign. + */ + @FlaggedApi(Flags.FLAG_VENDOR_SPECIFIC_CELLULAR_IDENTIFIER_DISCLOSURE_INDICATIONS) + public boolean isBenign() { + return mNasProtocolMessage == NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_FALSE; + } + @Override public int describeContents() { return 0; diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 63a12816f783..b7b209b78300 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -3690,8 +3690,8 @@ public final class SatelliteManager { * @param list The list of provisioned satellite subscriber infos. * @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 {@code true}. + * If the request is successful, {@link OutcomeReceiver#onResult} + * will be called. * If the request is not successful, * {@link OutcomeReceiver#onError(Throwable)} will return an error with * a SatelliteException. @@ -3704,7 +3704,7 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public void provisionSatellite(@NonNull List<SatelliteSubscriberInfo> list, @NonNull @CallbackExecutor Executor executor, - @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) { + @NonNull OutcomeReceiver<Void, SatelliteException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -3718,8 +3718,8 @@ public final class SatelliteManager { if (resultData.containsKey(KEY_PROVISION_SATELLITE_TOKENS)) { boolean isUpdated = resultData.getBoolean(KEY_PROVISION_SATELLITE_TOKENS); - executor.execute(() -> Binder.withCleanCallingIdentity(() -> - callback.onResult(isUpdated))); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> callback.onResult(null))); } else { loge("KEY_REQUEST_PROVISION_TOKENS does not exist."); executor.execute(() -> Binder.withCleanCallingIdentity(() -> @@ -3751,8 +3751,8 @@ public final class SatelliteManager { * @param list The list of deprovisioned satellite subscriber infos. * @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 {@code true}. + * If the request is successful, {@link OutcomeReceiver#onResult} + * will be called. * If the request is not successful, * {@link OutcomeReceiver#onError(Throwable)} will return an error with * a SatelliteException. @@ -3765,7 +3765,7 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public void deprovisionSatellite(@NonNull List<SatelliteSubscriberInfo> list, @NonNull @CallbackExecutor Executor executor, - @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) { + @NonNull OutcomeReceiver<Void, SatelliteException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -3780,7 +3780,7 @@ public final class SatelliteManager { boolean isUpdated = resultData.getBoolean(KEY_DEPROVISION_SATELLITE_TOKENS); executor.execute(() -> Binder.withCleanCallingIdentity(() -> - callback.onResult(isUpdated))); + callback.onResult(null))); } else { loge("KEY_DEPROVISION_SATELLITE_TOKENS does not exist."); executor.execute(() -> Binder.withCleanCallingIdentity(() -> diff --git a/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java index b5dfb631609c..e18fad3eda79 100644 --- a/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java @@ -78,6 +78,9 @@ public interface SatelliteTransmissionUpdateCallback { /** * Called when framework receives a request to send a datagram. * + * Informs external apps that device is working on sending a datagram out and is in the process + * of checking if all the conditions required to send datagrams are met. + * * @param datagramType The type of the requested datagram. */ @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt index 08b5f38a4655..75bd5d157bb2 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt @@ -17,7 +17,6 @@ package com.android.server.wm.flicker.activityembedding.open import android.graphics.Rect -import android.platform.test.annotations.Presubmit import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest @@ -68,13 +67,21 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest } } - @Ignore("Not applicable to this CUJ.") override fun navBarWindowIsVisibleAtStartAndEnd() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun navBarWindowIsVisibleAtStartAndEnd() {} - @FlakyTest(bugId = 291575593) override fun entireScreenCovered() {} + @FlakyTest(bugId = 291575593) + @Test + override fun entireScreenCovered() {} - @Ignore("Not applicable to this CUJ.") override fun statusBarWindowIsAlwaysVisible() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun statusBarWindowIsAlwaysVisible() {} - @Ignore("Not applicable to this CUJ.") override fun statusBarLayerPositionAtStartAndEnd() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun statusBarLayerPositionAtStartAndEnd() {} /** Transition begins with a split. */ @FlakyTest(bugId = 286952194) @@ -122,7 +129,6 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest /** Always expand activity is on top of the split. */ @FlakyTest(bugId = 286952194) - @Presubmit @Test fun endsWithAlwaysExpandActivityOnTop() { flicker.assertWmEnd { diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt index 0ca8f37b239b..e41364595648 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt @@ -176,12 +176,15 @@ class EnterSystemSplitTest(flicker: LegacyFlickerTest) : ActivityEmbeddingTestBa } @Ignore("Not applicable to this CUJ.") + @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() {} @FlakyTest(bugId = 342596801) + @Test override fun entireScreenCovered() = super.entireScreenCovered() @FlakyTest(bugId = 342596801) + @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt index b8f11dcf8970..ad083fa428a9 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.ime -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.Rotation import android.tools.flicker.junit.FlickerParametersRunnerFactory @@ -81,7 +80,6 @@ class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: LegacyF } @FlakyTest(bugId = 290767483) - @Postsubmit @Test fun imeLayerAlphaOneAfterSnapshotStartingWindowRemoval() { val layerTrace = flicker.reader.readLayersTrace() ?: error("Unable to read layers trace") diff --git a/tests/Input/AndroidManifest.xml b/tests/Input/AndroidManifest.xml index 914adc40194d..8d380f0d72a6 100644 --- a/tests/Input/AndroidManifest.xml +++ b/tests/Input/AndroidManifest.xml @@ -32,7 +32,7 @@ android:process=":externalProcess"> </activity> - <activity android:name="com.android.test.input.CaptureEventActivity" + <activity android:name="com.android.cts.input.CaptureEventActivity" android:label="Capture events" android:configChanges="touchscreen|uiMode|orientation|screenSize|screenLayout|keyboardHidden|uiMode|navigation|keyboard|density|fontScale|layoutDirection|locale|mcc|mnc|smallestScreenSize" android:enableOnBackInvokedCallback="false" diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 4d7085feb98f..8c04f647fb2f 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -59,6 +59,7 @@ import junitparams.Parameters import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -830,6 +831,18 @@ class KeyGestureControllerTests { KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), + TestData( + "META + ALT + 'V' -> Toggle Voice Access", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_V + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS, + intArrayOf(KeyEvent.KEYCODE_V), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), ) } @@ -843,6 +856,7 @@ class KeyGestureControllerTests { com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS, com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, + com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES, com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS ) @@ -861,6 +875,7 @@ class KeyGestureControllerTests { com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS, com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, + com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES, com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS ) @@ -1452,20 +1467,32 @@ class KeyGestureControllerTests { @Parameters(method = "customInputGesturesTestArguments") fun testCustomKeyGestures(test: TestData) { setupKeyGestureController() + val trigger = InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState + ) val builder = InputGestureData.Builder() .setKeyGestureType(test.expectedKeyGestureType) - .setTrigger( - InputGestureData.createKeyTrigger( - test.expectedKeys[0], - test.expectedModifierState - ) - ) + .setTrigger(trigger) if (test.expectedAppLaunchData != null) { builder.setAppLaunchData(test.expectedAppLaunchData) } val inputGestureData = builder.build() - keyGestureController.addCustomInputGesture(0, inputGestureData.aidlData) + assertNull( + test.toString(), + keyGestureController.getInputGesture(0, trigger.aidlTrigger) + ) + assertEquals( + test.toString(), + InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS, + keyGestureController.addCustomInputGesture(0, builder.build().aidlData) + ) + assertEquals( + test.toString(), + inputGestureData.aidlData, + keyGestureController.getInputGesture(0, trigger.aidlTrigger) + ) testKeyGestureInternal(test) } diff --git a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt b/tests/Input/src/com/android/test/input/CaptureEventActivity.kt deleted file mode 100644 index d54e3470d9c4..000000000000 --- a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.test.input - -import android.app.Activity -import android.os.Bundle -import android.view.InputEvent -import android.view.KeyEvent -import android.view.MotionEvent -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit -import org.junit.Assert.assertNull - -class CaptureEventActivity : Activity() { - private val events = LinkedBlockingQueue<InputEvent>() - var shouldHandleKeyEvents = true - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Set the fixed orientation if requested - if (intent.hasExtra(EXTRA_FIXED_ORIENTATION)) { - val orientation = intent.getIntExtra(EXTRA_FIXED_ORIENTATION, 0) - setRequestedOrientation(orientation) - } - - // Set the flag if requested - if (intent.hasExtra(EXTRA_WINDOW_FLAGS)) { - val flags = intent.getIntExtra(EXTRA_WINDOW_FLAGS, 0) - window.addFlags(flags) - } - } - - override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - events.add(KeyEvent(event)) - return shouldHandleKeyEvents - } - - override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - fun getInputEvent(): InputEvent? { - return events.poll(5, TimeUnit.SECONDS) - } - - fun hasReceivedEvents(): Boolean { - return !events.isEmpty() - } - - fun assertNoEvents() { - val event = events.poll(100, TimeUnit.MILLISECONDS) - assertNull("Expected no events, but received $event", event) - } - - companion object { - const val EXTRA_FIXED_ORIENTATION = "fixed_orientation" - const val EXTRA_WINDOW_FLAGS = "window_flags" - } -} diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt index 0b281d8d39e2..9e0f7347943d 100644 --- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt +++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt @@ -28,6 +28,7 @@ import android.view.InputEvent import android.view.MotionEvent import androidx.test.platform.app.InstrumentationRegistry import com.android.cts.input.BatchedEventSplitter +import com.android.cts.input.CaptureEventActivity import com.android.cts.input.InputJsonParser import com.android.cts.input.VirtualDisplayActivityScenario import com.android.cts.input.inputeventmatchers.isResampled diff --git a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java index 060133df0a40..e7e3d10c958b 100644 --- a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java +++ b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java @@ -81,7 +81,8 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.reportChange(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -90,7 +91,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.reportChange(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -99,7 +101,7 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChangeByPackageName(1, packageName, 0); + mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid()); } @Test @@ -108,7 +110,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChangeByPackageName(1, packageName, 0); + mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid()); } @Test @@ -133,7 +135,8 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -143,7 +146,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -152,7 +156,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -161,7 +166,7 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test @@ -171,7 +176,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test @@ -180,7 +185,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp index 449d93dd8c0b..f00a6cad6b46 100644 --- a/tools/aapt2/cmd/Command.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -53,61 +53,79 @@ std::string GetSafePath(StringPiece arg) { void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value, uint32_t flags) { - auto func = [value, flags](StringPiece arg) -> bool { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { + if (value) { + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + } return true; }; - flags_.emplace_back(Flag(name, description, /* required */ true, /* num_args */ 1, func)); + flags_.emplace_back( + Flag(name, description, /* required */ true, /* num_args */ 1, std::move(func))); } void Command::AddRequiredFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { - auto func = [value, flags](StringPiece arg) -> bool { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { + if (value) { + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + } return true; }; - flags_.emplace_back(Flag(name, description, /* required */ true, /* num_args */ 1, func)); + flags_.emplace_back( + Flag(name, description, /* required */ true, /* num_args */ 1, std::move(func))); } void Command::AddOptionalFlag(StringPiece name, StringPiece description, std::optional<std::string>* value, uint32_t flags) { - auto func = [value, flags](StringPiece arg) -> bool { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { + if (value) { + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + } return true; }; - flags_.emplace_back(Flag(name, description, /* required */ false, /* num_args */ 1, func)); + flags_.emplace_back( + Flag(name, description, /* required */ false, /* num_args */ 1, std::move(func))); } void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { - auto func = [value, flags](StringPiece arg) -> bool { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { + if (value) { + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + } return true; }; - flags_.emplace_back(Flag(name, description, /* required */ false, /* num_args */ 1, func)); + flags_.emplace_back( + Flag(name, description, /* required */ false, /* num_args */ 1, std::move(func))); } void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::unordered_set<std::string>* value) { - auto func = [value](StringPiece arg) -> bool { - value->emplace(arg); + auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { + if (value) { + value->emplace(arg); + } return true; }; - flags_.emplace_back(Flag(name, description, /* required */ false, /* num_args */ 1, func)); + flags_.emplace_back( + Flag(name, description, /* required */ false, /* num_args */ 1, std::move(func))); } void Command::AddOptionalSwitch(StringPiece name, StringPiece description, bool* value) { - auto func = [value](StringPiece arg) -> bool { - *value = true; + auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { + if (value) { + *value = true; + } return true; }; - flags_.emplace_back(Flag(name, description, /* required */ false, /* num_args */ 0, func)); + flags_.emplace_back( + Flag(name, description, /* required */ false, /* num_args */ 0, std::move(func))); } void Command::AddOptionalSubcommand(std::unique_ptr<Command>&& subcommand, bool experimental) { @@ -172,19 +190,74 @@ void Command::Usage(std::ostream* out) { argline = " "; } } - *out << " " << std::setw(kWidth) << std::left << "-h" - << "Displays this help menu\n"; out->flush(); } -int Command::Execute(const std::vector<StringPiece>& args, std::ostream* out_error) { +const std::string& Command::addEnvironmentArg(const Flag& flag, const char* env) { + if (*env && flag.num_args > 0) { + return environment_args_.emplace_back(flag.name + '=' + env); + } + return flag.name; +} + +// +// Looks for the flags specified in the environment and adds them to |args|. +// Expected format: +// - _AAPT2_UPPERCASE_NAME are added before all of the command line flags, so it's +// a default for the flag that may get overridden by the command line. +// - AAPT2_UPPERCASE_NAME_ are added after them, making this to be the final value +// even if there was something on the command line. +// - All dashes in the flag name get replaced with underscores, the rest of it is +// intact. +// +// E.g. +// --set-some-flag becomes either _AAPT2_SET_SOME_FLAG or AAPT2_SET_SOME_FLAG_ +// --set-param=2 is _AAPT2_SET_SOME_FLAG=2 +// +// Values get passed as it, with no processing or quoting. +// +// This way one can make sure aapt2 has the flags they need even when it is +// launched in a way they can't control, e.g. deep inside a build. +// +void Command::parseFlagsFromEnvironment(std::vector<StringPiece>& args) { + // If the first argument is a subcommand then skip it and prepend the flags past that (the root + // command should only have a single '-h' flag anyway). + const int insert_pos = args.empty() ? 0 : args.front().starts_with('-') ? 0 : 1; + + std::string env_name; + for (const Flag& flag : flags_) { + // First, the prefix version. + env_name.assign("_AAPT2_"); + // Append the uppercased flag name, skipping all dashes in front and replacing them with + // underscores later. + auto name_start = flag.name.begin(); + while (name_start != flag.name.end() && *name_start == '-') { + ++name_start; + } + std::transform(name_start, flag.name.end(), std::back_inserter(env_name), + [](char c) { return c == '-' ? '_' : toupper(c); }); + if (auto prefix_env = getenv(env_name.c_str())) { + args.insert(args.begin() + insert_pos, addEnvironmentArg(flag, prefix_env)); + } + // Now reuse the same name variable to construct a suffix version: append the + // underscore and just skip the one in front. + env_name += '_'; + if (auto suffix_env = getenv(env_name.c_str() + 1)) { + args.push_back(addEnvironmentArg(flag, suffix_env)); + } + } +} + +int Command::Execute(std::vector<StringPiece>& args, std::ostream* out_error) { TRACE_NAME_ARGS("Command::Execute", args); std::vector<std::string> file_args; + parseFlagsFromEnvironment(args); + for (size_t i = 0; i < args.size(); i++) { StringPiece arg = args[i]; if (*(arg.data()) != '-') { - // Continue parsing as the subcommand if the first argument matches one of the subcommands + // Continue parsing as a subcommand if the first argument matches one of the subcommands if (i == 0) { for (auto& subcommand : subcommands_) { if (arg == subcommand->name_ || (!subcommand->short_name_.empty() @@ -211,37 +284,67 @@ int Command::Execute(const std::vector<StringPiece>& args, std::ostream* out_err return 1; } + static constexpr auto matchShortArg = [](std::string_view arg, const Flag& flag) static { + return flag.name.starts_with("--") && + arg.compare(0, 2, std::string_view(flag.name.c_str() + 1, 2)) == 0; + }; + bool match = false; for (Flag& flag : flags_) { - // Allow both "--arg value" and "--arg=value" syntax. + // Allow both "--arg value" and "--arg=value" syntax, and look for the cases where we can + // safely deduce the "--arg" flag from the short "-a" version when there's no value expected + bool matched_current = false; if (arg.starts_with(flag.name) && (arg.size() == flag.name.size() || (flag.num_args > 0 && arg[flag.name.size()] == '='))) { - if (flag.num_args > 0) { - if (arg.size() == flag.name.size()) { - i++; - if (i >= args.size()) { - *out_error << flag.name << " missing argument.\n\n"; - Usage(out_error); - return 1; - } - arg = args[i]; - } else { - arg.remove_prefix(flag.name.size() + 1); - // Disallow empty arguments after '='. - if (arg.empty()) { - *out_error << flag.name << " has empty argument.\n\n"; - Usage(out_error); - return 1; - } + matched_current = true; + } else if (flag.num_args == 0 && matchShortArg(arg, flag)) { + matched_current = true; + // It matches, now need to make sure no other flag would match as well. + // This is really inefficient, but we don't expect to have enough flags for it to matter + // (famous last words). + for (const Flag& other_flag : flags_) { + if (&other_flag == &flag) { + continue; } - flag.action(arg); + if (matchShortArg(arg, other_flag)) { + matched_current = false; // ambiguous, skip this match + break; + } + } + } + if (!matched_current) { + continue; + } + + if (flag.num_args > 0) { + if (arg.size() == flag.name.size()) { + i++; + if (i >= args.size()) { + *out_error << flag.name << " missing argument.\n\n"; + Usage(out_error); + return 1; + } + arg = args[i]; } else { - flag.action({}); + arg.remove_prefix(flag.name.size() + 1); + // Disallow empty arguments after '='. + if (arg.empty()) { + *out_error << flag.name << " has empty argument.\n\n"; + Usage(out_error); + return 1; + } + } + if (!flag.action(arg, out_error)) { + return 1; + } + } else { + if (!flag.action({}, out_error)) { + return 1; } - flag.found = true; - match = true; - break; } + flag.found = true; + match = true; + break; } if (!match) { diff --git a/tools/aapt2/cmd/Command.h b/tools/aapt2/cmd/Command.h index 1416e980ed19..767ca9b0de9f 100644 --- a/tools/aapt2/cmd/Command.h +++ b/tools/aapt2/cmd/Command.h @@ -14,10 +14,11 @@ * limitations under the License. */ -#ifndef AAPT_COMMAND_H -#define AAPT_COMMAND_H +#pragma once +#include <deque> #include <functional> +#include <memory> #include <optional> #include <ostream> #include <string> @@ -30,10 +31,17 @@ namespace aapt { class Command { public: - explicit Command(android::StringPiece name) : name_(name), full_subcommand_name_(name){}; + explicit Command(android::StringPiece name) : Command(name, {}) { + } explicit Command(android::StringPiece name, android::StringPiece short_name) - : name_(name), short_name_(short_name), full_subcommand_name_(name){}; + : name_(name), short_name_(short_name), full_subcommand_name_(name) { + flags_.emplace_back("--help", "Displays this help menu", false, 0, + [this](android::StringPiece arg, std::ostream* out) { + Usage(out); + return false; + }); + } Command(Command&&) = default; Command& operator=(Command&&) = default; @@ -76,41 +84,51 @@ class Command { // Parses the command line arguments, sets the flag variable values, and runs the action of // the command. If the arguments fail to parse to the command and its subcommands, then the action // will not be run and the usage will be printed instead. - int Execute(const std::vector<android::StringPiece>& args, std::ostream* outError); + int Execute(std::vector<android::StringPiece>& args, std::ostream* out_error); + + // Same, but for a temporary vector of args. + int Execute(std::vector<android::StringPiece>&& args, std::ostream* out_error) { + return Execute(args, out_error); + } // The action to preform when the command is executed. virtual int Action(const std::vector<std::string>& args) = 0; private: struct Flag { - explicit Flag(android::StringPiece name, android::StringPiece description, - const bool is_required, const size_t num_args, - std::function<bool(android::StringPiece value)>&& action) + explicit Flag(android::StringPiece name, android::StringPiece description, bool is_required, + const size_t num_args, + std::function<bool(android::StringPiece value, std::ostream* out_err)>&& action) : name(name), description(description), - is_required(is_required), + action(std::move(action)), num_args(num_args), - action(std::move(action)) { + is_required(is_required) { } - const std::string name; - const std::string description; - const bool is_required; - const size_t num_args; - const std::function<bool(android::StringPiece value)> action; + std::string name; + std::string description; + std::function<bool(android::StringPiece value, std::ostream* out_error)> action; + size_t num_args; + bool is_required; bool found = false; }; + const std::string& addEnvironmentArg(const Flag& flag, const char* env); + void parseFlagsFromEnvironment(std::vector<android::StringPiece>& args); + std::string name_; std::string short_name_; - std::string description_ = ""; + std::string description_; std::string full_subcommand_name_; std::vector<Flag> flags_; std::vector<std::unique_ptr<Command>> subcommands_; std::vector<std::unique_ptr<Command>> experimental_subcommands_; + // A collection of arguments loaded from environment variables, with stable positions + // in memory - we add them to the vector of string views so the pointers may not change, + // with or without short string buffer utilization in std::string. + std::deque<std::string> environment_args_; }; } // namespace aapt - -#endif // AAPT_COMMAND_H diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp index 20d87e0025c3..ad167c979662 100644 --- a/tools/aapt2/cmd/Command_test.cpp +++ b/tools/aapt2/cmd/Command_test.cpp @@ -118,4 +118,63 @@ TEST(CommandTest, OptionsWithValues) { EXPECT_NE(0, command.Execute({"--flag1"s, "2"s}, &std::cerr)); } +TEST(CommandTest, ShortOptions) { + TestCommand command; + bool flag = false; + command.AddOptionalSwitch("--flag", "", &flag); + + ASSERT_EQ(0, command.Execute({"--flag"s}, &std::cerr)); + EXPECT_TRUE(flag); + + // Short version of a switch should work. + flag = false; + ASSERT_EQ(0, command.Execute({"-f"s}, &std::cerr)); + EXPECT_TRUE(flag); + + // Ambiguous names shouldn't parse via short options. + command.AddOptionalSwitch("--flag-2", "", &flag); + ASSERT_NE(0, command.Execute({"-f"s}, &std::cerr)); + + // But when we have a proper flag like that it should still work. + flag = false; + command.AddOptionalSwitch("-f", "", &flag); + ASSERT_EQ(0, command.Execute({"-f"s}, &std::cerr)); + EXPECT_TRUE(flag); + + // A regular short flag works fine as well. + flag = false; + command.AddOptionalSwitch("-d", "", &flag); + ASSERT_EQ(0, command.Execute({"-d"s}, &std::cerr)); + EXPECT_TRUE(flag); + + // A flag with a value only works via its long name syntax. + std::optional<std::string> val; + command.AddOptionalFlag("--with-val", "", &val); + ASSERT_EQ(0, command.Execute({"--with-val"s, "1"s}, &std::cerr)); + EXPECT_TRUE(val); + EXPECT_STREQ("1", val->c_str()); + + // Make sure the flags that require a value can't be parsed via short syntax, -w=blah + // looks weird. + ASSERT_NE(0, command.Execute({"-w"s, "2"s}, &std::cerr)); +} + +TEST(CommandTest, OptionsWithNullptrToAcceptValues) { + TestCommand command; + command.AddRequiredFlag("--rflag", "", nullptr); + command.AddRequiredFlagList("--rlflag", "", nullptr); + command.AddOptionalFlag("--oflag", "", nullptr); + command.AddOptionalFlagList("--olflag", "", (std::vector<std::string>*)nullptr); + command.AddOptionalFlagList("--olflag2", "", (std::unordered_set<std::string>*)nullptr); + command.AddOptionalSwitch("--switch", "", nullptr); + + ASSERT_EQ(0, command.Execute({ + "--rflag"s, "1"s, + "--rlflag"s, "1"s, + "--oflag"s, "1"s, + "--olflag"s, "1"s, + "--olflag2"s, "1"s, + "--switch"s}, &std::cerr)); +} + } // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index 6c3eae11eab9..060bc5fa2242 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -425,9 +425,6 @@ int ConvertCommand::Action(const std::vector<std::string>& args) { << output_format_.value()); return 1; } - if (enable_sparse_encoding_) { - table_flattener_options_.sparse_entries = SparseEntriesMode::Enabled; - } if (force_sparse_encoding_) { table_flattener_options_.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h index 9452e588953e..98c8f5ff89c0 100644 --- a/tools/aapt2/cmd/Convert.h +++ b/tools/aapt2/cmd/Convert.h @@ -36,11 +36,9 @@ class ConvertCommand : public Command { kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_); AddOptionalSwitch( "--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", - &enable_sparse_encoding_); + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" @@ -87,7 +85,6 @@ class ConvertCommand : public Command { std::string output_path_; std::optional<std::string> output_format_; bool verbose_ = false; - bool enable_sparse_encoding_ = false; bool force_sparse_encoding_ = false; bool enable_compact_entries_ = false; std::optional<std::string> resources_config_path_; diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 232b4024abd2..eb71189ffc46 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -2504,9 +2504,6 @@ int LinkCommand::Action(const std::vector<std::string>& args) { << "the --merge-only flag can be only used when building a static library"); return 1; } - if (options_.use_sparse_encoding) { - options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; - } // The default build type. context.SetPackageType(PackageType::kApp); diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index 2f17853718ec..b5bd905c02be 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -75,7 +75,6 @@ struct LinkOptions { bool no_resource_removal = false; bool no_xml_namespaces = false; bool do_not_compress_anything = false; - bool use_sparse_encoding = false; std::unordered_set<std::string> extensions_to_not_compress; std::optional<std::regex> regex_to_not_compress; FeatureFlagValues feature_flag_values; @@ -163,9 +162,11 @@ class LinkCommand : public Command { AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n" "defaults. Use this only when building runtime resource overlay packages.", &options_.no_resource_removal); - AddOptionalSwitch("--enable-sparse-encoding", - "This decreases APK size at the cost of resource retrieval performance.", - &options_.use_sparse_encoding); + AddOptionalSwitch( + "--enable-sparse-encoding", + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", &options_.table_flattener_options.use_compact_entries); diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 762441ee1872..f218307af578 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -406,9 +406,6 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) { return 1; } - if (options_.enable_sparse_encoding) { - options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; - } if (options_.force_sparse_encoding) { options_.table_flattener_options.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index 012b0f230ca2..e3af584cbbd9 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -61,9 +61,6 @@ struct OptimizeOptions { // TODO(b/246489170): keep the old option and format until transform to the new one std::optional<std::string> shortened_paths_map_path; - // Whether sparse encoding should be used for O+ resources. - bool enable_sparse_encoding = false; - // Whether sparse encoding should be used for all resources. bool force_sparse_encoding = false; @@ -106,11 +103,9 @@ class OptimizeCommand : public Command { &kept_artifacts_); AddOptionalSwitch( "--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", - &options_.enable_sparse_encoding); + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 1a82021bce71..b8ac7925d44e 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -201,7 +201,7 @@ class PackageFlattener { (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) { // Sparse encode if forced or sdk version is not set in context and config. } else { - // Otherwise, only sparse encode if the entries will be read on platforms S_V2+. + // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32). sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2); } diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index 0633bc81cb25..f1c4c3512ed3 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -37,8 +37,7 @@ constexpr const size_t kSparseEncodingThreshold = 60; enum class SparseEntriesMode { // Disables sparse encoding for entries. Disabled, - // Enables sparse encoding for all entries for APKs with O+ minSdk. For APKs with minSdk less - // than O only applies sparse encoding for resource configuration available on O+. + // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2). Enabled, // Enables sparse encoding for all entries regardless of minSdk. Forced, @@ -47,7 +46,7 @@ enum class SparseEntriesMode { struct TableFlattenerOptions { // When enabled, types for configurations with a sparse set of entries are encoded // as a sparse map of entry ID and offset to actual data. - SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled; + SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled; // When true, use compact entries for simple data bool use_compact_entries = false; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index 0f1168514c4a..e3d589eb078b 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -337,13 +337,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Enabled; + options.sparse_entries = SparseEntriesMode::Disabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); @@ -421,13 +421,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Enabled; + options.sparse_entries = SparseEntriesMode::Disabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); diff --git a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt index 6da6fc6f12c3..d0807f2ecd34 100644 --- a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt +++ b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt @@ -877,7 +877,7 @@ resource_table { } tool_fingerprint { tool: "Android Asset Packaging Tool (aapt)" - version: "2.19-SOONG BUILD NUMBER PLACEHOLDER" + version: "2.20-SOONG BUILD NUMBER PLACEHOLDER" } } xml_files { diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 8368f9d16af8..664d8412a3be 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -1,5 +1,11 @@ # Android Asset Packaging Tool 2.0 (AAPT2) release notes +## Version 2.20 +- Too many features, bug fixes, and improvements to list since the last minor version update in + 2017. This README will be updated more frequently in the future. +- Sparse encoding is now always enabled by default if the minSdkVersion is >= 32 (S_V2). The + `--enable-sparse-encoding` flag still exists, but is a no-op. + ## Version 2.19 - Added navigation resource type. - Fixed issue with resource deduplication. (bug 64397629) diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp index 3d83caf29bba..6a4dfa629394 100644 --- a/tools/aapt2/util/Util.cpp +++ b/tools/aapt2/util/Util.cpp @@ -227,7 +227,7 @@ std::string GetToolFingerprint() { static const char* const sMajorVersion = "2"; // Update minor version whenever a feature or flag is added. - static const char* const sMinorVersion = "19"; + static const char* const sMinorVersion = "20"; // The build id of aapt2 binary. static const std::string sBuildId = [] { |