diff options
398 files changed, 13061 insertions, 5514 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 214e0402c8fc..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", @@ -220,17 +220,6 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } -java_aconfig_library { - name: "telephony_flags_core_java_exported_lib", - aconfig_declarations: "telephony_flags", - mode: "exported", - min_sdk_version: "30", - apex_available: [ - "com.android.wifi", - ], - defaults: ["framework-minus-apex-aconfig-java-defaults"], -} - cc_aconfig_library { name: "telephony_flags_c_lib", aconfig_declarations: "telephony_flags", @@ -534,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 { @@ -559,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 @@ -839,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/core/api/current.txt b/core/api/current.txt index a87644741fe6..c4109392d6bd 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -8947,18 +8947,19 @@ package android.app.assist { method public android.content.ClipData getClipData(); method public android.os.Bundle getExtras(); method public android.content.Intent getIntent(); + method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") @Nullable public android.net.Uri getSessionTransferUri(); method public String getStructuredData(); method public android.net.Uri getWebUri(); method public boolean isAppProvidedIntent(); method public boolean isAppProvidedWebUri(); method public void setClipData(android.content.ClipData); method public void setIntent(android.content.Intent); + method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public void setSessionTransferUri(@Nullable android.net.Uri); method public void setStructuredData(String); method public void setWebUri(android.net.Uri); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.assist.AssistContent> CREATOR; field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXTRA_APP_FUNCTION_DATA = "android.app.assist.extra.APP_FUNCTION_DATA"; - field @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public static final String EXTRA_SESSION_TRANSFER_WEB_URI = "android.app.assist.extra.SESSION_TRANSFER_WEB_URI"; } public class AssistStructure implements android.os.Parcelable { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index f82aecbd6d44..22af517900d2 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 } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index cd2cc07b8cc3..a988acf1f4a9 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(); @@ -4537,7 +4538,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 8614bde775ad..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; @@ -1273,8 +1270,8 @@ public class Activity extends ContextThemeWrapper * Requests to show the “Open in browser” education. “Open in browser” is a feature * within the app header that allows users to switch from an app to the web. The feature * is made available when an application is opened by a user clicking a link or when a - * link is provided by an application. Links can be provided by utilizing - * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or + * link is provided by an application. Links can be provided by calling + * {@link AssistContent#setSessionTransferUri} or * {@link AssistContent#setWebUri}. * * <p>This method should be utilized when an activity wants to nudge the user to switch @@ -1287,7 +1284,7 @@ public class Activity extends ContextThemeWrapper * disruptive to the user to show the education and when it is optimal to switch the user to a * browser session. Before requesting to show the education, developers should assert that they * have set a link that can be used by the "Open in browser" feature through either - * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or + * {@link AssistContent#setSessionTransferUri} or * {@link AssistContent#setWebUri} so that users are navigated to a relevant page if they choose * to switch to the browser. If a URI is not set using either method, "Open in browser" will * utilize a generic link if available which will direct users to the homepage of the site @@ -1296,7 +1293,7 @@ public class Activity extends ContextThemeWrapper * the user will not be provided with the option to switch to the browser and the education will * not be shown if requested. * - * @see android.app.assist.AssistContent#EXTRA_SESSION_TRANSFER_WEB_URI + * @see android.app.assist.AssistContent#setSessionTransferUri */ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION) public final void requestOpenInBrowserEducation() { @@ -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/ActivityThread.java b/core/java/android/app/ActivityThread.java index 717a2acb4b4a..1f3e6559a695 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -105,6 +105,7 @@ import android.content.pm.ServiceInfo; import android.content.res.AssetManager; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; +import android.content.res.ResourceTimer; import android.content.res.Resources; import android.content.res.ResourcesImpl; import android.content.res.loader.ResourcesLoader; @@ -5253,6 +5254,7 @@ public final class ActivityThread extends ClientTransactionHandler Resources.dumpHistory(pw, ""); pw.flush(); + ResourceTimer.dumpTimers(info.fd.getFileDescriptor(), "-refresh"); if (info.finishCallback != null) { info.finishCallback.sendResult(null); } 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/Notification.java b/core/java/android/app/Notification.java index 24594ab41100..614e2aaf42e8 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. 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/assist/AssistContent.java b/core/java/android/app/assist/AssistContent.java index 3e3ca2488bd3..adf8c94ff8d5 100644 --- a/core/java/android/app/assist/AssistContent.java +++ b/core/java/android/app/assist/AssistContent.java @@ -1,6 +1,7 @@ package android.app.assist; import android.annotation.FlaggedApi; +import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.ClipData; import android.content.Intent; @@ -30,31 +31,6 @@ public class AssistContent implements Parcelable { public static final String EXTRA_APP_FUNCTION_DATA = "android.app.assist.extra.APP_FUNCTION_DATA"; - /** - * This extra can be optionally supplied in the {@link #getExtras} bundle to provide a - * {@link Uri} which will be utilized when transitioning a user's session to another surface. - * - * <p>If provided, instead of using the URI provided in {@link #setWebUri}, the - * "Open in browser" feature will use this URI to transition the current session from one - * surface to the other. Apps may choose to encode session or user information into this - * URI in order to provide a better session transfer experience. - * - * <p>Unlike {@link #setWebUri}, this URI will not be used for features where the user might - * accidentally share it with another user. However, developers should not encode - * authentication credentials into this URI, because it will be surfaced in the browser URL - * bar and may be copied and shared from there. - * - * <p>When providing this extra, developers should still continue to provide - * {@link #setWebUri} for backwards compatibility with features such as - * <a href="https://developer.android.com/guide/components/activities/recents#url-sharing"> - * recents URL sharing</a> which do not benefit from a session-transfer web URI. - * - * @see android.app.Activity#requestOpenInBrowserEducation() - */ - @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION) - public static final String EXTRA_SESSION_TRANSFER_WEB_URI = - "android.app.assist.extra.SESSION_TRANSFER_WEB_URI"; - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private boolean mIsAppProvidedIntent = false; private boolean mIsAppProvidedWebUri = false; @@ -66,6 +42,7 @@ public class AssistContent implements Parcelable { private ClipData mClipData; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private Uri mUri; + private Uri mSessionTransferUri; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private final Bundle mExtras; @@ -200,6 +177,41 @@ public class AssistContent implements Parcelable { } /** + * This method can be used to provide a {@link Uri} which will be utilized when transitioning a + * user's session to another surface. + * + * <p>If provided, instead of using the URI provided in {@link #setWebUri}, the + * "Open in browser" feature will use this URI to transition the current session from one + * surface to the other. Apps may choose to encode session or user information into this + * URI in order to provide a better session transfer experience. However, while this URI will + * only be available to the system and not other applications, developers should not encode + * authentication credentials into this URI, because it will be surfaced in the browser URL bar + * and may be copied and shared from there. + * + * <p>When providing this URI, developers should still continue to provide + * {@link #setWebUri} for backwards compatibility with features such as + * <a href="https://developer.android.com/guide/components/activities/recents#url-sharing"> + * recents URL sharing</a> which facilitate link sharing with other users and would not benefit + * from a session-transfer URI. + * + * @see android.app.Activity#requestOpenInBrowserEducation() + */ + @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION) + public void setSessionTransferUri(@Nullable Uri uri) { + mSessionTransferUri = uri; + } + + /** + * Return the content's session transfer web URI as per + * {@link #setSessionTransferUri(android.net.Uri)}, or null if there is none. + */ + @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION) + @Nullable + public Uri getSessionTransferUri() { + return mSessionTransferUri; + } + + /** * Return Bundle for extra vendor-specific data that can be modified and examined. */ public Bundle getExtras() { @@ -218,6 +230,9 @@ public class AssistContent implements Parcelable { mUri = Uri.CREATOR.createFromParcel(in); } if (in.readInt() != 0) { + mSessionTransferUri = Uri.CREATOR.createFromParcel(in); + } + if (in.readInt() != 0) { mStructuredData = in.readString(); } mIsAppProvidedIntent = in.readInt() == 1; @@ -245,6 +260,12 @@ public class AssistContent implements Parcelable { } else { dest.writeInt(0); } + if (mSessionTransferUri != null) { + dest.writeInt(1); + mSessionTransferUri.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } if (mStructuredData != null) { dest.writeInt(1); dest.writeString(mStructuredData); 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..e3e10388754c 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -12401,19 +12401,23 @@ public class Intent implements Parcelable, Cloneable { private void collectNestedIntentKeysRecur(Set<Intent> visited) { addExtendedFlags(EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED); - if (mExtras != null && !mExtras.isEmpty()) { + if (mExtras != null && !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 (!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 probably would never happen. But just in case, simply ignore it since + // it is not an intent anyway. value = null; } if (value instanceof Intent intent) { 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/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..b938aac811fd 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -25,6 +25,7 @@ import android.content.res.loader.ResourcesProvider; import android.ravenwood.annotation.RavenwoodClassLoadHook; import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.text.TextUtils; +import android.util.Log; import com.android.internal.annotations.GuardedBy; @@ -50,6 +51,7 @@ import java.util.Objects; @RavenwoodKeepWholeClass @RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK) public final class ApkAssets { + private static final boolean DEBUG = false; /** * The apk assets contains framework resource values specified by the system. @@ -134,6 +136,17 @@ public final class ApkAssets { @Nullable private final AssetsProvider mAssets; + @NonNull + private String mName; + + private static final int UPTODATE_FALSE = 0; + private static final int UPTODATE_TRUE = 1; + private static final int UPTODATE_ALWAYS_TRUE = 2; + + // Start with the only value that may change later and would force a native call to + // double check it. + private int mPreviousUpToDateResult = UPTODATE_TRUE; + /** * Creates a new ApkAssets instance from the given path on disk. * @@ -304,7 +317,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull String path, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); + this(format, flags, assets, path); Objects.requireNonNull(path, "path"); mNativePtr = nativeLoad(format, path, flags, assets); mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/); @@ -313,7 +326,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); + this(format, flags, assets, friendlyName); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); mNativePtr = nativeLoadFd(format, fd, friendlyName, flags, assets); @@ -323,7 +336,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, long offset, long length, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); + this(format, flags, assets, friendlyName); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); mNativePtr = nativeLoadFdOffsets(format, fd, friendlyName, offset, length, flags, assets); @@ -331,16 +344,17 @@ public final class ApkAssets { } private ApkAssets(@PropertyFlags int flags, @Nullable AssetsProvider assets) { - this(FORMAT_APK, flags, assets); + this(FORMAT_APK, flags, assets, "empty"); mNativePtr = nativeLoadEmpty(flags, assets); mStringBlock = null; } private ApkAssets(@FormatType int format, @PropertyFlags int flags, - @Nullable AssetsProvider assets) { + @Nullable AssetsProvider assets, @NonNull String name) { mFlags = flags; mAssets = assets; mIsOverlay = format == FORMAT_IDMAP; + if (DEBUG) mName = name; } @UnsupportedAppUsage @@ -421,13 +435,41 @@ public final class ApkAssets { } } + private static double intervalMs(long beginNs, long endNs) { + return (endNs - beginNs) / 1000000.0; + } + /** * Returns false if the underlying APK was changed since this ApkAssets was loaded. */ public boolean isUpToDate() { + // This function is performance-critical - it's called multiple times on every Resources + // object creation, and on few other cache accesses - so it's important to avoid the native + // call when we know for sure what it will return (which is the case for both ALWAYS_TRUE + // and FALSE). + if (mPreviousUpToDateResult != UPTODATE_TRUE) { + return mPreviousUpToDateResult == UPTODATE_ALWAYS_TRUE; + } + final long beforeTs, afterLockTs, afterNativeTs, afterUnlockTs; + if (DEBUG) beforeTs = System.nanoTime(); + final int res; synchronized (this) { - return nativeIsUpToDate(mNativePtr); + if (DEBUG) afterLockTs = System.nanoTime(); + res = nativeIsUpToDate(mNativePtr); + if (DEBUG) afterNativeTs = System.nanoTime(); + } + if (DEBUG) { + afterUnlockTs = System.nanoTime(); + if (afterUnlockTs - beforeTs >= 10L * 1000000) { + Log.d("ApkAssets", "isUpToDate(" + mName + ") took " + + intervalMs(beforeTs, afterUnlockTs) + + " ms: " + intervalMs(beforeTs, afterLockTs) + + " / " + intervalMs(afterLockTs, afterNativeTs) + + " / " + intervalMs(afterNativeTs, afterUnlockTs)); + } } + mPreviousUpToDateResult = res; + return res != UPTODATE_FALSE; } public boolean isSystem() { @@ -487,7 +529,7 @@ public final class ApkAssets { private static native @NonNull String nativeGetAssetPath(long ptr); private static native @NonNull String nativeGetDebugName(long ptr); private static native long nativeGetStringBlock(long ptr); - @CriticalNative private static native boolean nativeIsUpToDate(long ptr); + @CriticalNative private static native int nativeIsUpToDate(long ptr); private static native long nativeOpenXml(long ptr, @NonNull String fileName) throws IOException; private static native @Nullable OverlayableInfo nativeGetOverlayableInfo(long ptr, String overlayableName) throws IOException; diff --git a/core/java/android/content/res/ResourceTimer.java b/core/java/android/content/res/ResourceTimer.java index d51f64ce8106..2d1bf4d9d296 100644 --- a/core/java/android/content/res/ResourceTimer.java +++ b/core/java/android/content/res/ResourceTimer.java @@ -17,13 +17,10 @@ package android.content.res; import android.annotation.NonNull; -import android.annotation.Nullable; - import android.app.AppProtoEnums; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.SystemClock; import android.text.TextUtils; @@ -33,6 +30,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FrameworkStatsLog; +import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.PrintWriter; import java.util.Arrays; @@ -277,38 +275,40 @@ public final class ResourceTimer { * Update the metrics information and dump it. * @hide */ - public static void dumpTimers(@NonNull ParcelFileDescriptor pfd, @Nullable String[] args) { - FileOutputStream fout = new FileOutputStream(pfd.getFileDescriptor()); - PrintWriter pw = new FastPrintWriter(fout); - synchronized (sLock) { - if (!sEnabled || (sConfig == null)) { + public static void dumpTimers(@NonNull FileDescriptor fd, String... args) { + try (PrintWriter pw = new FastPrintWriter(new FileOutputStream(fd))) { + pw.println("\nDumping ResourceTimers"); + + final boolean enabled; + synchronized (sLock) { + enabled = sEnabled && sConfig != null; + } + if (!enabled) { pw.println(" Timers are not enabled in this process"); - pw.flush(); return; } - } - // Look for the --refresh switch. If the switch is present, then sTimers is updated. - // Otherwise, the current value of sTimers is displayed. - boolean refresh = Arrays.asList(args).contains("-refresh"); - - synchronized (sLock) { - update(refresh); - long runtime = sLastUpdated - sProcessStart; - pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName()); - for (int i = 0; i < sTimers.length; i++) { - Timer t = sTimers[i]; - if (t.count != 0) { - String name = sConfig.timers[i]; - pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s " - + "largest=%s\n", - name, t.count, t.total / t.count, t.mintime, t.maxtime, - packedString(t.percentile), - packedString(t.largest)); + // Look for the --refresh switch. If the switch is present, then sTimers is updated. + // Otherwise, the current value of sTimers is displayed. + boolean refresh = Arrays.asList(args).contains("-refresh"); + + synchronized (sLock) { + update(refresh); + long runtime = sLastUpdated - sProcessStart; + pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName()); + for (int i = 0; i < sTimers.length; i++) { + Timer t = sTimers[i]; + if (t.count != 0) { + String name = sConfig.timers[i]; + pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s " + + "largest=%s\n", + name, t.count, t.total / t.count, t.mintime, t.maxtime, + packedString(t.percentile), + packedString(t.largest)); + } } } } - pw.flush(); } // Enable (or disabled) the runtime timers. Note that timers are disabled by default. This 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/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/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..cb1e0161441f 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -129,6 +129,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; @@ -225,6 +226,7 @@ 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 { @@ -807,6 +809,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/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java index 8f6a50843ddb..12080ca511b2 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; } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c94526bcdcd7..ec58eff92410 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) @@ -17395,13 +17419,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/text/Layout.java b/core/java/android/text/Layout.java index e254bf3e016f..d53b98c65f9a 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -77,7 +77,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 = { 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/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..544f42b9acfa 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -148,6 +148,15 @@ 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; + /** * 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/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/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..de3e0d3faf43 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" 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..5635943cb76e 100644 --- a/core/java/com/android/internal/notification/SystemNotificationChannels.java +++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java @@ -67,6 +67,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"; @@ -203,6 +204,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/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/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp index 1e7bfe32ba79..e6364a96bd9f 100644 --- a/core/jni/android_content_res_ApkAssets.cpp +++ b/core/jni/android_content_res_ApkAssets.cpp @@ -111,9 +111,8 @@ static void DeleteGuardedApkAssets(Guarded<AssetManager2::ApkAssetsPtr>& apk_ass class LoaderAssetsProvider : public AssetsProvider { public: static std::unique_ptr<AssetsProvider> Create(JNIEnv* env, jobject assets_provider) { - return (!assets_provider) ? EmptyAssetsProvider::Create() - : std::unique_ptr<AssetsProvider>(new LoaderAssetsProvider( - env, assets_provider)); + return std::unique_ptr<AssetsProvider>{ + assets_provider ? new LoaderAssetsProvider(env, assets_provider) : nullptr}; } bool ForEachFile(const std::string& /* root_path */, @@ -129,8 +128,8 @@ class LoaderAssetsProvider : public AssetsProvider { return debug_name_; } - bool IsUpToDate() const override { - return true; + UpToDate IsUpToDate() const override { + return UpToDate::Always; } ~LoaderAssetsProvider() override { @@ -212,7 +211,7 @@ class LoaderAssetsProvider : public AssetsProvider { auto string_result = static_cast<jstring>(env->CallObjectMethod( assets_provider_, gAssetsProviderOffsets.toString)); ScopedUtfChars str(env, string_result); - debug_name_ = std::string(str.c_str(), str.size()); + debug_name_ = std::string(str.c_str()); } // The global reference to the AssetsProvider @@ -243,10 +242,10 @@ static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t forma apk_assets = ApkAssets::LoadOverlay(path.c_str(), property_flags); break; case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()), - std::move(loader_assets), - property_flags); - break; + apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()), + MultiAssetsProvider::Create(std::move(loader_assets)), + property_flags); + break; case FORMAT_DIRECTORY: { auto assets = MultiAssetsProvider::Create(std::move(loader_assets), DirectoryAssetsProvider::Create(path.c_str())); @@ -316,10 +315,11 @@ static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, const format_type_t break; } case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable( - AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */), - std::move(loader_assets), property_flags); - break; + apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd), + nullptr /* path */), + MultiAssetsProvider::Create(std::move(loader_assets)), + property_flags); + break; default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str()); @@ -386,12 +386,15 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_ break; } case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable( - AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */, - static_cast<off64_t>(offset), - static_cast<off64_t>(length)), - std::move(loader_assets), property_flags); - break; + apk_assets = + ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd), + nullptr /* path */, + static_cast<off64_t>(offset), + static_cast<off64_t>( + length)), + MultiAssetsProvider::Create(std::move(loader_assets)), + property_flags); + break; default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str()); @@ -408,13 +411,16 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_ } static jlong NativeLoadEmpty(JNIEnv* env, jclass /*clazz*/, jint flags, jobject assets_provider) { - auto apk_assets = ApkAssets::Load(LoaderAssetsProvider::Create(env, assets_provider), flags); - if (apk_assets == nullptr) { - const std::string error_msg = - base::StringPrintf("Failed to load empty assets with provider %p", (void*)assets_provider); - jniThrowException(env, "java/io/IOException", error_msg.c_str()); - return 0; - } + auto apk_assets = ApkAssets::Load(MultiAssetsProvider::Create( + LoaderAssetsProvider::Create(env, assets_provider)), + flags); + if (apk_assets == nullptr) { + const std::string error_msg = + base::StringPrintf("Failed to load empty assets with provider %p", + (void*)assets_provider); + jniThrowException(env, "java/io/IOException", error_msg.c_str()); + return 0; + } return CreateGuardedApkAssets(std::move(apk_assets)); } @@ -443,10 +449,10 @@ static jlong NativeGetStringBlock(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) return reinterpret_cast<jlong>(apk_assets->GetLoadedArsc()->GetStringPool()); } -static jboolean NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { +static jint NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { auto scoped_apk_assets = ScopedLock(ApkAssetsFromLong(ptr)); auto apk_assets = scoped_apk_assets->get(); - return apk_assets->IsUpToDate() ? JNI_TRUE : JNI_FALSE; + return (jint)apk_assets->IsUpToDate(); } static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring file_name) { @@ -558,7 +564,7 @@ static const JNINativeMethod gApkAssetsMethods[] = { {"nativeGetDebugName", "(J)Ljava/lang/String;", (void*)NativeGetDebugName}, {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock}, // @CriticalNative - {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate}, + {"nativeIsUpToDate", "(J)I", (void*)NativeIsUpToDate}, {"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml}, {"nativeGetOverlayableInfo", "(JLjava/lang/String;)Landroid/content/om/OverlayableInfo;", (void*)NativeGetOverlayableInfo}, 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..4f7ba9388a1d 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; 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/values/config.xml b/core/res/res/values/config.xml index 45a5d85a097d..c50c5e9d3341 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4244,12 +4244,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/strings.xml b/core/res/res/values/strings.xml index 6313054e47f5..ad9e7252c6a8 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -881,6 +881,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> @@ -4985,6 +4989,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..d20b95f59b0c 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3167,8 +3167,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 +3839,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"/> @@ -4025,6 +4032,7 @@ <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="config_defaultAutofillService" /> 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/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/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/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/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt index 06a55d3dbbd0..08079d94fcee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.apptoweb import android.app.assist.AssistContent -import android.app.assist.AssistContent.EXTRA_SESSION_TRANSFER_WEB_URI import android.content.Context import android.content.Intent import android.content.Intent.ACTION_VIEW @@ -113,5 +112,5 @@ fun getDomainVerificationUserState( * Returns the web uri from the given [AssistContent]. */ fun AssistContent.getSessionWebUri(): Uri? { - return extras.getParcelable(EXTRA_SESSION_TRANSFER_WEB_URI) ?: webUri + return sessionTransferUri ?: webUri } 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/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/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 1408b6efc7f9..2600bcc18f72 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 @@ -108,7 +108,6 @@ 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; @@ -438,29 +437,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 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..9e2b9b20be16 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 @@ -117,6 +117,7 @@ public abstract class Pip2Module { PipTouchHandler pipTouchHandler, PipAppOpsListener pipAppOpsListener, PhonePipMenuController pipMenuController, + PipUiEventLogger pipUiEventLogger, @ShellMainThread ShellExecutor mainExecutor) { if (!PipUtils.isPip2ExperimentEnabled()) { return Optional.empty(); @@ -126,7 +127,7 @@ public abstract class Pip2Module { displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, pipTransitionState, pipTouchHandler, pipAppOpsListener, pipMenuController, - mainExecutor)); + pipUiEventLogger, mainExecutor)); } } @@ -188,11 +189,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/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index ee817b34b24a..050dfb6f562c 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 @@ -310,6 +310,11 @@ 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]. */ @@ -1344,7 +1349,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 +1398,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) 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..9bf5555fc194 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 @@ -236,7 +236,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() 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..a62dd1c83520 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; } 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/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/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 032dac9ff3a2..76496b06a4dd 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 @@ -1227,19 +1227,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) { 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..5aa329108596 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 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/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/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/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index da27c08920dc..692b50303038 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 @@ -1497,7 +1497,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 +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_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { val task = setUpFreeformTask() @@ -1965,7 +1965,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 +2011,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 +2025,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() @@ -2095,7 +2095,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 +2147,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 +2808,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 +2849,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 +2867,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 +2934,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 +2974,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 +2992,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 +3084,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() @@ -3596,7 +3596,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() 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..89ab65a42bbf 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 @@ -239,7 +239,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) 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..32bb8bbdbbe3 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,25 @@ public class DragAndDropControllerTest extends ShellTestCase { mController.onDrag(dragLayout, event); verify(mDragAndDropListener, never()).onDragStarted(); } + + @Test + public void testOnDragStarted_withNoClipData() { + final View dragLayout = mock(View.class); + final Display display = mock(Display.class); + doReturn(display).when(dragLayout).getDisplay(); + doReturn(DEFAULT_DISPLAY).when(display).getDisplayId(); + + final ClipData clipData = createAppClipData(MIMETYPE_APPLICATION_SHORTCUT); + final DragEvent event = mock(DragEvent.class); + doReturn(ACTION_DRAG_STARTED).when(event).getAction(); + doReturn(null).when(event).getClipData(); + doReturn(clipData.getDescription()).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/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 7dac0859b7e9..6b02aeffd42a 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 @@ -20,7 +20,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static android.app.assist.AssistContent.EXTRA_SESSION_TRANSFER_WEB_URI; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; @@ -1176,7 +1175,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) - public void webUriLink_webUriLinkUsedWhenWhenAvailable() { + public void sessionTransferUri_sessionTransferUriUsedWhenWhenAvailable() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); final DesktopModeWindowDecoration decor = createWindowDecoration( taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */, @@ -1188,7 +1187,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) - public void webUriLink_webUriLinkUsedWhenSessionTransferUriUnavailable() { + public void webUri_webUriUsedWhenSessionTransferUriUnavailable() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); final DesktopModeWindowDecoration decor = createWindowDecoration( taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */, @@ -1200,7 +1199,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) - public void genericLink_genericLinkUsedWhenCapturedLinkAndWebUriUnavailable() { + public void genericLink_genericLinkUsedWhenCapturedLinkAndAssistContentUriUnavailable() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); final DesktopModeWindowDecoration decor = createWindowDecoration( taskInfo, null /* captured link */, null /* web uri */, @@ -1490,7 +1489,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { taskInfo.capturedLink = capturedLink; taskInfo.capturedLinkTimestamp = System.currentTimeMillis(); mAssistContent.setWebUri(webUri); - mAssistContent.getExtras().putObject(EXTRA_SESSION_TRANSFER_WEB_URI, sessionTransferUri); + mAssistContent.setSessionTransferUri(sessionTransferUri); final String genericLinkString = genericLink == null ? null : genericLink.toString(); doReturn(genericLinkString).when(mMockGenericLinksParser).getGenericLink(any()); // Relayout to set captured link diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp index dbb891455ddd..e693fcfd3918 100644 --- a/libs/androidfw/ApkAssets.cpp +++ b/libs/androidfw/ApkAssets.cpp @@ -162,10 +162,13 @@ const std::string& ApkAssets::GetDebugName() const { return assets_provider_->GetDebugName(); } -bool ApkAssets::IsUpToDate() const { +UpToDate ApkAssets::IsUpToDate() const { // Loaders are invalidated by the app, not the system, so assume they are up to date. - return IsLoader() || ((!loaded_idmap_ || loaded_idmap_->IsUpToDate()) - && assets_provider_->IsUpToDate()); + if (IsLoader()) { + return UpToDate::Always; + } + const auto idmap_res = loaded_idmap_ ? loaded_idmap_->IsUpToDate() : UpToDate::Always; + return combine(idmap_res, [this] { return assets_provider_->IsUpToDate(); }); } } // namespace android diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp index 2d3c06506a1f..11b12eb030a6 100644 --- a/libs/androidfw/AssetsProvider.cpp +++ b/libs/androidfw/AssetsProvider.cpp @@ -24,9 +24,8 @@ #include <ziparchive/zip_archive.h> namespace android { -namespace { -constexpr const char* kEmptyDebugString = "<empty>"; -} // namespace + +static constexpr std::string_view kEmptyDebugString = "<empty>"; std::unique_ptr<Asset> AssetsProvider::Open(const std::string& path, Asset::AccessMode mode, bool* file_exists) const { @@ -86,11 +85,9 @@ void ZipAssetsProvider::ZipCloser::operator()(ZipArchive* a) const { } ZipAssetsProvider::ZipAssetsProvider(ZipArchiveHandle handle, PathOrDebugName&& path, - package_property_t flags, time_t last_mod_time) - : zip_handle_(handle), - name_(std::move(path)), - flags_(flags), - last_mod_time_(last_mod_time) {} + package_property_t flags, ModDate last_mod_time) + : zip_handle_(handle), name_(std::move(path)), flags_(flags), last_mod_time_(last_mod_time) { +} std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, package_property_t flags, @@ -104,10 +101,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, return {}; } - struct stat sb{.st_mtime = -1}; + ModDate mod_date = kInvalidModDate; // Skip all up-to-date checks if the file won't ever change. - if (!isReadonlyFilesystem(path.c_str())) { - if ((released_fd < 0 ? stat(path.c_str(), &sb) : fstat(released_fd, &sb)) < 0) { + if (isKnownWritablePath(path.c_str()) || !isReadonlyFilesystem(GetFileDescriptor(handle))) { + if (mod_date = getFileModDate(GetFileDescriptor(handle)); mod_date == kInvalidModDate) { // Stat requires execute permissions on all directories path to the file. If the process does // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will // always have to return true. @@ -116,7 +113,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, } return std::unique_ptr<ZipAssetsProvider>( - new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, sb.st_mtime)); + new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, mod_date)); } std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, @@ -137,10 +134,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, return {}; } - struct stat sb{.st_mtime = -1}; + ModDate mod_date = kInvalidModDate; // Skip all up-to-date checks if the file won't ever change. if (!isReadonlyFilesystem(released_fd)) { - if (fstat(released_fd, &sb) < 0) { + if (mod_date = getFileModDate(released_fd); mod_date == kInvalidModDate) { // Stat requires execute permissions on all directories path to the file. If the process does // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will // always have to return true. @@ -150,7 +147,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, } return std::unique_ptr<ZipAssetsProvider>(new ZipAssetsProvider( - handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, sb.st_mtime)); + handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, mod_date)); } std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path, @@ -282,21 +279,16 @@ const std::string& ZipAssetsProvider::GetDebugName() const { return name_.GetDebugName(); } -bool ZipAssetsProvider::IsUpToDate() const { - if (last_mod_time_ == -1) { - return true; - } - struct stat sb{}; - if (fstat(GetFileDescriptor(zip_handle_.get()), &sb) < 0) { - // If fstat fails on the zip archive, return true so the zip archive the resource system does - // attempt to refresh the ApkAsset. - return true; +UpToDate ZipAssetsProvider::IsUpToDate() const { + if (last_mod_time_ == kInvalidModDate) { + return UpToDate::Always; } - return last_mod_time_ == sb.st_mtime; + return fromBool(last_mod_time_ == getFileModDate(GetFileDescriptor(zip_handle_.get()))); } -DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time) - : dir_(std::move(path)), last_mod_time_(last_mod_time) {} +DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time) + : dir_(std::move(path)), last_mod_time_(last_mod_time) { +} std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::string path) { struct stat sb; @@ -317,7 +309,7 @@ std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::st const bool isReadonly = isReadonlyFilesystem(path.c_str()); return std::unique_ptr<DirectoryAssetsProvider>( - new DirectoryAssetsProvider(std::move(path), isReadonly ? -1 : sb.st_mtime)); + new DirectoryAssetsProvider(std::move(path), isReadonly ? kInvalidModDate : getModDate(sb))); } std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path, @@ -346,17 +338,11 @@ const std::string& DirectoryAssetsProvider::GetDebugName() const { return dir_; } -bool DirectoryAssetsProvider::IsUpToDate() const { - if (last_mod_time_ == -1) { - return true; - } - struct stat sb; - if (stat(dir_.c_str(), &sb) < 0) { - // If stat fails on the zip archive, return true so the zip archive the resource system does - // attempt to refresh the ApkAsset. - return true; +UpToDate DirectoryAssetsProvider::IsUpToDate() const { + if (last_mod_time_ == kInvalidModDate) { + return UpToDate::Always; } - return last_mod_time_ == sb.st_mtime; + return fromBool(last_mod_time_ == getFileModDate(dir_.c_str())); } MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary, @@ -369,8 +355,14 @@ MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& prima std::unique_ptr<AssetsProvider> MultiAssetsProvider::Create( std::unique_ptr<AssetsProvider>&& primary, std::unique_ptr<AssetsProvider>&& secondary) { - if (primary == nullptr || secondary == nullptr) { - return nullptr; + if (primary == nullptr && secondary == nullptr) { + return EmptyAssetsProvider::Create(); + } + if (!primary) { + return secondary; + } + if (!secondary) { + return primary; } return std::unique_ptr<MultiAssetsProvider>(new MultiAssetsProvider(std::move(primary), std::move(secondary))); @@ -397,8 +389,8 @@ const std::string& MultiAssetsProvider::GetDebugName() const { return debug_name_; } -bool MultiAssetsProvider::IsUpToDate() const { - return primary_->IsUpToDate() && secondary_->IsUpToDate(); +UpToDate MultiAssetsProvider::IsUpToDate() const { + return combine(primary_->IsUpToDate(), [this] { return secondary_->IsUpToDate(); }); } EmptyAssetsProvider::EmptyAssetsProvider(std::optional<std::string>&& path) : @@ -438,12 +430,12 @@ const std::string& EmptyAssetsProvider::GetDebugName() const { if (path_.has_value()) { return *path_; } - const static std::string kEmpty = kEmptyDebugString; + constexpr static std::string kEmpty{kEmptyDebugString}; return kEmpty; } -bool EmptyAssetsProvider::IsUpToDate() const { - return true; +UpToDate EmptyAssetsProvider::IsUpToDate() const { + return UpToDate::Always; } } // namespace android diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp index 3ecd82b074a1..262e7df185b7 100644 --- a/libs/androidfw/Idmap.cpp +++ b/libs/androidfw/Idmap.cpp @@ -22,9 +22,10 @@ #include "android-base/logging.h" #include "android-base/stringprintf.h" #include "android-base/utf8.h" -#include "androidfw/misc.h" +#include "androidfw/AssetManager.h" #include "androidfw/ResourceTypes.h" #include "androidfw/Util.h" +#include "androidfw/misc.h" #include "utils/ByteOrder.h" #include "utils/Trace.h" @@ -268,11 +269,16 @@ LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* head configurations_(configs), overlay_entries_(overlay_entries), string_pool_(std::move(string_pool)), - idmap_fd_( - android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)), overlay_apk_path_(overlay_apk_path), target_apk_path_(target_apk_path), - idmap_last_mod_time_(getFileModDate(idmap_fd_.get())) { + idmap_last_mod_time_(kInvalidModDate) { + if (!isReadonlyFilesystem(std::string(overlay_apk_path_).c_str()) || + !(target_apk_path_ == AssetManager::TARGET_APK_PATH || + isReadonlyFilesystem(std::string(target_apk_path_).c_str()))) { + idmap_fd_.reset( + android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)); + idmap_last_mod_time_ = getFileModDate(idmap_fd_); + } } std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPiece idmap_data) { @@ -381,8 +387,11 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie overlay_entries, std::move(idmap_string_pool), *overlay_path, *target_path)); } -bool LoadedIdmap::IsUpToDate() const { - return idmap_last_mod_time_ == getFileModDate(idmap_fd_.get()); +UpToDate LoadedIdmap::IsUpToDate() const { + if (idmap_last_mod_time_ == kInvalidModDate) { + return UpToDate::Always; + } + return fromBool(idmap_last_mod_time_ == getFileModDate(idmap_fd_.get())); } } // namespace android diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index de9991a8be5e..a8eb062a2ece 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -152,12 +152,11 @@ static void fill9patchOffsets(Res_png_9patch* patch) { patch->colorsOffset = patch->yDivsOffset + (patch->numYDivs * sizeof(int32_t)); } -void Res_value::copyFrom_dtoh(const Res_value& src) -{ - size = dtohs(src.size); - res0 = src.res0; - dataType = src.dataType; - data = dtohl(src.data); +void Res_value::copyFrom_dtoh_slow(const Res_value& src) { + size = dtohs(src.size); + res0 = src.res0; + dataType = src.dataType; + data = dtohl(src.data); } void Res_png_9patch::deviceToFile() @@ -2031,16 +2030,6 @@ status_t ResXMLTree::validateNode(const ResXMLTree_node* node) const // -------------------------------------------------------------------- // -------------------------------------------------------------------- -void ResTable_config::copyFromDeviceNoSwap(const ResTable_config& o) { - const size_t size = dtohl(o.size); - if (size >= sizeof(ResTable_config)) { - *this = o; - } else { - memcpy(this, &o, size); - memset(((uint8_t*)this)+size, 0, sizeof(ResTable_config)-size); - } -} - /* static */ size_t unpackLanguageOrRegion(const char in[2], const char base, char out[4]) { if (in[0] & 0x80) { @@ -2105,34 +2094,33 @@ size_t ResTable_config::unpackRegion(char region[4]) const { return unpackLanguageOrRegion(this->country, '0', region); } - -void ResTable_config::copyFromDtoH(const ResTable_config& o) { - copyFromDeviceNoSwap(o); - size = sizeof(ResTable_config); - mcc = dtohs(mcc); - mnc = dtohs(mnc); - density = dtohs(density); - screenWidth = dtohs(screenWidth); - screenHeight = dtohs(screenHeight); - sdkVersion = dtohs(sdkVersion); - minorVersion = dtohs(minorVersion); - smallestScreenWidthDp = dtohs(smallestScreenWidthDp); - screenWidthDp = dtohs(screenWidthDp); - screenHeightDp = dtohs(screenHeightDp); -} - -void ResTable_config::swapHtoD() { - size = htodl(size); - mcc = htods(mcc); - mnc = htods(mnc); - density = htods(density); - screenWidth = htods(screenWidth); - screenHeight = htods(screenHeight); - sdkVersion = htods(sdkVersion); - minorVersion = htods(minorVersion); - smallestScreenWidthDp = htods(smallestScreenWidthDp); - screenWidthDp = htods(screenWidthDp); - screenHeightDp = htods(screenHeightDp); +void ResTable_config::copyFromDtoH_slow(const ResTable_config& o) { + copyFromDeviceNoSwap(o); + size = sizeof(ResTable_config); + mcc = dtohs(mcc); + mnc = dtohs(mnc); + density = dtohs(density); + screenWidth = dtohs(screenWidth); + screenHeight = dtohs(screenHeight); + sdkVersion = dtohs(sdkVersion); + minorVersion = dtohs(minorVersion); + smallestScreenWidthDp = dtohs(smallestScreenWidthDp); + screenWidthDp = dtohs(screenWidthDp); + screenHeightDp = dtohs(screenHeightDp); +} + +void ResTable_config::swapHtoD_slow() { + size = htodl(size); + mcc = htods(mcc); + mnc = htods(mnc); + density = htods(density); + screenWidth = htods(screenWidth); + screenHeight = htods(screenHeight); + sdkVersion = htods(sdkVersion); + minorVersion = htods(minorVersion); + smallestScreenWidthDp = htods(smallestScreenWidthDp); + screenWidthDp = htods(screenWidthDp); + screenHeightDp = htods(screenHeightDp); } /* static */ inline int compareLocales(const ResTable_config &l, const ResTable_config &r) { @@ -2145,7 +2133,7 @@ void ResTable_config::swapHtoD() { // systems should happen very infrequently (if at all.) // The comparison code relies on memcmp low-level optimizations that make it // more efficient than strncmp. - const char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'}; + static constexpr char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'}; const char *lScript = l.localeScriptWasComputed ? emptyScript : l.localeScript; const char *rScript = r.localeScriptWasComputed ? emptyScript : r.localeScript; diff --git a/libs/androidfw/Util.cpp b/libs/androidfw/Util.cpp index be55fe8b4bb6..86c459fb4647 100644 --- a/libs/androidfw/Util.cpp +++ b/libs/androidfw/Util.cpp @@ -32,13 +32,18 @@ namespace android { namespace util { void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out) { - char buf[5]; - while (*src && len != 0) { - char16_t c = static_cast<char16_t>(dtohs(*src)); - utf16_to_utf8(&c, 1, buf, sizeof(buf)); - out->append(buf, strlen(buf)); - ++src; - --len; + static constexpr bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001; + if constexpr (kDeviceEndiannessSame) { + *out = Utf16ToUtf8({(const char16_t*)src, strnlen16((const char16_t*)src, len)}); + } else { + char buf[5]; + while (*src && len != 0) { + char16_t c = static_cast<char16_t>(dtohs(*src)); + utf16_to_utf8(&c, 1, buf, sizeof(buf)); + out->append(buf, strlen(buf)); + ++src; + --len; + } } } @@ -63,8 +68,10 @@ std::string Utf16ToUtf8(StringPiece16 utf16) { } std::string utf8; - utf8.resize(utf8_length); - utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin(), utf8_length + 1); + utf8.resize_and_overwrite(utf8_length, [&utf16](char* data, size_t size) { + utf16_to_utf8(utf16.data(), utf16.length(), data, size + 1); + return size; + }); return utf8; } diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h index 231808beb718..3f6f4661f2f7 100644 --- a/libs/androidfw/include/androidfw/ApkAssets.h +++ b/libs/androidfw/include/androidfw/ApkAssets.h @@ -116,7 +116,7 @@ class ApkAssets : public RefBase { return resources_asset_ != nullptr && resources_asset_->isAllocated(); } - bool IsUpToDate() const; + UpToDate IsUpToDate() const; // DANGER! // This is a destructive method that rips the assets provider out of ApkAssets object. diff --git a/libs/androidfw/include/androidfw/AssetsProvider.h b/libs/androidfw/include/androidfw/AssetsProvider.h index d33c325ff369..e3b3ae41f7f4 100644 --- a/libs/androidfw/include/androidfw/AssetsProvider.h +++ b/libs/androidfw/include/androidfw/AssetsProvider.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef ANDROIDFW_ASSETSPROVIDER_H -#define ANDROIDFW_ASSETSPROVIDER_H +#pragma once #include <memory> #include <string> @@ -58,7 +57,7 @@ struct AssetsProvider { WARN_UNUSED virtual const std::string& GetDebugName() const = 0; // Returns whether the interface provides the most recent version of its files. - WARN_UNUSED virtual bool IsUpToDate() const = 0; + WARN_UNUSED virtual UpToDate IsUpToDate() const = 0; // Creates an Asset from a file on disk. static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path); @@ -95,7 +94,7 @@ struct ZipAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; WARN_UNUSED std::optional<uint32_t> GetCrc(std::string_view path) const; ~ZipAssetsProvider() override = default; @@ -106,7 +105,7 @@ struct ZipAssetsProvider : public AssetsProvider { private: struct PathOrDebugName; ZipAssetsProvider(ZipArchive* handle, PathOrDebugName&& path, package_property_t flags, - time_t last_mod_time); + ModDate last_mod_time); struct PathOrDebugName { static PathOrDebugName Path(std::string value) { @@ -135,7 +134,7 @@ struct ZipAssetsProvider : public AssetsProvider { std::unique_ptr<ZipArchive, ZipCloser> zip_handle_; PathOrDebugName name_; package_property_t flags_; - time_t last_mod_time_; + ModDate last_mod_time_; }; // Supplies assets from a root directory. @@ -147,7 +146,7 @@ struct DirectoryAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; ~DirectoryAssetsProvider() override = default; protected: @@ -156,23 +155,23 @@ struct DirectoryAssetsProvider : public AssetsProvider { bool* file_exists) const override; private: - explicit DirectoryAssetsProvider(std::string&& path, time_t last_mod_time); + explicit DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time); std::string dir_; - time_t last_mod_time_; + ModDate last_mod_time_; }; // Supplies assets from a `primary` asset provider and falls back to supplying assets from the // `secondary` asset provider if the asset cannot be found in the `primary`. struct MultiAssetsProvider : public AssetsProvider { static std::unique_ptr<AssetsProvider> Create(std::unique_ptr<AssetsProvider>&& primary, - std::unique_ptr<AssetsProvider>&& secondary); + std::unique_ptr<AssetsProvider>&& secondary = {}); bool ForEachFile(const std::string& root_path, base::function_ref<void(StringPiece, FileType)> f) const override; WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; ~MultiAssetsProvider() override = default; protected: @@ -199,7 +198,7 @@ struct EmptyAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; ~EmptyAssetsProvider() override = default; protected: @@ -212,5 +211,3 @@ struct EmptyAssetsProvider : public AssetsProvider { }; } // namespace android - -#endif /* ANDROIDFW_ASSETSPROVIDER_H */ diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index ac75eb3bb98c..87f3c9df9a91 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef IDMAP_H_ -#define IDMAP_H_ +#pragma once #include <memory> #include <string> @@ -32,6 +31,31 @@ namespace android { +// An enum that tracks more states than just 'up to date' or 'not' for a resources container: +// there are several cases where we know for sure that the object can't change and won't get +// out of date. Reporting those states to the managed layer allows it to stop checking here +// completely, speeding up the cache lookups by dozens of milliseconds. +enum class UpToDate : int { False, True, Always }; + +// Combines two UpToDate values, and only accesses the second one if it matters to the result. +template <class Getter> +UpToDate combine(UpToDate first, Getter secondGetter) { + switch (first) { + case UpToDate::False: + return UpToDate::False; + case UpToDate::True: { + const auto second = secondGetter(); + return second == UpToDate::False ? UpToDate::False : UpToDate::True; + } + case UpToDate::Always: + return secondGetter(); + } +} + +inline UpToDate fromBool(bool value) { + return value ? UpToDate::True : UpToDate::False; +} + class LoadedIdmap; class IdmapResMap; struct Idmap_header; @@ -196,7 +220,7 @@ class LoadedIdmap { // Returns whether the idmap file on disk has not been modified since the construction of this // LoadedIdmap. - bool IsUpToDate() const; + UpToDate IsUpToDate() const; protected: // Exposed as protected so that tests can subclass and mock this class out. @@ -231,5 +255,3 @@ class LoadedIdmap { }; } // namespace android - -#endif // IDMAP_H_ diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index e330410ed1a0..819fe4b38c87 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -47,6 +47,8 @@ namespace android { +constexpr const bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001; + constexpr const uint32_t kIdmapMagic = 0x504D4449u; constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Au; @@ -408,7 +410,16 @@ struct Res_value typedef uint32_t data_type; data_type data; - void copyFrom_dtoh(const Res_value& src); + void copyFrom_dtoh(const Res_value& src) { + if constexpr (kDeviceEndiannessSame) { + *this = src; + } else { + copyFrom_dtoh_slow(src); + } + } + + private: + void copyFrom_dtoh_slow(const Res_value& src); }; /** @@ -1254,11 +1265,32 @@ struct ResTable_config // Varies in length from 3 to 8 chars. Zero-filled value. char localeNumberingSystem[8]; - void copyFromDeviceNoSwap(const ResTable_config& o); - - void copyFromDtoH(const ResTable_config& o); - - void swapHtoD(); + void copyFromDeviceNoSwap(const ResTable_config& o) { + const auto o_size = dtohl(o.size); + if (o_size >= sizeof(ResTable_config)) [[likely]] { + *this = o; + } else { + memcpy(this, &o, o_size); + memset(((uint8_t*)this) + o_size, 0, sizeof(ResTable_config) - o_size); + } + this->size = sizeof(*this); + } + + void copyFromDtoH(const ResTable_config& o) { + if constexpr (kDeviceEndiannessSame) { + copyFromDeviceNoSwap(o); + } else { + copyFromDtoH_slow(o); + } + } + + void swapHtoD() { + if constexpr (kDeviceEndiannessSame) { + ; // noop + } else { + swapHtoD_slow(); + } + } int compare(const ResTable_config& o) const; int compareLogical(const ResTable_config& o) const; @@ -1384,6 +1416,10 @@ struct ResTable_config bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config* requested) const; String8 toString() const; + + private: + void copyFromDtoH_slow(const ResTable_config& o); + void swapHtoD_slow(); }; /** diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h index c9ba8a01a5e9..d8ca64a174a2 100644 --- a/libs/androidfw/include/androidfw/misc.h +++ b/libs/androidfw/include/androidfw/misc.h @@ -15,6 +15,7 @@ */ #pragma once +#include <sys/stat.h> #include <time.h> // @@ -64,10 +65,15 @@ ModDate getFileModDate(const char* fileName); /* same, but also returns -1 if the file has already been deleted */ ModDate getFileModDate(int fd); +// Extract the modification date from the stat structure. +ModDate getModDate(const struct ::stat& st); + // Check if |path| or |fd| resides on a readonly filesystem. bool isReadonlyFilesystem(const char* path); bool isReadonlyFilesystem(int fd); +bool isKnownWritablePath(const char* path); + } // namespace android // Whoever uses getFileModDate() will need this as well diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp index 32f3624a3aee..26eb320805c9 100644 --- a/libs/androidfw/misc.cpp +++ b/libs/androidfw/misc.cpp @@ -16,10 +16,10 @@ #define LOG_TAG "misc" -// -// Miscellaneous utility functions. -// -#include <androidfw/misc.h> +#include "androidfw/misc.h" + +#include <errno.h> +#include <sys/stat.h> #include "android-base/logging.h" @@ -28,9 +28,7 @@ #include <sys/vfs.h> #endif // __linux__ -#include <errno.h> -#include <sys/stat.h> - +#include <array> #include <cstdio> #include <cstring> #include <tuple> @@ -40,28 +38,26 @@ namespace android { /* * Get a file's type. */ -FileType getFileType(const char* fileName) -{ - struct stat sb; - - if (stat(fileName, &sb) < 0) { - if (errno == ENOENT || errno == ENOTDIR) - return kFileTypeNonexistent; - else { - PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed"; - return kFileTypeUnknown; - } - } else { - if (S_ISREG(sb.st_mode)) - return kFileTypeRegular; - else if (S_ISDIR(sb.st_mode)) - return kFileTypeDirectory; - else if (S_ISCHR(sb.st_mode)) - return kFileTypeCharDev; - else if (S_ISBLK(sb.st_mode)) - return kFileTypeBlockDev; - else if (S_ISFIFO(sb.st_mode)) - return kFileTypeFifo; +FileType getFileType(const char* fileName) { + struct stat sb; + if (stat(fileName, &sb) < 0) { + if (errno == ENOENT || errno == ENOTDIR) + return kFileTypeNonexistent; + else { + PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed"; + return kFileTypeUnknown; + } + } else { + if (S_ISREG(sb.st_mode)) + return kFileTypeRegular; + else if (S_ISDIR(sb.st_mode)) + return kFileTypeDirectory; + else if (S_ISCHR(sb.st_mode)) + return kFileTypeCharDev; + else if (S_ISBLK(sb.st_mode)) + return kFileTypeBlockDev; + else if (S_ISFIFO(sb.st_mode)) + return kFileTypeFifo; #if defined(S_ISLNK) else if (S_ISLNK(sb.st_mode)) return kFileTypeSymlink; @@ -75,7 +71,7 @@ FileType getFileType(const char* fileName) } } -static ModDate getModDate(const struct stat& st) { +ModDate getModDate(const struct stat& st) { #ifdef _WIN32 return st.st_mtime; #elif defined(__APPLE__) @@ -113,8 +109,14 @@ bool isReadonlyFilesystem(const char*) { bool isReadonlyFilesystem(int) { return false; } +bool isKnownWritablePath(const char*) { + return false; +} #else // __linux__ bool isReadonlyFilesystem(const char* path) { + if (isKnownWritablePath(path)) { + return false; + } struct statfs sfs; if (::statfs(path, &sfs)) { PLOG(ERROR) << "isReadonlyFilesystem(): statfs(" << path << ") failed"; @@ -131,6 +133,13 @@ bool isReadonlyFilesystem(int fd) { } return (sfs.f_flags & ST_RDONLY) != 0; } + +bool isKnownWritablePath(const char* path) { + // We know that all paths in /data/ are writable. + static constexpr char kRwPrefix[] = "/data/"; + return strncmp(kRwPrefix, path, std::size(kRwPrefix) - 1) == 0; +} + #endif // __linux__ } // namespace android diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp index cb2e56f5f5e4..22b9e69500d9 100644 --- a/libs/androidfw/tests/Idmap_test.cpp +++ b/libs/androidfw/tests/Idmap_test.cpp @@ -218,10 +218,11 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { auto apk_assets = ApkAssets::LoadOverlay(temp_file.path); ASSERT_NE(nullptr, apk_assets); - ASSERT_TRUE(apk_assets->IsUpToDate()); + ASSERT_TRUE(apk_assets->IsOverlay()); + ASSERT_EQ(UpToDate::True, apk_assets->IsUpToDate()); unlink(temp_file.path); - ASSERT_FALSE(apk_assets->IsUpToDate()); + ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate()); const auto sleep_duration = std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull)); @@ -230,7 +231,27 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { base::WriteStringToFile("hello", temp_file.path); std::this_thread::sleep_for(sleep_duration); - ASSERT_FALSE(apk_assets->IsUpToDate()); + ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate()); +} + +TEST(IdmapTestUpToDate, Combine) { + ASSERT_EQ(UpToDate::False, combine(UpToDate::False, [] { + ADD_FAILURE(); // Shouldn't get called at all. + return UpToDate::False; + })); + + ASSERT_EQ(UpToDate::False, combine(UpToDate::True, [] { return UpToDate::False; })); + + ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::True; })); + ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::Always; })); + ASSERT_EQ(UpToDate::True, combine(UpToDate::Always, [] { return UpToDate::True; })); + + ASSERT_EQ(UpToDate::Always, combine(UpToDate::Always, [] { return UpToDate::Always; })); +} + +TEST(IdmapTestUpToDate, FromBool) { + ASSERT_EQ(UpToDate::False, fromBool(false)); + ASSERT_EQ(UpToDate::True, fromBool(true)); } } // namespace diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index 84f59b5d01ae..c9625c405faa 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -5866,14 +5866,21 @@ final public class MediaCodec { @NonNull MediaCodec codec, @NonNull MediaFormat format); /** - * Called when the metrics for this codec have been flushed due to the - * start of a new subsession. + * Called when the metrics for this codec have been flushed "mid-stream" + * due to the start of a new subsession during execution. * <p> - * This can happen when the codec is reconfigured after stop(), or - * mid-stream e.g. if the video size changes. When this happens, the - * metrics for the previous subsession are flushed, and - * {@link MediaCodec#getMetrics} will return the metrics for the - * new subsession. This happens just before the {@link Callback#onOutputFormatChanged} + * A new codec subsession normally starts when the codec is reconfigured + * after stop(), but it can also happen mid-stream e.g. if the video size + * changes. When this happens, the metrics for the previous subsession + * are flushed, and {@link MediaCodec#getMetrics} will return the metrics + * for the new subsession. + * <p> + * For subsessions that begin due to a reconfiguration, the metrics for + * the prior subsession can be retrieved via {@link MediaCodec#getMetrics} + * prior to calling {@link #configure}. + * <p> + * When a new subsession begins "mid-stream", the metrics for the prior + * subsession are flushed just before the {@link Callback#onOutputFormatChanged} * event, so this <b>optional</b> callback is provided to be able to * capture the final metrics for the previous subsession. * 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/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/InputDevices/res/raw/keyboard_layout_romanian.kcm b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm new file mode 100644 index 000000000000..b384a2418ff2 --- /dev/null +++ b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm @@ -0,0 +1,357 @@ +# 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. + +# +# Romanian keyboard layout. +# + +type OVERLAY + +map key 86 PLUS + +### ROW 1 + +key GRAVE { + label: '\u201e' + base: '\u201e' + shift: '\u201d' + ralt: '`' + ralt+shift: '~' +} + +key 1 { + label: '1' + base: '1' + shift: '!' + ralt: '\u0303' +} + +key 2 { + label: '2' + base: '2' + shift: '@' + ralt: '\u030C' +} + +key 3 { + label: '3' + base: '3' + shift: '#' + ralt: '\u0302' +} + +key 4 { + label: '4' + base: '4' + shift: '$' + ralt: '\u0306' +} + +key 5 { + label: '5' + base: '5' + shift: '%' + ralt: '\u030A' +} + +key 6 { + label: '6' + base: '6' + shift: '^' + ralt: '\u0328' +} + +key 7 { + label: '7' + base: '7' + shift: '&' + ralt: '\u0300' +} + +key 8 { + label: '8' + base: '8' + shift: '*' + ralt: '\u0307' +} + +key 9 { + label: '9' + base: '9' + shift: '(' + ralt: '\u0301' +} + +key 0 { + label: '0' + base: '0' + shift: ')' + ralt: '\u030B' +} + +key MINUS { + label: '-' + base: '-' + shift: '_' + ralt: '\u0308' + ralt+shift: '\u2013' +} + +key EQUALS { + label: '=' + base: '=' + shift: '+' + ralt: '\u0327' + ralt+shift: '\u00b1' +} + +### ROW 2 + +key Q { + label: 'Q' + base, capslock+shift: 'q' + shift, capslock: 'Q' +} + +key W { + label: 'W' + base, capslock+shift: 'w' + shift, capslock: 'W' +} + +key E { + label: 'E' + base, capslock+shift: 'e' + shift, capslock: 'E' + ralt: '\u20ac' +} + +key R { + label: 'R' + base, capslock+shift: 'r' + shift, capslock: 'R' +} + +key T { + label: 'T' + base, capslock+shift: 't' + shift, capslock: 'T' +} + +key Y { + label: 'Y' + base, capslock+shift: 'y' + shift, capslock: 'Y' +} + +key U { + label: 'U' + base, capslock+shift: 'u' + shift, capslock: 'U' +} + +key I { + label: 'I' + base, capslock+shift: 'i' + shift, capslock: 'I' +} + +key O { + label: 'O' + base, capslock+shift: 'o' + shift, capslock: 'O' +} + +key P { + label: 'P' + base, capslock+shift: 'p' + shift, capslock: 'P' + ralt: '\u00a7' +} + +key LEFT_BRACKET { + label: '\u0102' + base, capslock+shift: '\u0103' + shift, capslock: '\u0102' + ralt: '[' + ralt+shift: '{' +} + +key RIGHT_BRACKET { + label: '\u00ce' + base, capslock+shift: '\u00ee' + shift, capslock: '\u00ce' + ralt: ']' + ralt+shift: '}' +} + +### ROW 3 + +key A { + label: 'A' + base, capslock+shift: 'a' + shift, capslock: 'A' +} + +key S { + label: 'S' + base, capslock+shift: 's' + shift, capslock: 'S' + ralt: '\u00df' +} + +key D { + label: 'D' + base, capslock+shift: 'd' + shift, capslock: 'D' + ralt: '\u0111' + ralt+shift, ralt+capslock: '\u0110' + ralt+shift+capslock: '\u0111' +} + +key F { + label: 'F' + base, capslock+shift: 'f' + shift, capslock: 'F' +} + +key G { + label: 'G' + base, capslock+shift: 'g' + shift, capslock: 'G' +} + +key H { + label: 'H' + base, capslock+shift: 'h' + shift, capslock: 'H' +} + +key J { + label: 'J' + base, capslock+shift: 'j' + shift, capslock: 'J' +} + +key K { + label: 'K' + base, capslock+shift: 'k' + shift, capslock: 'K' +} + +key L { + label: 'L' + base, capslock+shift: 'l' + shift, capslock: 'L' + ralt: '\u0142' + ralt+shift, ralt+capslock: '\u0141' + ralt+shift+capslock: '\u0142' +} + +key SEMICOLON { + label: '\u0218' + base, capslock+shift: '\u0219' + shift, capslock: '\u0218' + ralt: ';' + ralt+shift: ':' +} + +key APOSTROPHE { + label: '\u021a' + base, capslock+shift: '\u021b' + shift, capslock: '\u021a' + ralt: '\'' + ralt+shift: '\u0022' +} + +key BACKSLASH { + label: '\u00c2' + base, capslock+shift: '\u00e2' + shift, capslock: '\u00c2' + ralt: '\\' + ralt+shift: '|' +} + +### ROW 4 + +key PLUS { + label: '\\' + base: '\\' + shift: '|' +} + +key Z { + label: 'Z' + base, capslock+shift: 'z' + shift, capslock: 'Z' +} + +key X { + label: 'X' + base, capslock+shift: 'x' + shift, capslock: 'X' +} + +key C { + label: 'C' + base, capslock+shift: 'c' + shift, capslock: 'C' + ralt: '\u00a9' +} + +key V { + label: 'V' + base, capslock+shift: 'v' + shift, capslock: 'V' +} + +key B { + label: 'B' + base, capslock+shift: 'b' + shift, capslock: 'B' +} + +key N { + label: 'N' + base, capslock+shift: 'n' + shift, capslock: 'N' +} + +key M { + label: 'M' + base, capslock+shift: 'm' + shift, capslock: 'M' +} + +key COMMA { + label: ',' + base: ',' + shift: ';' + ralt: '<' + ralt+shift: '\u00ab' +} + +key PERIOD { + label: '.' + base: '.' + shift: ':' + ralt: '>' + ralt+shift: '\u00bb' +} + +key SLASH { + label: '/' + base: '/' + shift: '?' +} diff --git a/packages/InputDevices/res/values/strings.xml b/packages/InputDevices/res/values/strings.xml index 5a911256d9be..bd7cdc481524 100644 --- a/packages/InputDevices/res/values/strings.xml +++ b/packages/InputDevices/res/values/strings.xml @@ -164,4 +164,7 @@ <!-- Montenegrin (Cyrillic) keyboard layout label. [CHAR LIMIT=35] --> <string name="keyboard_layout_montenegrin_cyrillic">Montenegrin (Cyrillic)</string> + + <!-- Romanian keyboard layout label. [CHAR LIMIT=35] --> + <string name="keyboard_layout_romanian">Romanian</string> </resources> diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml index 93094890418d..9ce9a87a1f9f 100644 --- a/packages/InputDevices/res/xml/keyboard_layouts.xml +++ b/packages/InputDevices/res/xml/keyboard_layouts.xml @@ -360,4 +360,11 @@ android:keyboardLayout="@raw/keyboard_layout_serbian_and_montenegrin_cyrillic" android:keyboardLocale="cnr-Cyrl-ME" android:keyboardLayoutType="extended" /> + + <keyboard-layout + android:name="keyboard_layout_romanian" + android:label="@string/keyboard_layout_romanian" + android:keyboardLayout="@raw/keyboard_layout_romanian" + android:keyboardLocale="ro-Latn-RO" + android:keyboardLayoutType="qwerty" /> </keyboard-layouts> 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/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/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/framework/common/BytesFormatter.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt new file mode 100644 index 000000000000..5b7e2a86135a --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt @@ -0,0 +1,83 @@ +/* + * 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.settingslib.spaprivileged.framework.common + +import android.content.Context +import android.content.res.Resources +import android.icu.text.DecimalFormat +import android.icu.text.MeasureFormat +import android.icu.text.NumberFormat +import android.icu.text.UnicodeSet +import android.icu.text.UnicodeSetSpanner +import android.icu.util.Measure +import android.text.format.Formatter +import android.text.format.Formatter.RoundedBytesResult +import java.math.BigDecimal + +class BytesFormatter(resources: Resources) { + + enum class UseCase(val flag: Int) { + FileSize(Formatter.FLAG_SI_UNITS), + DataUsage(Formatter.FLAG_IEC_UNITS), + } + + data class Result(val number: String, val units: String) + + constructor(context: Context) : this(context.resources) + + private val locale = resources.configuration.locales[0] + + fun format(bytes: Long, useCase: UseCase): String { + val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag) + val numberFormatter = getNumberFormatter(rounded.fractionDigits) + return numberFormatter.formatRoundedBytesResult(rounded) + } + + fun formatWithUnits(bytes: Long, useCase: UseCase): Result { + val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag) + val numberFormatter = getNumberFormatter(rounded.fractionDigits) + val formattedString = numberFormatter.formatRoundedBytesResult(rounded) + val formattedNumber = numberFormatter.format(rounded.value) + return Result( + number = formattedNumber, + units = formattedString.removeFirst(formattedNumber), + ) + } + + private fun NumberFormat.formatRoundedBytesResult(rounded: RoundedBytesResult): String { + val measureFormatter = + MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.SHORT, this) + return measureFormatter.format(Measure(rounded.value, rounded.units)) + } + + private fun getNumberFormatter(fractionDigits: Int) = + NumberFormat.getInstance(locale).apply { + minimumFractionDigits = fractionDigits + maximumFractionDigits = fractionDigits + isGroupingUsed = false + if (this is DecimalFormat) { + setRoundingMode(BigDecimal.ROUND_HALF_UP) + } + } + + private companion object { + fun String.removeFirst(removed: String): String = + SPACES_AND_CONTROLS.trim(replaceFirst(removed, "")).toString() + + val SPACES_AND_CONTROLS = UnicodeSetSpanner(UnicodeSet("[[:Zs:][:Cf:]]").freeze()) + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt new file mode 100644 index 000000000000..7220848eebff --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt @@ -0,0 +1,178 @@ +/* + * 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.settingslib.spaprivileged.framework.common + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BytesFormatterTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val formatter = BytesFormatter(context) + + @Test + fun `Zero bytes`() { + // Given a byte value of 0, the formatted output should be "0 byte" for both FileSize + // and DataUsage UseCases. This verifies special handling of zero values. + + val fileSizeResult = formatter.format(0, BytesFormatter.UseCase.FileSize) + assertThat(fileSizeResult).isEqualTo("0 byte") + + val dataUsageResult = formatter.format(0, BytesFormatter.UseCase.DataUsage) + assertThat(dataUsageResult).isEqualTo("0 byte") + } + + @Test + fun `Positive bytes`() { + // Given a positive byte value (e.g., 1000), the formatted output should be correctly + // displayed with appropriate units (e.g., '1.00 kB') for both UseCases. + + val fileSizeResult = formatter.format(1000, BytesFormatter.UseCase.FileSize) + assertThat(fileSizeResult).isEqualTo("1.00 kB") + + val dataUsageResult = formatter.format(1024, BytesFormatter.UseCase.DataUsage) + assertThat(dataUsageResult).isEqualTo("1.00 kB") + } + + @Test + fun `Large bytes`() { + // Given a very large byte value (e.g., Long.MAX_VALUE), the formatted output should be + // correctly displayed with the largest unit (e.g., 'PB') for both UseCases. + + val fileSizeResult = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.FileSize) + assertThat(fileSizeResult).isEqualTo("9223 PB") + + val dataUsageResult = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.DataUsage) + assertThat(dataUsageResult).isEqualTo("8192 PB") + } + + @Test + fun `Bytes requiring rounding`() { + // Given byte values that require rounding (e.g., 1512), the formatted output should be + // rounded to the appropriate number of decimal places (e.g., '1.51 kB'). + + val fileSizeResult = formatter.format(1512, BytesFormatter.UseCase.FileSize) + assertThat(fileSizeResult).isEqualTo("1.51 kB") + + val dataUsageResult = formatter.format(1512, BytesFormatter.UseCase.DataUsage) + assertThat(dataUsageResult).isEqualTo("1.48 kB") + } + + @Test + fun `FileSize UseCase`() { + // When the UseCase is FileSize, the correct units (byte, KB, kB, GB, TB, PB) should + // be used. + val values = + listOf( + 1L, + 1024L, + 1024L * 1024L, + 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L * 1024L * 1024L, + ) + val expectedUnits = listOf("byte", "kB", "MB", "GB", "TB", "PB", "PB") + + values.zip(expectedUnits).forEach { (value, expectedUnit) -> + val result = formatter.format(value, BytesFormatter.UseCase.FileSize) + assertThat(result).contains(expectedUnit) + } + } + + @Test + fun `DataUsage UseCase`() { + // When the UseCase is DataUsage, the correct units (byte, kB, MB, GB, TB, PB) should + // be used. + val values = + listOf( + 1L, + 1024L, + 1024L * 1024L, + 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L * 1024L, + 1024L * 1024L * 1024L * 1024L * 1024L * 1024L, + ) + val expectedUnits = listOf("byte", "kB", "MB", "GB", "TB", "PB", "PB") + + values.zip(expectedUnits).forEach { (value, expectedUnit) -> + val result = formatter.format(value, BytesFormatter.UseCase.DataUsage) + assertThat(result).contains(expectedUnit) + } + } + + @Test + fun `Fraction digits`() { + // The number of fraction digits in the output should be correctly determined based on + // the rounded byte value. + + assertThat(formatter.format(1500, BytesFormatter.UseCase.FileSize)).isEqualTo("1.50 kB") + assertThat(formatter.format(1050, BytesFormatter.UseCase.FileSize)).isEqualTo("1.05 kB") + assertThat(formatter.format(999, BytesFormatter.UseCase.FileSize)).isEqualTo("1.00 kB") + } + + @Test + fun `Rounding mode`() { + // The rounding mode used for formatting should be ROUND_HALF_UP. + + val result = formatter.format(1006, BytesFormatter.UseCase.FileSize) + + assertThat(result).isEqualTo("1.01 kB") // Ensure rounding mode is effective + } + + @Test + fun `Grouping separator`() { + // Grouping separators should not be used in the formatted output. + + val result = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.FileSize) + + assertThat(result).isEqualTo("9223 PB") + } + + @Test + fun `Format with units`() { + // Verify that the `formatWithUnits` method correctly formats the given bytes with the + // specified units. + + val resultByte = formatter.formatWithUnits(0, BytesFormatter.UseCase.FileSize) + assertThat(resultByte).isEqualTo(BytesFormatter.Result("0", "byte")) + + val resultKb = formatter.formatWithUnits(1000, BytesFormatter.UseCase.FileSize) + assertThat(resultKb).isEqualTo(BytesFormatter.Result("1.00", "kB")) + + val resultMb = formatter.formatWithUnits(479_999_999, BytesFormatter.UseCase.FileSize) + assertThat(resultMb).isEqualTo(BytesFormatter.Result("480", "MB")) + + val resultGb = formatter.formatWithUnits(20_100_000_000, BytesFormatter.UseCase.FileSize) + assertThat(resultGb).isEqualTo(BytesFormatter.Result("20.10", "GB")) + + val resultTb = + formatter.formatWithUnits(300_100_000_000_000, BytesFormatter.UseCase.FileSize) + assertThat(resultTb).isEqualTo(BytesFormatter.Result("300", "TB")) + + val resultPb = + formatter.formatWithUnits(1000_000_000_000_000, BytesFormatter.UseCase.FileSize) + assertThat(resultPb).isEqualTo(BytesFormatter.Result("1.00", "PB")) + } +} 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/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/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/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 1fc1f05ae149..dd28402d705f 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, 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/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index d0e88d5d6a3c..b01f6229af16 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); 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..ef0bc3b100e0 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,13 @@ 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_CONVERT_NETWORK_POLICIES = + "failed_to_convert_network_policies"; + private static final String ERROR_UNKNOWN_BACKUP_SERIALIZATION_VERSION = + "unknown_backup_serialization_version"; // Name of the temporary file we use during full backup/restore. This is @@ -794,29 +802,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 +1185,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 +1211,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 +1344,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 @@ -1384,6 +1438,7 @@ public class SettingsBackupAgent extends BackupAgentHelper { try { out.writeInt(NETWORK_POLICIES_BACKUP_VERSION); out.writeInt(policies.length); + int numberOfPoliciesBackedUp = 0; for (NetworkPolicy policy : policies) { // We purposefully only backup policies that the user has // defined; any inferred policies might include @@ -1393,13 +1448,23 @@ public class SettingsBackupAgent extends BackupAgentHelper { out.writeByte(BackupUtils.NOT_NULL); out.writeInt(marshaledPolicy.length); out.write(marshaledPolicy); + if (areAgentMetricsEnabled) { + numberOfPoliciesBackedUp++; + } } else { out.writeByte(BackupUtils.NULL); } } + if (areAgentMetricsEnabled) { + numberOfSettingsPerKey.put(KEY_NETWORK_POLICIES, numberOfPoliciesBackedUp); + } } catch (IOException ioe) { Log.e(TAG, "Failed to convert NetworkPolicies to byte array " + ioe.getMessage()); baos.reset(); + mBackupRestoreEventLogger.logItemsBackupFailed( + KEY_NETWORK_POLICIES, + policies.length, + ERROR_FAILED_TO_CONVERT_NETWORK_POLICIES); } } return baos.toByteArray(); @@ -1433,6 +1498,10 @@ public class SettingsBackupAgent extends BackupAgentHelper { try { int version = in.readInt(); if (version < 1 || version > NETWORK_POLICIES_BACKUP_VERSION) { + mBackupRestoreEventLogger.logItemsRestoreFailed( + KEY_NETWORK_POLICIES, + /* count= */ 1, + ERROR_UNKNOWN_BACKUP_SERIALIZATION_VERSION); throw new BackupUtils.BadVersionException( "Unknown Backup Serialization Version"); } @@ -1449,10 +1518,15 @@ public class SettingsBackupAgent extends BackupAgentHelper { } // Only set the policies if there was no error in the restore operation networkPolicyManager.setNetworkPolicies(policies); + mBackupRestoreEventLogger.logItemsRestored(KEY_NETWORK_POLICIES, policies.length); } catch (NullPointerException | IOException | BackupUtils.BadVersionException | DateTimeException e) { // NPE can be thrown when trying to instantiate a NetworkPolicy Log.e(TAG, "Failed to convert byte array to NetworkPolicies " + e.getMessage()); + mBackupRestoreEventLogger.logItemsRestoreFailed( + KEY_NETWORK_POLICIES, + /* count= */ 1, + ERROR_FAILED_TO_CONVERT_NETWORK_POLICIES); } } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index dedd7ebd1ef7..5ad4b8a6dffe 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, 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..95dd0db40c0e 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,8 @@ package com.android.providers.settings; +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; @@ -28,6 +30,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; 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 +45,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; @@ -126,6 +131,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 +760,80 @@ 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)); + } + private byte[] generateBackupData(Map<String, String> keyValueData) { int totalBytes = 0; for (String key : keyValueData.keySet()) { @@ -890,6 +970,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 +1031,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 3cdaebc10d27..9adc95a01216 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -85,6 +85,9 @@ filegroup { filegroup { name: "SystemUI-tests-broken-robofiles-run", srcs: [ + "tests/src/**/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt", + "tests/src/**/systemui/power/PowerNotificationWarningsTest.java", + "tests/src/**/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt", "tests/src/**/systemui/dreams/touch/CommunalTouchHandlerTest.java", "tests/src/**/systemui/shade/NotificationShadeWindowViewControllerTest.kt", "tests/src/**/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt", @@ -285,6 +288,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..7d5fd903c01b 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, " 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..5f1f588bb2b5 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 @@ -37,6 +37,7 @@ import androidx.compose.ui.input.pointer.PointerEventPass 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 +53,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 +81,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 @@ -184,8 +190,8 @@ private class NestedDraggableNode( */ private var lastFirstDown: Offset? = null - /** 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)) @@ -256,7 +262,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 +303,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) } @@ -450,20 +459,24 @@ private class NestedDraggableNode( 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() + } } } @@ -491,12 +504,13 @@ private class NestedDraggableNode( if (nestedScrollController == null && 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/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt index 9c49090916e3..7f70e97411f4 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 @@ -33,10 +33,12 @@ 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.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot +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 +655,61 @@ 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 + 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) } + } + private fun ComposeContentTestRule.setContentWithTouchSlop( content: @Composable () -> Unit ): Float { @@ -688,6 +745,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 +754,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/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/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..3f6bce724b1b 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 @@ -54,7 +54,7 @@ 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 } @@ -69,6 +69,10 @@ internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean { * @return The best matching [UserActionResult], or `null` if no match is found. */ internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { + if (!areSwipesAllowed()) { + return null + } + var bestPoints = Int.MIN_VALUE var bestMatch: UserActionResult? = null userActions.forEach { (actionSwipe, actionResult) -> 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/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/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/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/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/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/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/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/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/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..789ca5158dbf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -531,9 +531,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mNotificationPanelViewController = new NotificationPanelViewController( mView, - mMainHandler, - mLayoutInflater, - mFeatureFlags, coordinator, expansionHandler, mDynamicPrivacyController, mKeyguardBypassController, mFalsingManager, new FalsingCollectorFake(), mKeyguardStateController, @@ -553,7 +550,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mKeyguardStatusBarViewComponentFactory, mLockscreenShadeTransitionController, mScrimController, - mUserManager, mMediaDataManager, mNotificationShadeDepthController, mAmbientState, @@ -564,7 +560,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mQsController, mFragmentService, mStatusBarService, - mContentResolver, mShadeHeaderController, mScreenOffAnimationController, mLockscreenGestureLogger, @@ -575,7 +570,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mKeyguardUnlockAnimationController, mKeyguardIndicationController, mNotificationListContainer, - mNotificationStackSizeCalculator, mUnlockedScreenOffAnimationController, systemClock, mKeyguardClockInteractor, @@ -594,7 +588,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/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/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index a62d9d5ce62f..0061c4142e8b 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 @@ -132,7 +132,7 @@ class CallChipViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chip) repo.setOngoingCallState( - inCallModel(startTimeMs = 1000, notificationIcon = mock<StatusBarIconView>()) + inCallModel(startTimeMs = 1000, notificationIcon = createStatusBarIconViewOrNull()) ) assertThat((latest as OngoingActivityChipModel.Shown).icon) @@ -147,11 +147,12 @@ class CallChipViewModelTest : SysuiTestCase() { @Test @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_positiveStartTime_notifIconFlagOn_iconIsNotifIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) - val notifIcon = mock<StatusBarIconView>() + val notifIcon = createStatusBarIconViewOrNull() repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = notifIcon)) assertThat((latest as OngoingActivityChipModel.Shown).icon) @@ -165,6 +166,24 @@ class CallChipViewModelTest : SysuiTestCase() { @Test @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + fun chip_positiveStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + repo.setOngoingCallState( + inCallModel( + startTimeMs = 1000, + notificationIcon = createStatusBarIconViewOrNull(), + notificationKey = "notifKey", + ) + ) + + assertThat((latest as OngoingActivityChipModel.Shown).icon) + .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey")) + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) fun chip_positiveStartTime_notifIconAndConnectedDisplaysFlagOn_iconIsNotifIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -192,7 +211,7 @@ class CallChipViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chip) repo.setOngoingCallState( - inCallModel(startTimeMs = 0, notificationIcon = mock<StatusBarIconView>()) + inCallModel(startTimeMs = 0, notificationIcon = createStatusBarIconViewOrNull()) ) assertThat((latest as OngoingActivityChipModel.Shown).icon) @@ -207,11 +226,12 @@ class CallChipViewModelTest : SysuiTestCase() { @Test @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_zeroStartTime_notifIconFlagOn_iconIsNotifIcon() = + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_zeroStartTime_notifIconFlagOn_cdFlagOff_iconIsNotifIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) - val notifIcon = mock<StatusBarIconView>() + val notifIcon = createStatusBarIconViewOrNull() repo.setOngoingCallState(inCallModel(startTimeMs = 0, notificationIcon = notifIcon)) assertThat((latest as OngoingActivityChipModel.Shown).icon) @@ -224,8 +244,27 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + fun chip_zeroStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + repo.setOngoingCallState( + inCallModel( + startTimeMs = 0, + notificationIcon = createStatusBarIconViewOrNull(), + notificationKey = "notifKey", + ) + ) + + assertThat((latest as OngoingActivityChipModel.Shown).icon) + .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey")) + } + + @Test @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_notifIconFlagOn_butNullNotifIcon_iconIsPhone() = + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOff_iconIsPhone() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -242,6 +281,24 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + fun chip_notifIconFlagOn_butNullNotifIcon_iconNotifKey() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + repo.setOngoingCallState( + inCallModel( + startTimeMs = 1000, + notificationIcon = null, + notificationKey = "notifKey", + ) + ) + + assertThat((latest as OngoingActivityChipModel.Shown).icon) + .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey")) + } + + @Test fun chip_positiveStartTime_colorsAreThemed() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -330,4 +387,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..e561e3ea27d7 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(), ) ) @@ -499,7 +517,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) @@ -531,7 +549,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "clickTest", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = PromotedNotificationContentModel.Builder("clickTest").build(), ) @@ -552,9 +570,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/notification/collection/render/RenderStageManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt index a70d24efada7..912633c874ed 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() { 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..06b1c432955a 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 @@ -40,7 +40,6 @@ import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRe import com.android.systemui.statusbar.notification.collection.render.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 { 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/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/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt index 3d6882c3fdaf..c6bae197ad76 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,7 @@ 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.volume.shared.model.AudioStream import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope @@ -402,115 +402,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/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/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/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/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 8bf4e373a6e0..1f7889214bd5 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -2116,6 +2116,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..a01ff3d5258f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3798,7 +3798,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] --> 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/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/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/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/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 63ac783ad42b..129a6bb72996 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -35,7 +35,6 @@ 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 +56,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 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/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..e255bdea6100 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,14 +27,19 @@ 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 com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking @@ -66,6 +71,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, @@ -102,15 +112,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( @@ -136,6 +154,16 @@ class InputGestureMaps @Inject constructor(private val context: Context) { 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/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/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..fbe31bbf36e6 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 @@ -418,8 +418,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 +483,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/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/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/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 e5ff2529e335..7ec523bc4dc5 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt @@ -19,6 +19,7 @@ import android.content.Context import android.hardware.display.DisplayManager import android.os.Bundle import android.os.UserHandle +import android.view.View import androidx.annotation.StyleRes import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger @@ -103,7 +104,6 @@ class ScreenRecordPermissionDialogDelegate( mediaProjectionMetricsLogger, defaultSelectedMode, displayManager, - dialog, controller, activityStarter, userContextProvider, @@ -119,6 +119,12 @@ class ScreenRecordPermissionDialogDelegate( super<BaseMediaProjectionPermissionDialogDelegate>.onCreate(dialog, savedInstanceState) setDialogTitle(R.string.screenrecord_permission_dialog_title) dialog.setTitle(R.string.screenrecord_title) + setStartButtonOnClickListener { v: View? -> + val screenRecordViewBinder: ScreenRecordPermissionViewBinder? = + viewBinder as ScreenRecordPermissionViewBinder? + screenRecordViewBinder?.startButtonOnClicked() + dialog.dismiss() + } setCancelButtonOnClickListener { dialog.dismiss() } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt index 9f7e1ade964a..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,56 +67,57 @@ 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 { _: View? -> - onStartRecordingClicked?.run() - if (selectedScreenShareOption.mode == ENTIRE_SCREEN) { - requestScreenCapture( - captureTarget = null, - displayId = selectedScreenShareOption.displayId, - ) - } - if (selectedScreenShareOption.mode == SINGLE_APP) { - val intent = Intent(dialog.context, MediaProjectionAppSelectorActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + setStartButtonOnClickListener { startButtonOnClicked() } + } - // We can't start activity for result here so we use result receiver to get - // the selected target to capture - intent.putExtra( - MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER, - CaptureTargetResultReceiver(), - ) + fun startButtonOnClicked() { + onStartRecordingClicked?.run() + if (selectedScreenShareOption.mode == ENTIRE_SCREEN) { + requestScreenCapture( + captureTarget = null, + displayId = selectedScreenShareOption.displayId, + ) + } + if (selectedScreenShareOption.mode == SINGLE_APP) { + val intent = + Intent(containerView.context, MediaProjectionAppSelectorActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - intent.putExtra( - MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE, - hostUserHandle, - ) - intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, hostUid) - intent.putExtra( - MediaProjectionAppSelectorActivity.EXTRA_SCREEN_SHARE_TYPE, - MediaProjectionAppSelectorActivity.ScreenShareType.ScreenRecord.name, - ) - activityStarter.startActivity(intent, /* dismissShade= */ true) - } - dialog.dismiss() + // We can't start activity for result here so we use result receiver to get + // the selected target to capture + intent.putExtra( + MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER, + CaptureTargetResultReceiver(), + ) + + intent.putExtra( + MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE, + hostUserHandle, + ) + intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, hostUid) + intent.putExtra( + MediaProjectionAppSelectorActivity.EXTRA_SCREEN_SHARE_TYPE, + MediaProjectionAppSelectorActivity.ScreenShareType.ScreenRecord.name, + ) + activityStarter.startActivity(intent, /* dismissShade= */ true) } } @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 @@ -126,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/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index e168025b2bf8..19152170757c 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,14 +99,11 @@ 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; @@ -162,7 +153,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 +164,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 +242,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 +273,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 +297,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 +306,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 +420,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 +445,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 +457,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 +511,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 +529,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 +560,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 +570,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump QuickSettingsControllerImpl quickSettingsController, FragmentService fragmentService, IStatusBarService statusBarService, - ContentResolver contentResolver, ShadeHeaderController shadeHeaderController, ScreenOffAnimationController screenOffAnimationController, LockscreenGestureLogger lockscreenGestureLogger, @@ -606,7 +580,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump KeyguardUnlockAnimationController keyguardUnlockAnimationController, KeyguardIndicationController keyguardIndicationController, NotificationListContainer notificationListContainer, - NotificationStackSizeCalculator notificationStackSizeCalculator, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, SystemClock systemClock, KeyguardClockInteractor keyguardClockInteractor, @@ -625,7 +598,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump SplitShadeStateController splitShadeStateController, PowerInteractor powerInteractor, KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm, - NaturalScrollingSettingObserver naturalScrollingSettingObserver, MSDLPlayer msdlPlayer, BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor) { SceneContainerFlag.assertInLegacyMode(); @@ -651,7 +623,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 +662,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 +678,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 +690,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 +702,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mPulseExpansionHandler = pulseExpansionHandler; mDozeParameters = dozeParameters; mScrimController = scrimController; - mUserManager = userManager; mMediaDataManager = mediaDataManager; mTapAgainViewController = tapAgainViewController; mSysUiState = sysUiState; @@ -889,7 +854,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Dreaming->Lockscreen collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(), - setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), + setDreamLockscreenTransitionAlpha(), mMainDispatcher); collectFlow(mView, mKeyguardTransitionInteractor.transition( @@ -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/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/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/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/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/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/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index 32de65be5b5b..d4d3cdf42fb1 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 @@ -27,7 +27,6 @@ import com.android.systemui.statusbar.notification.collection.render.NotifStackC import com.android.systemui.statusbar.notification.collection.render.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 +42,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 +51,11 @@ internal constructor( groupExpansionManagerImpl.attach(pipeline) } + // TODO: b/293167744 - Remove controller param. private fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) = traceSection("StackCoordinator.onAfterRenderList") { val notifStats = calculateNotifStats(entries) - if (FooterViewRefactor.isEnabled) { - activeNotificationsInteractor.setNotifStats(notifStats) - } else { - controller.setNotifStats(notifStats) - } + activeNotificationsInteractor.setNotifStats(notifStats) renderListInteractor.setRenderedList(entries) } 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/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..b892bebb3120 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,11 +29,8 @@ 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; @@ -64,14 +61,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 +85,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; @@ -115,14 +103,15 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Di import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; 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.DefaultNotifStackController; 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,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final NotificationListContainerImpl mNotificationListContainer = new NotificationListContainerImpl(); + // TODO: b/293167744 - Remove this. private final NotifStackController mNotifStackController = - new NotifStackControllerImpl(); - - @Nullable - private NotificationActivityStarter mNotificationActivityStarter; + new DefaultNotifStackController(); @VisibleForTesting final View.OnAttachStateChangeListener mOnAttachStateChangeListener = @@ -248,9 +222,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 +235,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 +255,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 +264,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 +287,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.updateBgColor(); mView.updateDecorViews(); mView.reinflateViews(); - if (!FooterViewRefactor.isEnabled()) { - updateShowEmptyShadeView(); - updateFooter(); - } } @Override @@ -363,7 +295,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 +332,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 +343,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { public void onUserChanged(int userId) { updateSensitivenessWithAnimation(false); mHistoryEnabled = null; - if (!FooterViewRefactor.isEnabled()) { - updateFooter(); - } } }; @@ -656,7 +574,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { == null) { mHeadsUpManager.removeNotification( row.getEntry().getSbn().getKey(), - /* removeImmediately= */ true , + /* removeImmediately= */ true, /* reason= */ "onChildSnappedBack" ); } @@ -714,14 +632,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 +644,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 +658,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 +678,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { SensitiveNotificationProtectionController sensitiveNotificationProtectionController, WallpaperInteractor wallpaperInteractor) { mView = view; - mKeyguardTransitionRepo = keyguardTransitionRepo; mViewBinder = viewBinder; mStackStateLogger = stackLogger; mLogger = logger; @@ -795,15 +697,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 +714,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 +746,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 +774,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 +791,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 +812,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 +821,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 +1028,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 +1139,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { public void setQsFullScreen(boolean fullScreen) { mView.setQsFullScreen(fullScreen); - if (!FooterViewRefactor.isEnabled()) { - updateShowEmptyShadeView(); - } } public void setScrollingEnabled(boolean enabled) { @@ -1464,64 +1316,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 +1377,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 +1409,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 +1431,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 +1457,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; } @@ -1772,13 +1526,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 +1627,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.animateNextTopPaddingChange(); } - public void setNotificationActivityStarter(NotificationActivityStarter activityStarter) { - mNotificationActivityStarter = activityStarter; - } - public NotificationTargetsHelper getNotificationTargetsHelper() { return mNotificationTargetsHelper; } @@ -1898,18 +1641,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 +2019,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/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..0b2b84e60f4b 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() - } } } } 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/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 1474789ea0e3..3d6cd7e49dfe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1487,8 +1487,6 @@ 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( 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/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 7e06c35315f9..0dd7c8499861 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,6 +41,7 @@ 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 @@ -115,7 +116,11 @@ constructor( } } - if (Flags.statusBarScreenSharingChips() && !StatusBarNotifChips.isEnabled) { + if ( + Flags.statusBarScreenSharingChips() && + !StatusBarNotifChips.isEnabled && + !StatusBarChipsModernization.isEnabled + ) { val primaryChipView: View = view.requireViewById(R.id.ongoing_activity_chip_primary) launch { @@ -157,7 +162,11 @@ constructor( } } - if (Flags.statusBarScreenSharingChips() && StatusBarNotifChips.isEnabled) { + if ( + Flags.statusBarScreenSharingChips() && + StatusBarNotifChips.isEnabled && + !StatusBarChipsModernization.isEnabled + ) { val primaryChipView: View = view.requireViewById(R.id.ongoing_activity_chip_primary) val secondaryChipView: View = 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..f286a1a148fa 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 @@ -164,11 +164,19 @@ fun StatusBarRoot( 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/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/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/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/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/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/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/multivalentTests/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/multivalentTests/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/multivalentTests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java index 2aa300df4f7c..2aa300df4f7c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerNotificationWarningsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java 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/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt index 2c37f510a45c..77bac59b9dcd 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 @@ -33,15 +32,14 @@ import com.android.systemui.statusbar.notification.collection.render.NotifStackC import com.android.systemui.statusbar.notification.collection.render.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 @@ -82,9 +80,9 @@ 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 @@ -94,93 +92,9 @@ class StackCoordinatorTest : SysuiTestCase() { } @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) - .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) - .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) - .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( @@ -195,12 +109,8 @@ class StackCoordinatorTest : SysuiTestCase() { } @Test - @EnableFlags( - FooterViewRefactor.FLAG_NAME, - FLAG_SCREENSHARE_NOTIFICATION_HIDING, - FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, - ) - fun testSetNotificationStats_footerFlagOn_isSensitiveStateActive_nonClearableAlerting() { + @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) @@ -218,8 +128,7 @@ class StackCoordinatorTest : SysuiTestCase() { } @Test - @EnableFlags(FooterViewRefactor.FLAG_NAME) - fun testSetNotificationStats_footerFlagOn_clearableSilent() { + fun testSetNotificationStats_clearableSilent() { whenever(section.bucket).thenReturn(BUCKET_SILENT) afterRenderListListener.onAfterRenderList(listOf(entry), stackController) verify(activeNotificationsInteractor) @@ -236,12 +145,8 @@ class StackCoordinatorTest : SysuiTestCase() { } @Test - @EnableFlags( - FooterViewRefactor.FLAG_NAME, - FLAG_SCREENSHARE_NOTIFICATION_HIDING, - FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX, - ) - fun testSetNotificationStats_footerFlagOn_isSensitiveStateActive_nonClearableSilent() { + @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) @@ -259,8 +164,7 @@ class StackCoordinatorTest : SysuiTestCase() { } @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) 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..57a12df0cfee 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 @@ -38,6 +38,7 @@ 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 @@ -123,7 +124,8 @@ class IconManagerTest : SysuiTestCase() { @Test @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun testCreateIcons_chipNotifIconFlagEnabled_statusBarChipIconIsNull() { + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagDisabled_statusBarChipIconIsNotNull() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) entry?.let { iconManager.createIcons(it) } @@ -133,6 +135,17 @@ class IconManagerTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagEnabled_statusBarChipIconIsNull() { + val entry = + notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) + entry?.let { iconManager.createIcons(it) } + testScope.runCurrent() + + assertThat(entry?.icons?.statusBarChipIcon).isNull() + } + + @Test fun testCreateIcons_importantConversation_shortcutIcon() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) @@ -158,7 +171,7 @@ class IconManagerTest : SysuiTestCase() { notificationEntry( hasShortcut = false, hasMessageSenderIcon = false, - hasLargeIcon = true + hasLargeIcon = true, ) entry?.channel?.isImportantConversation = true entry?.let { iconManager.createIcons(it) } @@ -172,7 +185,7 @@ class IconManagerTest : SysuiTestCase() { notificationEntry( hasShortcut = false, hasMessageSenderIcon = false, - hasLargeIcon = false + hasLargeIcon = false, ) entry?.channel?.isImportantConversation = true entry?.let { iconManager.createIcons(it) } @@ -187,7 +200,7 @@ class IconManagerTest : SysuiTestCase() { hasShortcut = true, hasMessageSenderIcon = true, useMessagingStyle = false, - hasLargeIcon = true + hasLargeIcon = true, ) entry?.channel?.isImportantConversation = true entry?.let { iconManager.createIcons(it) } @@ -205,7 +218,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 +233,24 @@ class IconManagerTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, 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 @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun testUpdateIcons_sensitiveImportantConversation() { + @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun testUpdateIcons_cdFlagDisabled_sensitiveImportantConversation() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) entry?.setSensitive(true, true) @@ -236,6 +266,23 @@ class IconManagerTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, 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 +301,7 @@ class IconManagerTest : SysuiTestCase() { hasShortcut: Boolean, hasMessageSenderIcon: Boolean, useMessagingStyle: Boolean = true, - hasLargeIcon: Boolean + hasLargeIcon: Boolean, ): NotificationEntry? { val n = Notification.Builder(mContext, "id") @@ -270,7 +317,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/multivalentTests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt index 59ad38a87146..59ad38a87146 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt 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/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/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/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index 1881a94c8984..afe48214832f 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) { + testScope.runTestWithSnapshots testBody@{ this@runTest.testBody() } } fun Kosmos.runCurrent() = testScope.runCurrent() 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/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/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/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 37d045bf6422..6cd1f721d215 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() { @@ -569,6 +570,12 @@ 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 = new HearingDevicePhoneCallNotificationController( + context); + } 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/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..aeb2f5e9be84 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); 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..31f6ef9fc062 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -1708,7 +1708,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/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/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/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/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 9f785ac81398..24296406da00 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, 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..b2c35e1f362e 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 { @@ -3575,8 +3575,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 +3716,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 +3734,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 +3754,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/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/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 34bb4155c943..86fc732e9d04 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -18,6 +18,7 @@ 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.media.quality.AmbientBacklightSettings; @@ -35,8 +36,13 @@ import android.media.quality.SoundProfileHandle; import android.os.Binder; import android.os.Bundle; import android.os.PersistableBundle; +import android.os.RemoteCallbackList; +import android.os.RemoteException; 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; @@ -45,9 +51,11 @@ 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 +72,13 @@ 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<>(); public MediaQualityService(Context context) { super(context); mContext = context; + mPackageManager = mContext.getPackageManager(); mPictureProfileTempIdMap = new BiMap<>(); mSoundProfileTempIdMap = new BiMap<>(); mMediaQualityDbHelper = new MediaQualityDbHelper(mContext); @@ -85,12 +96,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 +123,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 +141,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 +203,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 +248,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 +294,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 +320,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); + + if (!hasPermissionToUpdateSoundProfile(dbId, sp)) { + //TODO: error handling + return; + } - ContentValues values = getContentValues(intId, + ContentValues values = getContentValues(dbId, sp.getProfileType(), sp.getName(), sp.getPackageName(), @@ -242,27 +338,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 +393,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 getSoundProfileWithTempIdFromCursor(cursor); + 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 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 +443,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 +476,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 +614,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 +626,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 +686,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 +701,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 +769,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,6 +806,9 @@ public class MediaQualityService extends SystemService { @Override public void setAutoPictureQualityEnabled(boolean enabled, UserHandle user) { + if (!hasGlobalPictureQualityServicePermission()) { + //TODO: error handling + } } @Override @@ -583,6 +818,9 @@ public class MediaQualityService extends SystemService { @Override public void setSuperResolutionEnabled(boolean enabled, UserHandle user) { + if (!hasGlobalPictureQualityServicePermission()) { + //TODO: error handling + } } @Override @@ -592,6 +830,9 @@ public class MediaQualityService extends SystemService { @Override public void setAutoSoundQualityEnabled(boolean enabled, UserHandle user) { + if (!hasGlobalSoundQualityServicePermission()) { + //TODO: error handling + } } @Override @@ -604,4 +845,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/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index f50e8aa7eb7b..9567c818fa18 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -5903,8 +5903,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 +5913,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 +6068,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 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..2b4d71e85dc0 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -196,4 +196,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/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/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/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/resources/ResourcesManagerShellCommand.java b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java index a75d110e3cd1..17739712d65a 100644 --- a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java +++ b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java @@ -88,6 +88,5 @@ public class ResourcesManagerShellCommand extends ShellCommand { out.println(" Print this help text."); out.println(" dump <PROCESS>"); out.println(" Dump the Resources objects in use as well as the history of Resources"); - } } 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/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 093df8c5075c..29f1f93a844f 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3209,7 +3209,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 +3240,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() { @@ -8435,8 +8435,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 +8444,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; } @@ -10221,7 +10220,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mAppCompatController.getAppCompatOrientationOverrides() .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..0967078deac3 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 @@ -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, + mAppCompatReachabilityPolicy = 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 @@ -138,8 +104,8 @@ class AppCompatController { } @NonNull - AppCompatResizeOverrides getAppCompatResizeOverrides() { - return mAppCompatOverrides.getAppCompatResizeOverrides(); + AppCompatResizeOverrides getResizeOverrides() { + return mAppCompatOverrides.getResizeOverrides(); } @NonNull diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java index 2f03105846bd..58b37becc373 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; @@ -34,13 +35,14 @@ public class AppCompatOverrides { @NonNull private final AppCompatFocusOverrides mAppCompatFocusOverrides; @NonNull - private final AppCompatResizeOverrides mAppCompatResizeOverrides; + private final AppCompatResizeOverrides mResizeOverrides; @NonNull private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides; @NonNull private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides; AppCompatOverrides(@NonNull ActivityRecord activityRecord, + @NonNull PackageManager packageManager, @NonNull AppCompatConfiguration appCompatConfiguration, @NonNull OptPropFactory optPropBuilder, @NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery) { @@ -55,7 +57,8 @@ public class AppCompatOverrides { mAppCompatReachabilityOverrides); mAppCompatFocusOverrides = new AppCompatFocusOverrides(activityRecord, appCompatConfiguration, optPropBuilder); - mAppCompatResizeOverrides = new AppCompatResizeOverrides(activityRecord, optPropBuilder); + mResizeOverrides = new AppCompatResizeOverrides(activityRecord, packageManager, + optPropBuilder); mAppCompatLetterboxOverrides = new AppCompatLetterboxOverrides(activityRecord, appCompatConfiguration); } @@ -81,8 +84,8 @@ public class AppCompatOverrides { } @NonNull - AppCompatResizeOverrides getAppCompatResizeOverrides() { - return mAppCompatResizeOverrides; + AppCompatResizeOverrides getResizeOverrides() { + return mResizeOverrides; } @NonNull 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/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/Transition.java b/services/core/java/com/android/server/wm/Transition.java index f0faa8e4691f..a9646783b92d 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1412,6 +1412,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/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..e69a7414dd76 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -21907,7 +21907,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/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..0210791cfeda --- /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: "print" + description: "Do not use the flag Context.BIND_INCLUDE_CAPABILITIES when binding to the service" + bug: "291281543" +} 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/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/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/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/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..0d86d4c3fa28 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 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 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 testPublishWithNoActivity() { // 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 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 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 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 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 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/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/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 301165f8151d..7885c9b902e2 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -11213,7 +11213,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 +11225,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}. */ 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/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/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/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java index b6e393d7be0c..03d904283e83 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java @@ -342,8 +342,8 @@ public class AppTransitionTests extends WindowTestsBase { public void testCancelRemoteAnimationWhenFreeze() { final DisplayContent dc = createNewDisplay(Display.STATE_ON); doReturn(false).when(dc).onDescendantOrientationChanged(any()); - final WindowState exitingAppWindow = createWindow(null /* parent */, TYPE_BASE_APPLICATION, - dc, "exiting app"); + final WindowState exitingAppWindow = newWindowBuilder("exiting app", + TYPE_BASE_APPLICATION).setDisplay(dc).build(); final ActivityRecord exitingActivity = exitingAppWindow.mActivityRecord; // Wait until everything in animation handler get executed to prevent the exiting window // from being removed during WindowSurfacePlacer Traversal. diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java index 14276ae21899..7033d79d0ee1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java @@ -266,10 +266,10 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { mSetFlagsRule.enableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH); prepareSecondaryDisplay(); - final WindowState defaultDisplayWindow = createWindow(/* parent= */ null, - TYPE_BASE_APPLICATION, mDisplayContent, "DefaultDisplayWindow"); - final WindowState secondaryDisplayWindow = createWindow(/* parent= */ null, - TYPE_BASE_APPLICATION, mSecondaryDisplayContent, "SecondaryDisplayWindow"); + final WindowState defaultDisplayWindow = newWindowBuilder("DefaultDisplayWindow", + TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build(); + final WindowState secondaryDisplayWindow = newWindowBuilder("SecondaryDisplayWindow", + TYPE_BASE_APPLICATION).setDisplay(mSecondaryDisplayContent).build(); makeWindowVisibleAndNotDrawn(defaultDisplayWindow, secondaryDisplayWindow); // Mark as display switching only for the default display as we filter out 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/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java index bd15bc42e811..347d1bc1becc 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java @@ -379,13 +379,11 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay); assertThat(mDisplay.findAreaForTokenInLayer(imeToken)).isEqualTo(imeContainer); - final WindowState firstActivityWin = - createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity, - "firstActivityWin"); + final WindowState firstActivityWin = newWindowBuilder("firstActivityWin", + TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build(); spyOn(firstActivityWin); - final WindowState secondActivityWin = - createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mSecondActivity, - "firstActivityWin"); + final WindowState secondActivityWin = newWindowBuilder("secondActivityWin", + TYPE_APPLICATION_STARTING).setWindowToken(mSecondActivity).build(); spyOn(secondActivityWin); // firstActivityWin should be the target @@ -424,13 +422,11 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { setupImeWindow(); final DisplayArea.Tokens imeContainer = mDisplay.getImeContainer(); final WindowToken imeToken = tokenOfType(TYPE_INPUT_METHOD); - final WindowState firstActivityWin = - createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity, - "firstActivityWin"); + final WindowState firstActivityWin = newWindowBuilder("firstActivityWin", + TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build(); spyOn(firstActivityWin); - final WindowState secondActivityWin = - createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mSecondActivity, - "secondActivityWin"); + final WindowState secondActivityWin = newWindowBuilder("secondActivityWin", + TYPE_APPLICATION_STARTING).setWindowToken(mSecondActivity).build(); spyOn(secondActivityWin); // firstActivityWin should be the target @@ -464,9 +460,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay); assertThat(mDisplay.findAreaForTokenInLayer(imeToken)).isEqualTo(imeContainer); - final WindowState firstActivityWin = - createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity, - "firstActivityWin"); + final WindowState firstActivityWin = newWindowBuilder("firstActivityWin", + TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build(); spyOn(firstActivityWin); // firstActivityWin should be the target doReturn(true).when(firstActivityWin).canBeImeTarget(); @@ -499,9 +494,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay); // firstActivityWin should be the target - final WindowState firstActivityWin = - createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity, - "firstActivityWin"); + final WindowState firstActivityWin = newWindowBuilder("firstActivityWin", + TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build(); spyOn(firstActivityWin); doReturn(true).when(firstActivityWin).canBeImeTarget(); WindowState imeTarget = mDisplay.computeImeTarget(true /* updateImeTarget */); @@ -560,8 +554,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { } private void setupImeWindow() { - final WindowState imeWindow = createWindow(null /* parent */, - TYPE_INPUT_METHOD, mDisplay, "mImeWindow"); + final WindowState imeWindow = newWindowBuilder("mImeWindow", TYPE_INPUT_METHOD).setDisplay( + mDisplay).build(); imeWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE; mDisplay.mInputMethodWindow = imeWindow; } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index dc4adcc4315b..299717393028 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -878,8 +878,10 @@ public class TaskFragmentTest extends WindowTestsBase { .build(); final ActivityRecord activity0 = tf0.getTopMostActivity(); final ActivityRecord activity1 = tf1.getTopMostActivity(); - final WindowState win0 = createWindow(null, TYPE_BASE_APPLICATION, activity0, "win0"); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity1, "win1"); + final WindowState win0 = newWindowBuilder("win0", TYPE_BASE_APPLICATION).setWindowToken( + activity0).build(); + final WindowState win1 = newWindowBuilder("win1", TYPE_BASE_APPLICATION).setWindowToken( + activity1).build(); doReturn(false).when(mDisplayContent).shouldImeAttachedToApp(); mDisplayContent.setImeInputTarget(win0); @@ -1174,8 +1176,8 @@ public class TaskFragmentTest extends WindowTestsBase { } private WindowState createAppWindow(ActivityRecord app, String name) { - final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, app, name, - 0 /* ownerId */, false /* ownerCanAddInternalSystemWindow */, new TestIWindow()); + final WindowState win = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken( + app).setClientWindow(new TestIWindow()).build(); mWm.mWindowMap.put(win.mClient.asBinder(), win); return win; } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java index f145b40d2292..f9250f9ecf5d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java @@ -63,7 +63,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase { @Test public void testAppRemoved() { - final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window"); + final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build(); mCache.putSnapshot(window.getTask(), createSnapshot()); assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */)); mCache.onAppRemoved(window.mActivityRecord); @@ -72,7 +72,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase { @Test public void testAppDied() { - final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window"); + final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build(); mCache.putSnapshot(window.getTask(), createSnapshot()); assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */)); mCache.onAppDied(window.mActivityRecord); @@ -81,7 +81,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase { @Test public void testTaskRemoved() { - final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window"); + final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build(); mCache.putSnapshot(window.getTask(), createSnapshot()); assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */)); mCache.onIdRemoved(window.getTask().mTaskId); @@ -90,7 +90,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase { @Test public void testReduced_notCached() { - final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window"); + final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build(); mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot()); mSnapshotPersistQueue.waitForQueueEmpty(); assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */)); @@ -105,7 +105,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase { @Test public void testRestoreFromDisk() { - final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window"); + final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build(); mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot()); mSnapshotPersistQueue.waitForQueueEmpty(); assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */)); @@ -117,7 +117,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase { @Test public void testClearCache() { - final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window"); + final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build(); mCache.putSnapshot(window.getTask(), mSnapshot); assertEquals(mSnapshot, mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */)); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java index c6b2a6b8d42f..1bca53aff034 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java @@ -74,8 +74,8 @@ public class TaskSnapshotControllerTest extends WindowTestsBase { @Test public void testGetClosingApps_closing() { - final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW, - "closingWindow"); + final WindowState closingWindow = newWindowBuilder("closingWindow", + FIRST_APPLICATION_WINDOW).build(); closingWindow.mActivityRecord.commitVisibility( false /* visible */, true /* performLayout */); final ArraySet<ActivityRecord> closingApps = new ArraySet<>(); @@ -88,8 +88,8 @@ public class TaskSnapshotControllerTest extends WindowTestsBase { @Test public void testGetClosingApps_notClosing() { - final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW, - "closingWindow"); + final WindowState closingWindow = newWindowBuilder("closingWindow", + FIRST_APPLICATION_WINDOW).build(); final WindowState openingWindow = createAppWindow(closingWindow.getTask(), FIRST_APPLICATION_WINDOW, "openingWindow"); closingWindow.mActivityRecord.commitVisibility( @@ -105,8 +105,8 @@ public class TaskSnapshotControllerTest extends WindowTestsBase { @Test public void testGetClosingApps_skipClosingAppsSnapshotTasks() { - final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW, - "closingWindow"); + final WindowState closingWindow = newWindowBuilder("closingWindow", + FIRST_APPLICATION_WINDOW).build(); closingWindow.mActivityRecord.commitVisibility( false /* visible */, true /* performLayout */); final ArraySet<ActivityRecord> closingApps = new ArraySet<>(); @@ -133,19 +133,19 @@ public class TaskSnapshotControllerTest extends WindowTestsBase { @Test public void testGetSnapshotMode() { - final WindowState disabledWindow = createWindow(null, - FIRST_APPLICATION_WINDOW, mDisplayContent, "disabledWindow"); + final WindowState disabledWindow = newWindowBuilder("disabledWindow", + FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build(); disabledWindow.mActivityRecord.setRecentsScreenshotEnabled(false); assertEquals(SNAPSHOT_MODE_APP_THEME, mWm.mTaskSnapshotController.getSnapshotMode(disabledWindow.getTask())); - final WindowState normalWindow = createWindow(null, - FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow"); + final WindowState normalWindow = newWindowBuilder("normalWindow", + FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build(); assertEquals(SNAPSHOT_MODE_REAL, mWm.mTaskSnapshotController.getSnapshotMode(normalWindow.getTask())); - final WindowState secureWindow = createWindow(null, - FIRST_APPLICATION_WINDOW, mDisplayContent, "secureWindow"); + final WindowState secureWindow = newWindowBuilder("secureWindow", + FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build(); secureWindow.mAttrs.flags |= FLAG_SECURE; assertEquals(SNAPSHOT_MODE_APP_THEME, mWm.mTaskSnapshotController.getSnapshotMode(secureWindow.getTask())); @@ -297,8 +297,8 @@ public class TaskSnapshotControllerTest extends WindowTestsBase { spyOn(mWm.mTaskSnapshotController); doReturn(false).when(mWm.mTaskSnapshotController).shouldDisableSnapshots(); - final WindowState normalWindow = createWindow(null, - FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow"); + final WindowState normalWindow = newWindowBuilder("normalWindow", + FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build(); final TaskSnapshot snapshot = new TaskSnapshotPersisterTestBase.TaskSnapshotBuilder() .setTopActivityComponent(normalWindow.mActivityRecord.mActivityComponent).build(); doReturn(snapshot).when(mWm.mTaskSnapshotController).snapshot(any()); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java index 9bde0663d4a3..51ea498811fc 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java @@ -41,7 +41,7 @@ import java.io.File; * Test class for {@link TaskSnapshotPersister} and {@link AppSnapshotLoader} * * Build/Install/Run: - * atest TaskSnapshotPersisterLoaderTest + * atest TaskSnapshotLowResDisabledTest */ @MediumTest @Presubmit @@ -126,7 +126,7 @@ public class TaskSnapshotLowResDisabledTest extends TaskSnapshotPersisterTestBas @Test public void testReduced_notCached() { - final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window"); + final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build(); mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot()); mSnapshotPersistQueue.waitForQueueEmpty(); assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */)); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java index 1fa657822189..5ed2df30518b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -504,8 +504,8 @@ public class WindowContainerTests extends WindowTestsBase { assertTrue(child.isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION)); assertFalse(child.isAnimating(PARENTS, ANIMATION_TYPE_SCREEN_ROTATION)); - final WindowState windowState = createWindow(null /* parent */, TYPE_BASE_APPLICATION, - mDisplayContent, "TestWindowState"); + final WindowState windowState = newWindowBuilder("TestWindowState", + TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build(); WindowContainer parent = windowState.getParent(); spyOn(windowState.mSurfaceAnimator); doReturn(true).when(windowState.mSurfaceAnimator).isAnimating(); @@ -1045,8 +1045,8 @@ public class WindowContainerTests extends WindowTestsBase { // An animating window with mRemoveOnExit can be removed by handleCompleteDeferredRemoval // once it no longer animates. - final WindowState exitingWindow = createWindow(null, TYPE_APPLICATION_OVERLAY, - displayContent, "exiting window"); + final WindowState exitingWindow = newWindowBuilder("exiting window", + TYPE_APPLICATION_OVERLAY).setDisplay(displayContent).build(); exitingWindow.startAnimation(exitingWindow.getPendingTransaction(), mock(AnimationAdapter.class), false /* hidden */, SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION); @@ -1063,7 +1063,7 @@ public class WindowContainerTests extends WindowTestsBase { final ActivityRecord r = new TaskBuilder(mSupervisor).setCreateActivity(true) .setDisplay(displayContent).build().getTopMostActivity(); // Add a window and make the activity animating so the removal of activity is deferred. - createWindow(null, TYPE_BASE_APPLICATION, r, "win"); + newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken(r).build(); doReturn(true).when(r).isAnimating(anyInt(), anyInt()); displayContent.remove(); @@ -1216,7 +1216,8 @@ public class WindowContainerTests extends WindowTestsBase { public void testFreezeInsets() { final Task task = createTask(mDisplayContent); final ActivityRecord activity = createActivityRecord(mDisplayContent, task); - final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win"); + final WindowState win = newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken( + activity).build(); // Set visibility to false, verify the main window of the task will be set the frozen // insets state immediately. @@ -1233,7 +1234,8 @@ public class WindowContainerTests extends WindowTestsBase { final Task rootTask = createTask(mDisplayContent); final Task task = createTaskInRootTask(rootTask, 0 /* userId */); final ActivityRecord activity = createActivityRecord(mDisplayContent, task); - final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win"); + final WindowState win = newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken( + activity).build(); task.getDisplayContent().prepareAppTransition(TRANSIT_CLOSE); spyOn(win); doReturn(true).when(task).okToAnimate(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java index 72935cb546d9..8606581539ff 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java @@ -49,9 +49,10 @@ public class WindowContainerTraversalTests extends WindowTestsBase { @SetupWindows(addWindows = { W_DOCK_DIVIDER, W_INPUT_METHOD }) @Test public void testDockedDividerPosition() { - final WindowState splitScreenWindow = createWindow(null, - WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION, - mDisplayContent, "splitScreenWindow"); + final WindowState splitScreenWindow = newWindowBuilder("splitScreenWindow", + TYPE_BASE_APPLICATION).setWindowingMode( + WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay( + mDisplayContent).build(); mDisplayContent.setImeLayeringTarget(splitScreenWindow); 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 50e0e181cd68..ab9abfc4a876 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -154,9 +154,11 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testIsParentWindowHidden() { - final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow"); - final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1"); - final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2"); + final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build(); + final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent( + parentWindow).build(); + final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent( + parentWindow).build(); // parentWindow is initially set to hidden. assertTrue(parentWindow.mHidden); @@ -172,10 +174,12 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testIsChildWindow() { - final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow"); - final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1"); - final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2"); - final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow"); + final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build(); + final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent( + parentWindow).build(); + final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent( + parentWindow).build(); + final WindowState randomWindow = newWindowBuilder("randomWindow", TYPE_APPLICATION).build(); assertFalse(parentWindow.isChildWindow()); assertTrue(child1.isChildWindow()); @@ -185,12 +189,15 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testHasChild() { - final WindowState win1 = createWindow(null, TYPE_APPLICATION, "win1"); - final WindowState win11 = createWindow(win1, FIRST_SUB_WINDOW, "win11"); - final WindowState win12 = createWindow(win1, FIRST_SUB_WINDOW, "win12"); - final WindowState win2 = createWindow(null, TYPE_APPLICATION, "win2"); - final WindowState win21 = createWindow(win2, FIRST_SUB_WINDOW, "win21"); - final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow"); + final WindowState win1 = newWindowBuilder("win1", TYPE_APPLICATION).build(); + final WindowState win11 = newWindowBuilder("win11", FIRST_SUB_WINDOW).setParent( + win1).build(); + final WindowState win12 = newWindowBuilder("win12", FIRST_SUB_WINDOW).setParent( + win1).build(); + final WindowState win2 = newWindowBuilder("win2", TYPE_APPLICATION).build(); + final WindowState win21 = newWindowBuilder("win21", FIRST_SUB_WINDOW).setParent( + win2).build(); + final WindowState randomWindow = newWindowBuilder("randomWindow", TYPE_APPLICATION).build(); assertTrue(win1.hasChild(win11)); assertTrue(win1.hasChild(win12)); @@ -206,9 +213,11 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testGetParentWindow() { - final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow"); - final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1"); - final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2"); + final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build(); + final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent( + parentWindow).build(); + final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent( + parentWindow).build(); assertNull(parentWindow.getParentWindow()); assertEquals(parentWindow, child1.getParentWindow()); @@ -217,8 +226,8 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testOverlayWindowHiddenWhenSuspended() { - final WindowState overlayWindow = spy(createWindow(null, TYPE_APPLICATION_OVERLAY, - "overlayWindow")); + final WindowState overlayWindow = spy( + newWindowBuilder("overlayWindow", TYPE_APPLICATION_OVERLAY).build()); overlayWindow.setHiddenWhileSuspended(true); verify(overlayWindow).hide(true /* doAnimation */, true /* requestAnim */); overlayWindow.setHiddenWhileSuspended(false); @@ -227,9 +236,11 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testGetTopParentWindow() { - final WindowState root = createWindow(null, TYPE_APPLICATION, "root"); - final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1"); - final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2"); + final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build(); + final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent( + root).build(); + final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent( + child1).build(); assertEquals(root, root.getTopParentWindow()); assertEquals(root, child1.getTopParentWindow()); @@ -244,7 +255,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testIsOnScreen_hiddenByPolicy() { - final WindowState window = createWindow(null, TYPE_APPLICATION, "window"); + final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build(); window.setHasSurface(true); assertTrue(window.isOnScreen()); window.hide(false /* doAnimation */, false /* requestAnim */); @@ -273,8 +284,8 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testCanBeImeTarget() { - final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow"); - final WindowState imeWindow = createWindow(null, TYPE_INPUT_METHOD, "imeWindow"); + final WindowState appWindow = newWindowBuilder("appWindow", TYPE_APPLICATION).build(); + final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_INPUT_METHOD).build(); // Setting FLAG_NOT_FOCUSABLE prevents the window from being an IME target. appWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE; @@ -328,16 +339,17 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testGetWindow() { - final WindowState root = createWindow(null, TYPE_APPLICATION, "root"); - final WindowState mediaChild = createWindow(root, TYPE_APPLICATION_MEDIA, "mediaChild"); - final WindowState mediaOverlayChild = createWindow(root, - TYPE_APPLICATION_MEDIA_OVERLAY, "mediaOverlayChild"); - final WindowState attachedDialogChild = createWindow(root, - TYPE_APPLICATION_ATTACHED_DIALOG, "attachedDialogChild"); - final WindowState subPanelChild = createWindow(root, - TYPE_APPLICATION_SUB_PANEL, "subPanelChild"); - final WindowState aboveSubPanelChild = createWindow(root, - TYPE_APPLICATION_ABOVE_SUB_PANEL, "aboveSubPanelChild"); + final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build(); + final WindowState mediaChild = newWindowBuilder("mediaChild", + TYPE_APPLICATION_MEDIA).setParent(root).build(); + final WindowState mediaOverlayChild = newWindowBuilder("mediaOverlayChild", + TYPE_APPLICATION_MEDIA_OVERLAY).setParent(root).build(); + final WindowState attachedDialogChild = newWindowBuilder("attachedDialogChild", + TYPE_APPLICATION_ATTACHED_DIALOG).setParent(root).build(); + final WindowState subPanelChild = newWindowBuilder("subPanelChild", + TYPE_APPLICATION_SUB_PANEL).setParent(root).build(); + final WindowState aboveSubPanelChild = newWindowBuilder("aboveSubPanelChild", + TYPE_APPLICATION_ABOVE_SUB_PANEL).setParent(root).build(); final LinkedList<WindowState> windows = new LinkedList<>(); @@ -358,7 +370,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testDestroySurface() { - final WindowState win = createWindow(null, TYPE_APPLICATION, "win"); + final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build(); win.mHasSurface = win.mAnimatingExit = true; win.mWinAnimator.mSurfaceControl = mock(SurfaceControl.class); win.onExitAnimationDone(); @@ -384,8 +396,10 @@ public class WindowStateTests extends WindowTestsBase { // Call prepareWindowToDisplayDuringRelayout for a window without FLAG_TURN_SCREEN_ON before // calling setCurrentLaunchCanTurnScreenOn for windows with flag in the same activity. final ActivityRecord activity = createActivityRecord(mDisplayContent); - final WindowState first = createWindow(null, TYPE_APPLICATION, activity, "first"); - final WindowState second = createWindow(null, TYPE_APPLICATION, activity, "second"); + final WindowState first = newWindowBuilder("first", TYPE_APPLICATION).setWindowToken( + activity).build(); + final WindowState second = newWindowBuilder("second", TYPE_APPLICATION).setWindowToken( + activity).build(); testPrepareWindowToDisplayDuringRelayout(first, false /* expectedWakeupCalled */, true /* expectedCurrentLaunchCanTurnScreenOn */); @@ -423,10 +437,10 @@ public class WindowStateTests extends WindowTestsBase { // Call prepareWindowToDisplayDuringRelayout for a windows that are not children of an // activity. Both windows have the FLAG_TURNS_SCREEN_ON so both should call wakeup final WindowToken windowToken = createTestWindowToken(FIRST_SUB_WINDOW, mDisplayContent); - final WindowState firstWindow = createWindow(null, TYPE_APPLICATION, windowToken, - "firstWindow"); - final WindowState secondWindow = createWindow(null, TYPE_APPLICATION, windowToken, - "secondWindow"); + final WindowState firstWindow = newWindowBuilder("firstWindow", + TYPE_APPLICATION).setWindowToken(windowToken).build(); + final WindowState secondWindow = newWindowBuilder("secondWindow", + TYPE_APPLICATION).setWindowToken(windowToken).build(); firstWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON; secondWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON; @@ -459,7 +473,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testCanAffectSystemUiFlags() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); app.mActivityRecord.setVisible(true); assertTrue(app.canAffectSystemUiFlags()); app.mActivityRecord.setVisible(false); @@ -471,7 +485,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testCanAffectSystemUiFlags_starting() { - final WindowState app = createWindow(null, TYPE_APPLICATION_STARTING, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION_STARTING).build(); app.mActivityRecord.setVisible(true); app.mStartingData = new SnapshotStartingData(mWm, null, 0); assertFalse(app.canAffectSystemUiFlags()); @@ -481,7 +495,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testCanAffectSystemUiFlags_disallow() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); app.mActivityRecord.setVisible(true); assertTrue(app.canAffectSystemUiFlags()); app.getTask().setCanAffectSystemUiFlags(false); @@ -538,9 +552,11 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testIsSelfOrAncestorWindowAnimating() { - final WindowState root = createWindow(null, TYPE_APPLICATION, "root"); - final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1"); - final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2"); + final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build(); + final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent( + root).build(); + final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent( + child1).build(); assertFalse(child2.isSelfOrAncestorWindowAnimatingExit()); child2.mAnimatingExit = true; assertTrue(child2.isSelfOrAncestorWindowAnimatingExit()); @@ -551,7 +567,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testDeferredRemovalByAnimating() { - final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow"); + final WindowState appWindow = newWindowBuilder("appWindow", TYPE_APPLICATION).build(); makeWindowVisible(appWindow); spyOn(appWindow.mWinAnimator); doReturn(true).when(appWindow.mWinAnimator).getShown(); @@ -571,8 +587,9 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testOnExitAnimationDone() { - final WindowState parent = createWindow(null, TYPE_APPLICATION, "parent"); - final WindowState child = createWindow(parent, TYPE_APPLICATION_PANEL, "child"); + final WindowState parent = newWindowBuilder("parent", TYPE_APPLICATION).build(); + final WindowState child = newWindowBuilder("child", TYPE_APPLICATION_PANEL).setParent( + parent).build(); final SurfaceControl.Transaction t = parent.getPendingTransaction(); child.startAnimation(t, mock(AnimationAdapter.class), false /* hidden */, SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION); @@ -609,7 +626,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testLayoutSeqResetOnReparent() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); app.mLayoutSeq = 1; mDisplayContent.mLayoutSeq = 1; @@ -622,7 +639,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testDisplayIdUpdatedOnReparent() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); // fake a different display app.mInputWindowHandle.setDisplayId(mDisplayContent.getDisplayId() + 1); app.onDisplayChanged(mDisplayContent); @@ -633,7 +650,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testApplyWithNextDraw() { - final WindowState win = createWindow(null, TYPE_APPLICATION_OVERLAY, "app"); + final WindowState win = newWindowBuilder("app", TYPE_APPLICATION_OVERLAY).build(); final SurfaceControl.Transaction[] handledT = { null }; // The normal case that the draw transaction is applied with finishing drawing. win.applyWithNextDraw(t -> handledT[0] = t); @@ -657,7 +674,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testSeamlesslyRotateWindow() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); final SurfaceControl.Transaction t = spy(StubTransaction.class); makeWindowVisible(app); @@ -707,7 +724,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testVisibilityChangeSwitchUser() { - final WindowState window = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState window = newWindowBuilder("app", TYPE_APPLICATION).build(); window.mHasSurface = true; spyOn(window); doReturn(false).when(window).showForAllUsers(); @@ -729,8 +746,9 @@ public class WindowStateTests extends WindowTestsBase { final CompatModePackages cmp = mWm.mAtmService.mCompatModePackages; spyOn(cmp); doReturn(overrideScale).when(cmp).getCompatScale(anyString(), anyInt()); - final WindowState w = createWindow(null, TYPE_APPLICATION_OVERLAY, "win"); - final WindowState child = createWindow(w, TYPE_APPLICATION_PANEL, "child"); + final WindowState w = newWindowBuilder("win", TYPE_APPLICATION_OVERLAY).build(); + final WindowState child = newWindowBuilder("child", TYPE_APPLICATION_PANEL).setParent( + w).build(); assertTrue(w.hasCompatScale()); assertTrue(child.hasCompatScale()); @@ -788,7 +806,8 @@ public class WindowStateTests extends WindowTestsBase { // Child window without scale (e.g. different app) should apply inverse scale of parent. doReturn(1f).when(cmp).getCompatScale(anyString(), anyInt()); - final WindowState child2 = createWindow(w, TYPE_APPLICATION_SUB_PANEL, "child2"); + final WindowState child2 = newWindowBuilder("child2", TYPE_APPLICATION_SUB_PANEL).setParent( + w).build(); makeWindowVisible(w, child2); clearInvocations(t); child2.prepareSurfaces(); @@ -798,10 +817,10 @@ public class WindowStateTests extends WindowTestsBase { @SetupWindows(addWindows = { W_ABOVE_ACTIVITY, W_NOTIFICATION_SHADE }) @Test public void testRequestDrawIfNeeded() { - final WindowState startingApp = createWindow(null /* parent */, - TYPE_BASE_APPLICATION, "startingApp"); - final WindowState startingWindow = createWindow(null /* parent */, - TYPE_APPLICATION_STARTING, startingApp.mToken, "starting"); + final WindowState startingApp = newWindowBuilder("startingApp", + TYPE_BASE_APPLICATION).build(); + final WindowState startingWindow = newWindowBuilder("starting", + TYPE_APPLICATION_STARTING).setWindowToken(startingApp.mToken).build(); startingApp.mActivityRecord.mStartingWindow = startingWindow; final WindowState keyguardHostWindow = mNotificationShadeWindow; final WindowState allDrawnApp = mAppWindow; @@ -878,7 +897,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testRequestResizeForBlastSync() { - final WindowState win = createWindow(null, TYPE_APPLICATION, "window"); + final WindowState win = newWindowBuilder("window", TYPE_APPLICATION).build(); makeWindowVisible(win); makeLastConfigReportedToClient(win, true /* visible */); win.mLayoutSeq = win.getDisplayContent().mLayoutSeq; @@ -926,8 +945,8 @@ public class WindowStateTests extends WindowTestsBase { final Task task = createTask(mDisplayContent); final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer); final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity(); - final WindowState win = createWindow(null /* parent */, TYPE_APPLICATION, embeddedActivity, - "App window"); + final WindowState win = newWindowBuilder("App window", TYPE_APPLICATION).setWindowToken( + embeddedActivity).build(); doReturn(true).when(embeddedActivity).isVisible(); embeddedActivity.setVisibleRequested(true); makeWindowVisible(win); @@ -949,14 +968,14 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testCantReceiveTouchWhenAppTokenHiddenRequested() { - final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0"); + final WindowState win0 = newWindowBuilder("win0", TYPE_APPLICATION).build(); win0.mActivityRecord.setVisibleRequested(false); assertFalse(win0.canReceiveTouchInput()); } @Test public void testCantReceiveTouchWhenNotFocusable() { - final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0"); + final WindowState win0 = newWindowBuilder("win0", TYPE_APPLICATION).build(); final Task rootTask = win0.mActivityRecord.getRootTask(); spyOn(rootTask); when(rootTask.shouldIgnoreInput()).thenReturn(true); @@ -969,7 +988,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testUpdateInputWindowHandle() { - final WindowState win = createWindow(null, TYPE_APPLICATION, "win"); + final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build(); win.mAttrs.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; win.mAttrs.flags = FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH; final InputWindowHandle handle = new InputWindowHandle( @@ -982,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); @@ -1026,7 +1044,7 @@ public class WindowStateTests extends WindowTestsBase { @DisableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX) @Test public void testTouchRegionUsesLetterboxBoundsIfTransformedBoundsAndLetterboxScrolling() { - final WindowState win = createWindow(null, TYPE_APPLICATION, "win"); + final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build(); // Transformed bounds used for size of touchable region if letterbox inner bounds are empty. final Rect transformedBounds = new Rect(0, 0, 300, 500); @@ -1051,7 +1069,7 @@ public class WindowStateTests extends WindowTestsBase { @DisableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX) @Test public void testTouchRegionUsesLetterboxBoundsIfNullTransformedBoundsAndLetterboxScrolling() { - final WindowState win = createWindow(null, TYPE_APPLICATION, "win"); + final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build(); // Fragment bounds used for size of touchable region if letterbox inner bounds are empty // and Transform bounds are null. @@ -1083,7 +1101,7 @@ public class WindowStateTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX) @Test public void testTouchRegionUsesTransformedBoundsIfLetterboxScrolling() { - final WindowState win = createWindow(null, TYPE_APPLICATION, "win"); + final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build(); // Transformed bounds used for size of touchable region if letterbox inner bounds are empty. final Rect transformedBounds = new Rect(0, 0, 300, 500); @@ -1109,7 +1127,7 @@ public class WindowStateTests extends WindowTestsBase { public void testHasActiveVisibleWindow() { final int uid = ActivityBuilder.DEFAULT_FAKE_UID; - final WindowState app = createWindow(null, TYPE_APPLICATION, "app", uid); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).setOwnerId(uid).build(); app.mActivityRecord.setVisible(false); app.mActivityRecord.setVisibility(false); assertFalse(mAtm.hasActiveVisibleWindow(uid)); @@ -1120,15 +1138,17 @@ public class WindowStateTests extends WindowTestsBase { // Make the activity invisible and add a visible toast. The uid should have no active // visible window because toast can be misused by legacy app to bypass background check. app.mActivityRecord.setVisibility(false); - final WindowState overlay = createWindow(null, TYPE_APPLICATION_OVERLAY, "overlay", uid); - final WindowState toast = createWindow(null, TYPE_TOAST, app.mToken, "toast", uid); + final WindowState overlay = newWindowBuilder("overlay", + TYPE_APPLICATION_OVERLAY).setOwnerId(uid).build(); + final WindowState toast = newWindowBuilder("toast", TYPE_TOAST).setWindowToken( + app.mToken).setOwnerId(uid).build(); toast.onSurfaceShownChanged(true); assertFalse(mAtm.hasActiveVisibleWindow(uid)); // Though starting window should belong to system. Make sure it is ignored to avoid being // allow-list unexpectedly, see b/129563343. - final WindowState starting = - createWindow(null, TYPE_APPLICATION_STARTING, app.mToken, "starting", uid); + final WindowState starting = newWindowBuilder("starting", + TYPE_APPLICATION_STARTING).setWindowToken(app.mToken).setOwnerId(uid).build(); starting.onSurfaceShownChanged(true); assertFalse(mAtm.hasActiveVisibleWindow(uid)); @@ -1145,8 +1165,8 @@ public class WindowStateTests extends WindowTestsBase { @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD }) @Test public void testNeedsRelativeLayeringToIme_notAttached() { - WindowState sameTokenWindow = createWindow(null, TYPE_BASE_APPLICATION, mAppWindow.mToken, - "SameTokenWindow"); + WindowState sameTokenWindow = newWindowBuilder("SameTokenWindow", + TYPE_BASE_APPLICATION).setWindowToken(mAppWindow.mToken).build(); mDisplayContent.setImeLayeringTarget(mAppWindow); makeWindowVisible(mImeWindow); sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); @@ -1158,8 +1178,8 @@ public class WindowStateTests extends WindowTestsBase { @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD }) @Test public void testNeedsRelativeLayeringToIme_startingWindow() { - WindowState sameTokenWindow = createWindow(null, TYPE_APPLICATION_STARTING, - mAppWindow.mToken, "SameTokenWindow"); + WindowState sameTokenWindow = newWindowBuilder("SameTokenWindow", + TYPE_APPLICATION_STARTING).setWindowToken(mAppWindow.mToken).build(); mDisplayContent.setImeLayeringTarget(mAppWindow); makeWindowVisible(mImeWindow); sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); @@ -1169,9 +1189,9 @@ public class WindowStateTests extends WindowTestsBase { @UseTestDisplay(addWindows = {W_ACTIVITY, W_INPUT_METHOD}) @Test public void testNeedsRelativeLayeringToIme_systemDialog() { - WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY, - mDisplayContent, - "SystemDialog", true); + WindowState systemDialogWindow = newWindowBuilder("SystemDialog", + TYPE_SECURE_SYSTEM_OVERLAY).setDisplay( + mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build(); mDisplayContent.setImeLayeringTarget(mAppWindow); mAppWindow.getTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); makeWindowVisible(mImeWindow); @@ -1182,20 +1202,21 @@ public class WindowStateTests extends WindowTestsBase { @UseTestDisplay(addWindows = {W_INPUT_METHOD}) @Test public void testNeedsRelativeLayeringToIme_notificationShadeShouldNotHideSystemDialog() { - WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY, - mDisplayContent, - "SystemDialog", true); + WindowState systemDialogWindow = newWindowBuilder("SystemDialog", + TYPE_SECURE_SYSTEM_OVERLAY).setDisplay( + mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build(); mDisplayContent.setImeLayeringTarget(systemDialogWindow); makeWindowVisible(mImeWindow); - WindowState notificationShade = createWindow(null, TYPE_NOTIFICATION_SHADE, - mDisplayContent, "NotificationShade", true); + WindowState notificationShade = newWindowBuilder("NotificationShade", + TYPE_NOTIFICATION_SHADE).setDisplay( + mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build(); notificationShade.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; assertFalse(notificationShade.needsRelativeLayeringToIme()); } @Test public void testSetFreezeInsetsState() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); spyOn(app); doReturn(true).when(app).isVisible(); @@ -1216,7 +1237,7 @@ public class WindowStateTests extends WindowTestsBase { verify(app).notifyInsetsChanged(); // Verify that invisible non-activity window won't dispatch insets changed. - final WindowState overlay = createWindow(null, TYPE_APPLICATION_OVERLAY, "overlay"); + final WindowState overlay = newWindowBuilder("overlay", TYPE_APPLICATION_OVERLAY).build(); makeWindowVisible(overlay); assertTrue(overlay.isReadyToDispatchInsetsState()); overlay.mHasSurface = false; @@ -1244,9 +1265,9 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testAdjustImeInsetsVisibilityWhenSwitchingApps() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); - final WindowState app2 = createWindow(null, TYPE_APPLICATION, "app2"); - final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); + final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build(); + final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_APPLICATION).build(); spyOn(imeWindow); doReturn(true).when(imeWindow).isVisible(); mDisplayContent.mInputMethodWindow = imeWindow; @@ -1279,10 +1300,11 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testAdjustImeInsetsVisibilityWhenSwitchingApps_toAppInMultiWindowMode() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); - final WindowState app2 = createWindow(null, WINDOWING_MODE_MULTI_WINDOW, - ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app2"); - final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); + final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).setWindowingMode( + WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay( + mDisplayContent).build(); + final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_APPLICATION).build(); spyOn(imeWindow); doReturn(true).when(imeWindow).isVisible(); mDisplayContent.mInputMethodWindow = imeWindow; @@ -1321,8 +1343,8 @@ public class WindowStateTests extends WindowTestsBase { @SetupWindows(addWindows = W_ACTIVITY) @Test public void testUpdateImeControlTargetWhenLeavingMultiWindow() { - WindowState app = createWindow(null, TYPE_BASE_APPLICATION, - mAppWindow.mToken, "app"); + WindowState app = newWindowBuilder("app", TYPE_BASE_APPLICATION).setWindowToken( + mAppWindow.mToken).build(); mDisplayContent.setRemoteInsetsController(createDisplayWindowInsetsController()); spyOn(app); @@ -1349,8 +1371,8 @@ public class WindowStateTests extends WindowTestsBase { @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD, W_NOTIFICATION_SHADE }) @Test public void testNotificationShadeHasImeInsetsWhenMultiWindow() { - WindowState app = createWindow(null, TYPE_BASE_APPLICATION, - mAppWindow.mToken, "app"); + WindowState app = newWindowBuilder("app", TYPE_BASE_APPLICATION).setWindowToken( + mAppWindow.mToken).build(); // Simulate entering multi-window mode and windowing mode is multi-window. app.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); @@ -1376,7 +1398,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testRequestedVisibility() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); app.mActivityRecord.setVisible(false); app.mActivityRecord.setVisibility(false); assertFalse(app.isVisibleRequested()); @@ -1391,7 +1413,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testKeepClearAreas() { - final WindowState window = createWindow(null, TYPE_APPLICATION, "window"); + final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build(); makeWindowVisible(window); final Rect keepClearArea1 = new Rect(0, 0, 10, 10); @@ -1433,7 +1455,7 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testUnrestrictedKeepClearAreas() { - final WindowState window = createWindow(null, TYPE_APPLICATION, "window"); + final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build(); makeWindowVisible(window); final Rect keepClearArea1 = new Rect(0, 0, 10, 10); @@ -1481,8 +1503,9 @@ public class WindowStateTests extends WindowTestsBase { final InputMethodManagerInternal immi = InputMethodManagerInternal.get(); spyOn(immi); - final WindowState imeTarget = createWindow(null /* parent */, TYPE_BASE_APPLICATION, - createActivityRecord(mDisplayContent), "imeTarget"); + final WindowState imeTarget = newWindowBuilder("imeTarget", + TYPE_BASE_APPLICATION).setWindowToken( + createActivityRecord(mDisplayContent)).build(); imeTarget.mActivityRecord.setVisibleRequested(true); makeWindowVisible(imeTarget); @@ -1562,8 +1585,8 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testIsSecureLocked_flagSecureSet() { - WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window", - 1 /* ownerId */); + WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId( + 1).build(); window.mAttrs.flags |= WindowManager.LayoutParams.FLAG_SECURE; assertTrue(window.isSecureLocked()); @@ -1571,8 +1594,8 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testIsSecureLocked_flagSecureNotSet() { - WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window", - 1 /* ownerId */); + WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId( + 1).build(); assertFalse(window.isSecureLocked()); } @@ -1581,8 +1604,8 @@ public class WindowStateTests extends WindowTestsBase { public void testIsSecureLocked_disableSecureWindows() { assumeTrue(Build.IS_DEBUGGABLE); - WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window", - 1 /* ownerId */); + WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId( + 1).build(); window.mAttrs.flags |= WindowManager.LayoutParams.FLAG_SECURE; ContentResolver cr = useFakeSettingsProvider(); @@ -1617,8 +1640,10 @@ public class WindowStateTests extends WindowTestsBase { String testPackage = "test"; int ownerId1 = 20; int ownerId2 = 21; - final WindowState window1 = createWindow(null, TYPE_APPLICATION, "window1", ownerId1); - final WindowState window2 = createWindow(null, TYPE_APPLICATION, "window2", ownerId2); + final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setOwnerId( + ownerId1).build(); + final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setOwnerId( + ownerId2).build(); // Setting packagename for targeted feature window1.mAttrs.packageName = testPackage; @@ -1638,7 +1663,8 @@ public class WindowStateTests extends WindowTestsBase { public void testIsSecureLocked_sensitiveContentBlockOrClearScreenCaptureForApp() { String testPackage = "test"; int ownerId = 20; - final WindowState window = createWindow(null, TYPE_APPLICATION, "window", ownerId); + final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).setOwnerId( + ownerId).build(); window.mAttrs.packageName = testPackage; assertFalse(window.isSecureLocked()); 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/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java index f226b9d29ca0..a02c3db1e636 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java @@ -74,11 +74,16 @@ public class WindowTokenTests extends WindowTestsBase { assertEquals(0, token.getWindowsCount()); - final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1"); - final WindowState window11 = createWindow(window1, FIRST_SUB_WINDOW, token, "window11"); - final WindowState window12 = createWindow(window1, FIRST_SUB_WINDOW, token, "window12"); - final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2"); - final WindowState window3 = createWindow(null, TYPE_APPLICATION, token, "window3"); + final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setWindowToken( + token).build(); + final WindowState window11 = newWindowBuilder("window11", FIRST_SUB_WINDOW).setParent( + window1).setWindowToken(token).build(); + final WindowState window12 = newWindowBuilder("window12", FIRST_SUB_WINDOW).setParent( + window1).setWindowToken(token).build(); + final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setWindowToken( + token).build(); + final WindowState window3 = newWindowBuilder("window3", TYPE_APPLICATION).setWindowToken( + token).build(); token.addWindow(window1); // NOTE: Child windows will not be added to the token as window containers can only @@ -105,8 +110,10 @@ public class WindowTokenTests extends WindowTestsBase { public void testAddWindow_assignsLayers() { final TestWindowToken token1 = createTestWindowToken(0, mDisplayContent); final TestWindowToken token2 = createTestWindowToken(0, mDisplayContent); - final WindowState window1 = createWindow(null, TYPE_STATUS_BAR, token1, "window1"); - final WindowState window2 = createWindow(null, TYPE_STATUS_BAR, token2, "window2"); + final WindowState window1 = newWindowBuilder("window1", TYPE_STATUS_BAR).setWindowToken( + token1).build(); + final WindowState window2 = newWindowBuilder("window2", TYPE_STATUS_BAR).setWindowToken( + token2).build(); token1.addWindow(window1); token2.addWindow(window2); @@ -122,8 +129,10 @@ public class WindowTokenTests extends WindowTestsBase { assertEquals(token, dc.getWindowToken(token.token)); - final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1"); - final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2"); + final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setWindowToken( + token).build(); + final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setWindowToken( + token).build(); window2.removeImmediately(); // The token should still be mapped in the display content since it still has a child. @@ -147,8 +156,10 @@ public class WindowTokenTests extends WindowTestsBase { // Verify that the token is on the display assertNotNull(mDisplayContent.getWindowToken(token.token)); - final WindowState window1 = createWindow(null, TYPE_TOAST, token, "window1"); - final WindowState window2 = createWindow(null, TYPE_TOAST, token, "window2"); + final WindowState window1 = newWindowBuilder("window1", TYPE_TOAST).setWindowToken( + token).build(); + final WindowState window2 = newWindowBuilder("window2", TYPE_TOAST).setWindowToken( + token).build(); mDisplayContent.removeWindowToken(token.token, true /* animateExit */); // Verify that the token is no longer mapped on the display @@ -231,7 +242,8 @@ public class WindowTokenTests extends WindowTestsBase { assertNull(fromClientToken.mSurfaceControl); - createWindow(null, TYPE_APPLICATION_OVERLAY, fromClientToken, "window"); + newWindowBuilder("window", TYPE_APPLICATION_OVERLAY).setWindowToken( + fromClientToken).build(); assertNotNull(fromClientToken.mSurfaceControl); final WindowToken nonClientToken = new WindowToken.Builder(mDisplayContent.mWmService, @@ -285,7 +297,7 @@ public class WindowTokenTests extends WindowTestsBase { // Simulate an app window to be the IME layering target, assume the app window has no // frozen insets state by default. - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build(); mDisplayContent.setImeLayeringTarget(app); assertNull(app.getFrozenInsetsState()); assertTrue(app.isImeLayeringTarget()); @@ -299,7 +311,8 @@ public class WindowTokenTests extends WindowTestsBase { @Test public void testRemoveWindowToken_noAnimateExitWhenSet() { final TestWindowToken token = createTestWindowToken(0, mDisplayContent); - final WindowState win = createWindow(null, TYPE_APPLICATION, token, "win"); + final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).setWindowToken( + token).build(); makeWindowVisible(win); assertTrue(win.isOnScreen()); spyOn(win); diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java index 4f60106db93d..84e21181a7b9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java @@ -221,7 +221,7 @@ public class ZOrderingTests extends WindowTestsBase { } WindowState createWindow(String name) { - return createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, name); + return newWindowBuilder(name, TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build(); } @Test @@ -263,12 +263,12 @@ public class ZOrderingTests extends WindowTestsBase { @Test public void testAssignWindowLayers_ForImeWithAppTargetWithChildWindows() { final WindowState imeAppTarget = createWindow("imeAppTarget"); - final WindowState imeAppTargetChildAboveWindow = createWindow(imeAppTarget, - TYPE_APPLICATION_ATTACHED_DIALOG, imeAppTarget.mToken, - "imeAppTargetChildAboveWindow"); - final WindowState imeAppTargetChildBelowWindow = createWindow(imeAppTarget, - TYPE_APPLICATION_MEDIA_OVERLAY, imeAppTarget.mToken, - "imeAppTargetChildBelowWindow"); + final WindowState imeAppTargetChildAboveWindow = newWindowBuilder( + "imeAppTargetChildAboveWindow", TYPE_APPLICATION_ATTACHED_DIALOG).setParent( + imeAppTarget).setWindowToken(imeAppTarget.mToken).build(); + final WindowState imeAppTargetChildBelowWindow = newWindowBuilder( + "imeAppTargetChildBelowWindow", TYPE_APPLICATION_MEDIA_OVERLAY).setParent( + imeAppTarget).setWindowToken(imeAppTarget.mToken).build(); mDisplayContent.setImeLayeringTarget(imeAppTarget); makeWindowVisible(mImeWindow); @@ -313,9 +313,9 @@ public class ZOrderingTests extends WindowTestsBase { @Test public void testAssignWindowLayers_ForImeNonAppImeTarget() { - final WindowState imeSystemOverlayTarget = createWindow(null, TYPE_SYSTEM_OVERLAY, - mDisplayContent, "imeSystemOverlayTarget", - true /* ownerCanAddInternalSystemWindow */); + final WindowState imeSystemOverlayTarget = newWindowBuilder("imeSystemOverlayTarget", + TYPE_SYSTEM_OVERLAY).setDisplay(mDisplayContent).setOwnerCanAddInternalSystemWindow( + true).build(); mDisplayContent.setImeLayeringTarget(imeSystemOverlayTarget); mDisplayContent.assignChildLayers(mTransaction); @@ -354,18 +354,19 @@ public class ZOrderingTests extends WindowTestsBase { @Test public void testStackLayers() { final WindowState anyWindow1 = createWindow("anyWindow"); - final WindowState pinnedStackWindow = createWindow(null, WINDOWING_MODE_PINNED, - ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION, mDisplayContent, - "pinnedStackWindow"); - final WindowState dockedStackWindow = createWindow(null, - WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION, - mDisplayContent, "dockedStackWindow"); - final WindowState assistantStackWindow = createWindow(null, - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_ASSISTANT, TYPE_BASE_APPLICATION, - mDisplayContent, "assistantStackWindow"); - final WindowState homeActivityWindow = createWindow(null, WINDOWING_MODE_FULLSCREEN, - ACTIVITY_TYPE_HOME, TYPE_BASE_APPLICATION, - mDisplayContent, "homeActivityWindow"); + final WindowState pinnedStackWindow = newWindowBuilder("pinnedStackWindow", + TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_PINNED).setActivityType( + ACTIVITY_TYPE_STANDARD).setDisplay(mDisplayContent).build(); + final WindowState dockedStackWindow = newWindowBuilder("dockedStackWindow", + TYPE_BASE_APPLICATION).setWindowingMode( + WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay( + mDisplayContent).build(); + final WindowState assistantStackWindow = newWindowBuilder("assistantStackWindow", + TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_FULLSCREEN).setActivityType( + ACTIVITY_TYPE_ASSISTANT).setDisplay(mDisplayContent).build(); + final WindowState homeActivityWindow = newWindowBuilder("homeActivityWindow", + TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_FULLSCREEN).setActivityType( + ACTIVITY_TYPE_HOME).setDisplay(mDisplayContent).build(); final WindowState anyWindow2 = createWindow("anyWindow2"); mDisplayContent.assignChildLayers(mTransaction); @@ -383,13 +384,12 @@ public class ZOrderingTests extends WindowTestsBase { @Test public void testAssignWindowLayers_ForSysUiPanels() { - final WindowState navBarPanel = - createWindow(null, TYPE_NAVIGATION_BAR_PANEL, mDisplayContent, "NavBarPanel"); - final WindowState statusBarPanel = - createWindow(null, TYPE_STATUS_BAR_ADDITIONAL, mDisplayContent, - "StatusBarAdditional"); - final WindowState statusBarSubPanel = - createWindow(null, TYPE_STATUS_BAR_SUB_PANEL, mDisplayContent, "StatusBarSubPanel"); + final WindowState navBarPanel = newWindowBuilder("NavBarPanel", + TYPE_NAVIGATION_BAR_PANEL).setDisplay(mDisplayContent).build(); + final WindowState statusBarPanel = newWindowBuilder("StatusBarAdditional", + TYPE_STATUS_BAR_ADDITIONAL).setDisplay(mDisplayContent).build(); + final WindowState statusBarSubPanel = newWindowBuilder("StatusBarSubPanel", + TYPE_STATUS_BAR_SUB_PANEL).setDisplay(mDisplayContent).build(); mDisplayContent.assignChildLayers(mTransaction); // Ime should be above all app windows and below system windows if it is targeting an app @@ -401,15 +401,16 @@ public class ZOrderingTests extends WindowTestsBase { @Test public void testAssignWindowLayers_ForImeOnPopupImeLayeringTarget() { - final WindowState imeAppTarget = createWindow(null, TYPE_APPLICATION, - mAppWindow.mActivityRecord, "imeAppTarget"); + final WindowState imeAppTarget = newWindowBuilder("imeAppTarget", + TYPE_APPLICATION).setWindowToken(mAppWindow.mActivityRecord).build(); mDisplayContent.setImeInputTarget(imeAppTarget); mDisplayContent.setImeLayeringTarget(imeAppTarget); mDisplayContent.setImeControlTarget(imeAppTarget); // Set a popup IME layering target and keeps the original IME control target behinds it. - final WindowState popupImeTargetWin = createWindow(imeAppTarget, - TYPE_APPLICATION_SUB_PANEL, mAppWindow.mActivityRecord, "popupImeTargetWin"); + final WindowState popupImeTargetWin = newWindowBuilder("popupImeTargetWin", + TYPE_APPLICATION_SUB_PANEL).setParent(imeAppTarget).setWindowToken( + mAppWindow.mActivityRecord).build(); mDisplayContent.setImeLayeringTarget(popupImeTargetWin); mDisplayContent.updateImeParent(); @@ -424,11 +425,11 @@ public class ZOrderingTests extends WindowTestsBase { // then we can drop all negative layering on the windowing side. final WindowState anyWindow = createWindow("anyWindow"); - final WindowState child = createWindow(anyWindow, TYPE_APPLICATION_MEDIA, mDisplayContent, - "TypeApplicationMediaChild"); - final WindowState mediaOverlayChild = createWindow(anyWindow, - TYPE_APPLICATION_MEDIA_OVERLAY, - mDisplayContent, "TypeApplicationMediaOverlayChild"); + final WindowState child = newWindowBuilder("TypeApplicationMediaChild", + TYPE_APPLICATION_MEDIA).setParent(anyWindow).setDisplay(mDisplayContent).build(); + final WindowState mediaOverlayChild = newWindowBuilder("TypeApplicationMediaOverlayChild", + TYPE_APPLICATION_MEDIA_OVERLAY).setParent(anyWindow).setDisplay( + mDisplayContent).build(); mDisplayContent.assignChildLayers(mTransaction); @@ -440,14 +441,17 @@ public class ZOrderingTests extends WindowTestsBase { public void testAssignWindowLayers_ForPostivelyZOrderedSubtype() { final WindowState anyWindow = createWindow("anyWindow"); final ArrayList<WindowState> childList = new ArrayList<>(); - childList.add(createWindow(anyWindow, TYPE_APPLICATION_PANEL, mDisplayContent, - "TypeApplicationPanelChild")); - childList.add(createWindow(anyWindow, TYPE_APPLICATION_SUB_PANEL, mDisplayContent, - "TypeApplicationSubPanelChild")); - childList.add(createWindow(anyWindow, TYPE_APPLICATION_ATTACHED_DIALOG, mDisplayContent, - "TypeApplicationAttachedDialogChild")); - childList.add(createWindow(anyWindow, TYPE_APPLICATION_ABOVE_SUB_PANEL, mDisplayContent, - "TypeApplicationAboveSubPanelPanelChild")); + childList.add(newWindowBuilder("TypeApplicationPanelChild", + TYPE_APPLICATION_PANEL).setParent(anyWindow).setDisplay(mDisplayContent).build()); + childList.add(newWindowBuilder("TypeApplicationSubPanelChild", + TYPE_APPLICATION_SUB_PANEL).setParent(anyWindow).setDisplay( + mDisplayContent).build()); + childList.add(newWindowBuilder("TypeApplicationAttachedDialogChild", + TYPE_APPLICATION_ATTACHED_DIALOG).setParent(anyWindow).setDisplay( + mDisplayContent).build()); + childList.add(newWindowBuilder("TypeApplicationAboveSubPanelPanelChild", + TYPE_APPLICATION_ABOVE_SUB_PANEL).setParent(anyWindow).setDisplay( + mDisplayContent).build()); final LayerRecordingTransaction t = mTransaction; mDisplayContent.assignChildLayers(t); @@ -469,8 +473,8 @@ public class ZOrderingTests extends WindowTestsBase { // Create a popupWindow assertWindowHigher(mImeWindow, mAppWindow); - final WindowState popupWindow = createWindow(mAppWindow, TYPE_APPLICATION_PANEL, - mDisplayContent, "PopupWindow"); + final WindowState popupWindow = newWindowBuilder("PopupWindow", + TYPE_APPLICATION_PANEL).setParent(mAppWindow).setDisplay(mDisplayContent).build(); spyOn(popupWindow); mDisplayContent.assignChildLayers(mTransaction); @@ -492,8 +496,9 @@ public class ZOrderingTests extends WindowTestsBase { makeWindowVisible(mImeWindow); // Create a popupWindow - final WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY, - mDisplayContent, "SystemDialog", true); + final WindowState systemDialogWindow = newWindowBuilder("SystemDialog", + TYPE_SECURE_SYSTEM_OVERLAY).setDisplay( + mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build(); systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; spyOn(systemDialogWindow); 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/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/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/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 4d7085feb98f..d35c9008e8cb 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -830,6 +830,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 +855,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 +874,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 ) diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp index 449d93dd8c0b..20315561cceb 100644 --- a/tools/aapt2/cmd/Command.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -53,61 +53,67 @@ 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 { + auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { *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 { + auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { 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 { + auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { *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 { + auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { 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 { + auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { 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 { + auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { *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 +178,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 +272,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; + } + 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; } - flag.action(arg); + 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..2a3cb2a0c65d 100644 --- a/tools/aapt2/cmd/Command_test.cpp +++ b/tools/aapt2/cmd/Command_test.cpp @@ -118,4 +118,45 @@ 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)); +} + } // namespace aapt
\ No newline at end of file |