summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/system-current.txt1
-rw-r--r--core/java/android/accessibilityservice/AccessibilityService.java99
-rw-r--r--core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl2
-rw-r--r--core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl3
-rw-r--r--core/java/android/app/ApplicationPackageManager.java3
-rw-r--r--core/java/android/app/Notification.java8
-rw-r--r--core/java/android/app/UiAutomation.java5
-rw-r--r--core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java8
-rw-r--r--core/java/android/appwidget/flags.aconfig10
-rw-r--r--core/java/android/companion/virtual/flags/flags.aconfig10
-rw-r--r--core/java/android/content/pm/PackageInstaller.java3
-rw-r--r--core/java/android/text/Layout.java81
-rw-r--r--core/java/android/text/SpanColors.java89
-rw-r--r--core/java/android/view/InsetsSourceControl.java12
-rw-r--r--core/java/android/view/ViewRootImpl.java11
-rw-r--r--core/java/android/view/accessibility/AccessibilityDisplayProxy.java4
-rw-r--r--core/java/android/view/accessibility/flags/accessibility_flags.aconfig10
-rw-r--r--core/java/android/window/ClientWindowFrames.java15
-rw-r--r--core/java/android/window/SnapshotDrawerUtils.java11
-rw-r--r--core/java/com/android/internal/util/NewlineNormalizer.java39
-rw-r--r--core/java/com/android/internal/util/NotificationBigTextNormalizer.java123
-rw-r--r--core/jni/android_media_AudioSystem.cpp14
-rw-r--r--core/proto/android/app/appexitinfo.proto2
-rw-r--r--core/proto/android/app/appstartinfo.proto2
-rw-r--r--core/proto/android/os/batterystats.proto2
-rw-r--r--core/proto/android/server/activitymanagerservice.proto2
-rw-r--r--core/proto/android/server/jobscheduler.proto2
-rw-r--r--core/proto/android/server/powermanagerservice.proto2
-rw-r--r--core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java18
-rw-r--r--core/tests/coretests/src/android/text/LayoutTest.java78
-rw-r--r--core/tests/coretests/src/android/text/SpanColorsTest.java78
-rw-r--r--core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java4
-rw-r--r--core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java71
-rw-r--r--core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java148
-rw-r--r--keystore/java/android/security/keystore/KeyGenParameterSpec.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt2
-rw-r--r--media/java/android/media/AudioSystem.java4
-rw-r--r--nfc/api/system-current.txt1
-rw-r--r--nfc/java/android/nfc/NfcAdapter.java46
-rw-r--r--nfc/java/android/nfc/flags.aconfig9
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt43
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt11
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt9
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt6
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt6
-rw-r--r--packages/SettingsLib/Color/res/values/colors.xml1
-rw-r--r--packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt10
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt25
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt18
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/util/Assert.java15
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt95
-rw-r--r--packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml18
-rw-r--r--packages/SystemUI/res/raw/face_dialog_authenticating.json1
-rw-r--r--packages/SystemUI/res/values/strings.xml39
-rw-r--r--packages/SystemUI/res/values/styles.xml4
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt97
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt413
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt53
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java130
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt184
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt40
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt45
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt54
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt18
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt106
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt9
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java61
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java19
-rw-r--r--services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java8
-rw-r--r--services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java10
-rw-r--r--services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java2
-rw-r--r--services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java4
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java9
-rw-r--r--services/core/java/com/android/server/PackageWatchdog.java26
-rw-r--r--services/core/java/com/android/server/am/PendingIntentRecord.java6
-rw-r--r--services/core/java/com/android/server/audio/AudioPolicyFacade.java4
-rw-r--r--services/core/java/com/android/server/audio/AudioServerPermissionProvider.java149
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java235
-rw-r--r--services/core/java/com/android/server/audio/AudioSystemAdapter.java15
-rw-r--r--services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java25
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiControlService.java4
-rw-r--r--services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java164
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerService.java4
-rw-r--r--services/core/java/com/android/server/media/MediaRoute2Provider.java28
-rw-r--r--services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java23
-rw-r--r--services/core/java/com/android/server/media/SystemMediaRoute2Provider.java49
-rw-r--r--services/core/java/com/android/server/pm/PackageArchiver.java3
-rw-r--r--services/core/java/com/android/server/trust/TrustManagerService.java314
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java22
-rw-r--r--services/core/java/com/android/server/wm/BackgroundActivityStartController.java5
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java8
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java52
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java17
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java308
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java3
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java19
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java13
-rw-r--r--telephony/java/android/telephony/CarrierRestrictionRules.java5
-rw-r--r--tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt2
-rw-r--r--tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java10
-rw-r--r--tests/TrustTests/AndroidManifest.xml11
-rw-r--r--tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt227
-rw-r--r--tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt44
-rw-r--r--tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt62
-rw-r--r--tests/TrustTests/src/android/trust/test/lib/Utils.kt (renamed from tests/TrustTests/src/android/trust/test/lib/utils.kt)33
128 files changed, 3981 insertions, 800 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 96315ebccc49..50d97cf0626f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -14435,6 +14435,7 @@ package android.telephony {
method @NonNull public android.telephony.CarrierRestrictionRules build();
method @NonNull public android.telephony.CarrierRestrictionRules.Builder setAllCarriersAllowed();
method @NonNull public android.telephony.CarrierRestrictionRules.Builder setAllowedCarriers(@NonNull java.util.List<android.service.carrier.CarrierIdentifier>);
+ method @FlaggedApi("com.android.internal.telephony.flags.set_carrier_restriction_status") @NonNull public android.telephony.CarrierRestrictionRules.Builder setCarrierRestrictionStatus(int);
method @NonNull public android.telephony.CarrierRestrictionRules.Builder setDefaultCarrierRestriction(int);
method @NonNull public android.telephony.CarrierRestrictionRules.Builder setExcludedCarriers(@NonNull java.util.List<android.service.carrier.CarrierIdentifier>);
method @NonNull public android.telephony.CarrierRestrictionRules.Builder setMultiSimPolicy(int);
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index fd9600c1f87f..65628d32e583 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -16,6 +16,7 @@
package android.accessibilityservice;
+import static android.accessibilityservice.AccessibilityServiceInfo.CAPABILITY_CAN_CONTROL_MAGNIFICATION;
import static android.accessibilityservice.MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN;
import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
@@ -69,6 +70,8 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.accessibility.AccessibilityWindowInfo;
import android.view.inputmethod.EditorInfo;
+import androidx.annotation.GuardedBy;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.CancellationGroup;
import com.android.internal.inputmethod.IAccessibilityInputMethodSession;
@@ -640,6 +643,8 @@ public abstract class AccessibilityService extends Service {
/** The detected gesture information for different displays */
boolean onGesture(AccessibilityGestureEvent gestureInfo);
boolean onKeyEvent(KeyEvent event);
+ /** Magnification SystemUI connection changed callbacks */
+ void onMagnificationSystemUIConnectionChanged(boolean connected);
/** Magnification changed callbacks for different displays */
void onMagnificationChanged(int displayId, @NonNull Region region,
MagnificationConfig config);
@@ -790,7 +795,6 @@ public abstract class AccessibilityService extends Service {
public static final String KEY_ACCESSIBILITY_SCREENSHOT_TIMESTAMP =
"screenshot_timestamp";
-
/**
* Annotations for result codes of attaching accessibility overlays.
*
@@ -837,6 +841,13 @@ public abstract class AccessibilityService extends Service {
private WindowManager mWindowManager;
+ @GuardedBy("mLock")
+ private boolean mServiceConnected;
+ @GuardedBy("mLock")
+ private boolean mMagnificationSystemUIConnected;
+ @GuardedBy("mLock")
+ private boolean mServiceConnectedNotified;
+
/** List of magnification controllers, mapping from displayId -> MagnificationController. */
private final SparseArray<MagnificationController> mMagnificationControllers =
new SparseArray<>(0);
@@ -886,11 +897,14 @@ public abstract class AccessibilityService extends Service {
for (int i = 0; i < mMagnificationControllers.size(); i++) {
mMagnificationControllers.valueAt(i).onServiceConnectedLocked();
}
+ checkIsMagnificationSystemUIConnectedAlready();
final AccessibilityServiceInfo info = getServiceInfo();
if (info != null) {
updateInputMethod(info);
mMotionEventSources = info.getMotionEventSources();
}
+ mServiceConnected = true;
+ mServiceConnectedNotified = false;
}
if (mSoftKeyboardController != null) {
mSoftKeyboardController.onServiceConnected();
@@ -898,7 +912,57 @@ public abstract class AccessibilityService extends Service {
// The client gets to handle service connection last, after we've set
// up any state upon which their code may rely.
- onServiceConnected();
+ if (android.view.accessibility.Flags
+ .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) {
+ notifyOnServiceConnectedIfReady();
+ } else {
+ onServiceConnected();
+ }
+ }
+
+ private void notifyOnServiceConnectedIfReady() {
+ synchronized (mLock) {
+ if (mServiceConnectedNotified) {
+ return;
+ }
+ boolean canControlMagnification;
+ final AccessibilityServiceInfo info = getServiceInfo();
+ if (info != null) {
+ int flagMask = CAPABILITY_CAN_CONTROL_MAGNIFICATION;
+ canControlMagnification = (info.getCapabilities() & flagMask) == flagMask;
+ } else {
+ canControlMagnification = false;
+ }
+ boolean ready = canControlMagnification
+ ? (mServiceConnected && mMagnificationSystemUIConnected)
+ : mServiceConnected;
+ if (ready) {
+ getMainExecutor().execute(() -> onServiceConnected());
+ mServiceConnectedNotified = true;
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void checkIsMagnificationSystemUIConnectedAlready() {
+ if (!android.view.accessibility.Flags
+ .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) {
+ return;
+ }
+ if (mMagnificationSystemUIConnected) {
+ return;
+ }
+ final IAccessibilityServiceConnection connection =
+ AccessibilityInteractionClient.getInstance(this).getConnection(mConnectionId);
+ if (connection != null) {
+ try {
+ boolean connected = connection.isMagnificationSystemUIConnected();
+ mMagnificationSystemUIConnected = connected;
+ } catch (RemoteException re) {
+ Log.w(LOG_TAG, "Failed to check magnification system ui connection", re);
+ re.rethrowFromSystemServer();
+ }
+ }
}
private void updateInputMethod(AccessibilityServiceInfo info) {
@@ -1360,6 +1424,22 @@ public abstract class AccessibilityService extends Service {
}
}
+ private void onMagnificationSystemUIConnectionChanged(boolean connected) {
+ if (!android.view.accessibility.Flags
+ .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) {
+ return;
+ }
+
+ synchronized (mLock) {
+ boolean changed = (mMagnificationSystemUIConnected != connected);
+ mMagnificationSystemUIConnected = connected;
+
+ if (changed) {
+ notifyOnServiceConnectedIfReady();
+ }
+ }
+ }
+
private void onMagnificationChanged(int displayId, @NonNull Region region,
MagnificationConfig config) {
MagnificationController controller;
@@ -2823,6 +2903,11 @@ public abstract class AccessibilityService extends Service {
}
@Override
+ public void onMagnificationSystemUIConnectionChanged(boolean connected) {
+ AccessibilityService.this.onMagnificationSystemUIConnectionChanged(connected);
+ }
+
+ @Override
public void onMagnificationChanged(int displayId, @NonNull Region region,
MagnificationConfig config) {
AccessibilityService.this.onMagnificationChanged(displayId, region, config);
@@ -3032,6 +3117,16 @@ public abstract class AccessibilityService extends Service {
});
}
+ @Override
+ public void onMagnificationSystemUIConnectionChanged(boolean connected) {
+ mExecutor.execute(() -> {
+ if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+ mCallback.onMagnificationSystemUIConnectionChanged(connected);
+ }
+ return;
+ });
+ }
+
/** Magnification changed callbacks for different displays */
public void onMagnificationChanged(int displayId, @NonNull Region region,
MagnificationConfig config) {
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
index 3bc61e560d8c..f1479ef79dd9 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
@@ -48,6 +48,8 @@ import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
void onKeyEvent(in KeyEvent event, int sequence);
+ void onMagnificationSystemUIConnectionChanged(boolean connected);
+
void onMagnificationChanged(int displayId, in Region region, in MagnificationConfig config);
void onMotionEvent(in MotionEvent event);
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 713d8e5dd12f..149e7194a43b 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -130,6 +130,9 @@ interface IAccessibilityServiceConnection {
void setMagnificationCallbackEnabled(int displayId, boolean enabled);
@RequiresNoPermission
+ boolean isMagnificationSystemUIConnected();
+
+ @RequiresNoPermission
boolean setSoftKeyboardShowMode(int showMode);
@RequiresNoPermission
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index c0f723241c82..5956e2bde242 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -2617,6 +2617,9 @@ public class ApplicationPackageManager extends PackageManager {
try {
Objects.requireNonNull(packageName);
return mPM.isAppArchivable(packageName, new UserHandle(getUserId()));
+ } catch (ParcelableException e) {
+ e.maybeRethrow(NameNotFoundException.class);
+ throw new RuntimeException(e);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 329fb00c1d9b..fc3bb0288d67 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -114,7 +114,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ContrastColorUtil;
-import com.android.internal.util.NewlineNormalizer;
+import com.android.internal.util.NotificationBigTextNormalizer;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -3262,12 +3262,12 @@ public class Notification implements Parcelable
return cs.toString();
}
- private static CharSequence cleanUpNewLines(@Nullable CharSequence charSequence) {
+ private static CharSequence normalizeBigText(@Nullable CharSequence charSequence) {
if (charSequence == null) {
return charSequence;
}
- return NewlineNormalizer.normalizeNewlines(charSequence.toString());
+ return NotificationBigTextNormalizer.normalizeBigText(charSequence.toString());
}
private static CharSequence removeTextSizeSpans(CharSequence charSequence) {
@@ -8566,7 +8566,7 @@ public class Notification implements Parcelable
// Replace the text with the big text, but only if the big text is not empty.
CharSequence bigTextText = mBuilder.processLegacyText(mBigText);
if (Flags.cleanUpSpansAndNewLines()) {
- bigTextText = cleanUpNewLines(stripStyling(bigTextText));
+ bigTextText = normalizeBigText(stripStyling(bigTextText));
}
if (!TextUtils.isEmpty(bigTextText)) {
p.text(bigTextText);
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index 348d4d8fd809..273a79efb591 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -1969,6 +1969,11 @@ public final class UiAutomation {
}
@Override
+ public void onMagnificationSystemUIConnectionChanged(boolean connected) {
+ /* do nothing */
+ }
+
+ @Override
public void onMagnificationChanged(int displayId, @NonNull Region region,
MagnificationConfig config) {
/* do nothing */
diff --git a/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java b/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java
index ed18a057118c..eb31db18473f 100644
--- a/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java
+++ b/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java
@@ -27,6 +27,8 @@ import android.view.IWindow;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.Objects;
/**
@@ -38,7 +40,9 @@ public class WindowStateInsetsControlChangeItem extends WindowStateTransactionIt
private static final String TAG = "WindowStateInsetsControlChangeItem";
private InsetsState mInsetsState;
- private InsetsSourceControl.Array mActiveControls;
+
+ @VisibleForTesting
+ public InsetsSourceControl.Array mActiveControls;
@Override
public void execute(@NonNull ClientTransactionHandler client, @NonNull IWindow window,
@@ -51,6 +55,8 @@ public class WindowStateInsetsControlChangeItem extends WindowStateTransactionIt
// An exception could happen if the process is restarted. It is safe to ignore since
// the window should no longer exist.
Log.w(TAG, "The original window no longer exists in the new process", e);
+ // Prevent leak
+ mActiveControls.release();
}
Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
}
diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig
index 374be6fd2272..18cfca686107 100644
--- a/core/java/android/appwidget/flags.aconfig
+++ b/core/java/android/appwidget/flags.aconfig
@@ -40,3 +40,13 @@ flag {
description: "Throttle the widget view updates to mitigate transaction exceptions"
bug: "326145514"
}
+
+flag {
+ name: "support_resume_restore_after_reboot"
+ namespace: "app_widgets"
+ description: "Enable support for resume restore widget after reboot by persisting intermediate states to disk"
+ bug: "336976070"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index ed5d66227574..1e7815329f3b 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -64,3 +64,13 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "virtual_devices"
+ name: "intent_interception_action_matching_fix"
+ description: "Do not match intents without actions if the filter has actions"
+ bug: "343805037"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 270fc32a4e32..bbd0e9ff4738 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -2431,6 +2431,7 @@ public class PackageInstaller {
statusReceiver, new UserHandle(mUserId));
} catch (ParcelableException e) {
e.maybeRethrow(PackageManager.NameNotFoundException.class);
+ throw new RuntimeException(e);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2467,6 +2468,7 @@ public class PackageInstaller {
} catch (ParcelableException e) {
e.maybeRethrow(IOException.class);
e.maybeRethrow(PackageManager.NameNotFoundException.class);
+ throw new RuntimeException(e);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2499,6 +2501,7 @@ public class PackageInstaller {
userActionIntent, new UserHandle(mUserId));
} catch (ParcelableException e) {
e.maybeRethrow(PackageManager.NameNotFoundException.class);
+ throw new RuntimeException(e);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index c674968bba8a..0dec13ff0c02 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -18,9 +18,10 @@ package android.text;
import static com.android.graphics.hwui.flags.Flags.highContrastTextLuminance;
import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
-import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
+import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
+import android.annotation.ColorInt;
import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.IntDef;
@@ -398,6 +399,20 @@ public abstract class Layout {
mUseBoundsForWidth = useBoundsForWidth;
mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang;
mMinimumFontMetrics = minimumFontMetrics;
+
+ initSpanColors();
+ }
+
+ private void initSpanColors() {
+ if (mSpannedText && Flags.highContrastTextSmallTextRect()) {
+ if (mSpanColors == null) {
+ mSpanColors = new SpanColors();
+ } else {
+ mSpanColors.recycle();
+ }
+ } else {
+ mSpanColors = null;
+ }
}
/**
@@ -417,6 +432,7 @@ public abstract class Layout {
mSpacingMult = spacingmult;
mSpacingAdd = spacingadd;
mSpannedText = text instanceof Spanned;
+ initSpanColors();
}
/**
@@ -643,20 +659,20 @@ public abstract class Layout {
return null;
}
- return isHighContrastTextDark() ? BlendMode.MULTIPLY : BlendMode.DIFFERENCE;
+ return isHighContrastTextDark(mPaint.getColor()) ? BlendMode.MULTIPLY
+ : BlendMode.DIFFERENCE;
}
- private boolean isHighContrastTextDark() {
+ private boolean isHighContrastTextDark(@ColorInt int color) {
// High-contrast text mode
// Determine if the text is black-on-white or white-on-black, so we know what blendmode will
// give the highest contrast and most realistic text color.
// This equation should match the one in libs/hwui/hwui/DrawTextFunctor.h
if (highContrastTextLuminance()) {
var lab = new double[3];
- ColorUtils.colorToLAB(mPaint.getColor(), lab);
- return lab[0] < 0.5;
+ ColorUtils.colorToLAB(color, lab);
+ return lab[0] < 50.0;
} else {
- var color = mPaint.getColor();
int channelSum = Color.red(color) + Color.green(color) + Color.blue(color);
return channelSum < (128 * 3);
}
@@ -1010,15 +1026,22 @@ public abstract class Layout {
var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX,
mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR);
+ var originalTextColor = mPaint.getColor();
var bgPaint = mWorkPlainPaint;
bgPaint.reset();
- bgPaint.setColor(isHighContrastTextDark() ? Color.WHITE : Color.BLACK);
+ bgPaint.setColor(isHighContrastTextDark(originalTextColor) ? Color.WHITE : Color.BLACK);
bgPaint.setStyle(Paint.Style.FILL);
int start = getLineStart(firstLine);
int end = getLineEnd(lastLine);
// Draw a separate background rectangle for each line of text, that only surrounds the
- // characters on that line.
+ // characters on that line. But we also have to check the text color for each character, and
+ // make sure we are drawing the correct contrasting background. This is because Spans can
+ // change colors throughout the text and we'll need to match our backgrounds.
+ if (mSpannedText && mSpanColors != null) {
+ mSpanColors.init(mWorkPaint, ((Spanned) mText), start, end);
+ }
+
forEachCharacterBounds(
start,
end,
@@ -1028,13 +1051,24 @@ public abstract class Layout {
int mLastLineNum = -1;
final RectF mLineBackground = new RectF();
+ @ColorInt int mLastColor = originalTextColor;
+
@Override
public void onCharacterBounds(int index, int lineNum, float left, float top,
float right, float bottom) {
- if (lineNum != mLastLineNum) {
+
+ var newBackground = determineContrastingBackgroundColor(index);
+ var hasBgColorChanged = newBackground != bgPaint.getColor();
+
+ if (lineNum != mLastLineNum || hasBgColorChanged) {
+ // Draw what we have so far, then reset the rect and update its color
drawRect();
mLineBackground.set(left, top, right, bottom);
mLastLineNum = lineNum;
+
+ if (hasBgColorChanged) {
+ bgPaint.setColor(newBackground);
+ }
} else {
mLineBackground.union(left, top, right, bottom);
}
@@ -1051,8 +1085,36 @@ public abstract class Layout {
canvas.drawRect(mLineBackground, bgPaint);
}
}
+
+ private int determineContrastingBackgroundColor(int index) {
+ if (!mSpannedText || mSpanColors == null) {
+ // The text is not Spanned. it's all one color.
+ return bgPaint.getColor();
+ }
+
+ // Sometimes the color will change, but not enough to warrant a background
+ // color change. e.g. from black to dark grey still gets clamped to black,
+ // so the background stays white and we don't need to draw a fresh
+ // background.
+ var textColor = mSpanColors.getColorAt(index);
+ if (textColor == SpanColors.NO_COLOR_FOUND) {
+ textColor = originalTextColor;
+ }
+ var hasColorChanged = textColor != mLastColor;
+ if (hasColorChanged) {
+ mLastColor = textColor;
+
+ return isHighContrastTextDark(textColor) ? Color.WHITE : Color.BLACK;
+ }
+
+ return bgPaint.getColor();
+ }
}
);
+
+ if (mSpanColors != null) {
+ mSpanColors.recycle();
+ }
}
/**
@@ -3580,6 +3642,7 @@ public abstract class Layout {
private float mSpacingAdd;
private static final Rect sTempRect = new Rect();
private boolean mSpannedText;
+ @Nullable private SpanColors mSpanColors;
private TextDirectionHeuristic mTextDir;
private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
private boolean mIncludePad;
diff --git a/core/java/android/text/SpanColors.java b/core/java/android/text/SpanColors.java
new file mode 100644
index 000000000000..fcd242b62700
--- /dev/null
+++ b/core/java/android/text/SpanColors.java
@@ -0,0 +1,89 @@
+/*
+ * 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.text;
+
+import android.annotation.ColorInt;
+import android.annotation.Nullable;
+import android.graphics.Color;
+import android.text.style.CharacterStyle;
+
+/**
+ * Finds the foreground text color for the given Spanned text so you can iterate through each color
+ * change.
+ *
+ * @hide
+ */
+public class SpanColors {
+ public static final @ColorInt int NO_COLOR_FOUND = Color.TRANSPARENT;
+
+ private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
+ new SpanSet<>(CharacterStyle.class);
+ @Nullable private TextPaint mWorkPaint;
+
+ public SpanColors() {}
+
+ /**
+ * Init for the given text
+ *
+ * @param workPaint A temporary TextPaint object that will be used to calculate the colors. The
+ * paint properties will be mutated on calls to {@link #getColorAt(int)} so
+ * make sure to reset it before you use it for something else.
+ * @param spanned the text to examine
+ * @param start index to start at
+ * @param end index of the end
+ */
+ public void init(TextPaint workPaint, Spanned spanned, int start, int end) {
+ mWorkPaint = workPaint;
+ mCharacterStyleSpanSet.init(spanned, start, end);
+ }
+
+ /**
+ * Removes all internal references to the spans to avoid memory leaks.
+ */
+ public void recycle() {
+ mWorkPaint = null;
+ mCharacterStyleSpanSet.recycle();
+ }
+
+ /**
+ * Calculates the foreground color of the text at the given character index.
+ *
+ * <p>You must call {@link #init(TextPaint, Spanned, int, int)} before calling this
+ */
+ public @ColorInt int getColorAt(int index) {
+ var finalColor = NO_COLOR_FOUND;
+ // Reset the paint so if we get a CharacterStyle that doesn't actually specify color,
+ // (like UnderlineSpan), we still return no color found.
+ mWorkPaint.setColor(finalColor);
+ for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
+ if ((index >= mCharacterStyleSpanSet.spanStarts[k])
+ && (index <= mCharacterStyleSpanSet.spanEnds[k])) {
+ final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
+ span.updateDrawState(mWorkPaint);
+
+ finalColor = calculateFinalColor(mWorkPaint);
+ }
+ }
+ return finalColor;
+ }
+
+ private @ColorInt int calculateFinalColor(TextPaint workPaint) {
+ // TODO: can we figure out what the getColorFilter() will do?
+ // if so, we also need to reset colorFilter before the loop in getColorAt()
+ return workPaint.getColor();
+ }
+}
diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index 588e9e0a3b12..487214c5c33a 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -305,6 +305,18 @@ public class InsetsSourceControl implements Parcelable {
return mControls;
}
+ /** Cleanup {@link SurfaceControl} stored in controls to prevent leak. */
+ public void release() {
+ if (mControls == null) {
+ return;
+ }
+ for (InsetsSourceControl control : mControls) {
+ if (control != null) {
+ control.release(SurfaceControl::release);
+ }
+ }
+ }
+
/** Sets the given flags to all controls. */
public void setParcelableFlags(int parcelableFlags) {
if (mControls == null) {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index b54e052cf538..496e8992fc41 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2294,12 +2294,8 @@ public final class ViewRootImpl implements ViewParent,
mInsetsController.onStateChanged(insetsState);
if (mAdded) {
mInsetsController.onControlsChanged(controls);
- } else if (controls != null) {
- for (InsetsSourceControl control : controls) {
- if (control != null) {
- control.release(SurfaceControl::release);
- }
- }
+ } else {
+ activeControls.release();
}
}
@@ -11306,6 +11302,9 @@ public final class ViewRootImpl implements ViewParent,
mIsFromTransactionItem = false;
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor == null) {
+ if (isFromInsetsControlChangeItem) {
+ activeControls.release();
+ }
return;
}
if (insetsState.isSourceOrDefaultVisible(ID_IME, Type.ime())) {
diff --git a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
index 12e08148a651..1fe8180aa7b2 100644
--- a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
+++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
@@ -302,6 +302,10 @@ public abstract class AccessibilityDisplayProxy {
}
@Override
+ public void onMagnificationSystemUIConnectionChanged(boolean connected) {
+ }
+
+ @Override
public void onMagnificationChanged(int displayId, @NonNull Region region,
MagnificationConfig config) {
}
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index ab7b2261dc17..edf33875b765 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -169,3 +169,13 @@ flag {
description: "Feature flag for declaring system pinch zoom opt-out apis"
bug: "315089687"
}
+
+flag {
+ name: "wait_magnification_system_ui_connection_to_notify_service_connected"
+ namespace: "accessibility"
+ description: "Decide whether AccessibilityService needs to wait until magnification system ui connection is ready to trigger onServiceConnected"
+ bug: "337800504"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/window/ClientWindowFrames.java b/core/java/android/window/ClientWindowFrames.java
index 1bd921b339f6..d5398e6268dc 100644
--- a/core/java/android/window/ClientWindowFrames.java
+++ b/core/java/android/window/ClientWindowFrames.java
@@ -56,7 +56,16 @@ public class ClientWindowFrames implements Parcelable {
public ClientWindowFrames() {
}
- public ClientWindowFrames(ClientWindowFrames other) {
+ public ClientWindowFrames(@NonNull ClientWindowFrames other) {
+ setTo(other);
+ }
+
+ private ClientWindowFrames(@NonNull Parcel in) {
+ readFromParcel(in);
+ }
+
+ /** Updates the current frames to the given frames. */
+ public void setTo(@NonNull ClientWindowFrames other) {
frame.set(other.frame);
displayFrame.set(other.displayFrame);
parentFrame.set(other.parentFrame);
@@ -67,10 +76,6 @@ public class ClientWindowFrames implements Parcelable {
compatScale = other.compatScale;
}
- private ClientWindowFrames(Parcel in) {
- readFromParcel(in);
- }
-
/** Needed for AIDL out parameters. */
public void readFromParcel(Parcel in) {
frame.readFromParcel(in);
diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java
index f928f509bdb6..4c8bad6d0aff 100644
--- a/core/java/android/window/SnapshotDrawerUtils.java
+++ b/core/java/android/window/SnapshotDrawerUtils.java
@@ -77,6 +77,14 @@ public class SnapshotDrawerUtils {
private static final String TAG = "SnapshotDrawerUtils";
/**
+ * Used to check if toolkitSetFrameRateReadOnly flag is enabled
+ *
+ * @hide
+ */
+ private static boolean sToolkitSetFrameRateReadOnlyFlagValue =
+ android.view.flags.Flags.toolkitSetFrameRateReadOnly();
+
+ /**
* When creating the starting window, we use the exact same layout flags such that we end up
* with a window with the exact same dimensions etc. However, these flags are not used in layout
* and might cause other side effects so we exclude them.
@@ -439,6 +447,9 @@ public class SnapshotDrawerUtils {
layoutParams.setFitInsetsTypes(attrs.getFitInsetsTypes());
layoutParams.setFitInsetsSides(attrs.getFitInsetsSides());
layoutParams.setFitInsetsIgnoringVisibility(attrs.isFitInsetsIgnoringVisibility());
+ if (sToolkitSetFrameRateReadOnlyFlagValue) {
+ layoutParams.setFrameRatePowerSavingsBalanced(false);
+ }
layoutParams.setTitle(title);
layoutParams.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL;
diff --git a/core/java/com/android/internal/util/NewlineNormalizer.java b/core/java/com/android/internal/util/NewlineNormalizer.java
deleted file mode 100644
index 0104d1f56f83..000000000000
--- a/core/java/com/android/internal/util/NewlineNormalizer.java
+++ /dev/null
@@ -1,39 +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.internal.util;
-
-
-import java.util.regex.Pattern;
-
-/**
- * Utility class that replaces consecutive empty lines with single new line.
- * @hide
- */
-public class NewlineNormalizer {
-
- private static final Pattern MULTIPLE_NEWLINES = Pattern.compile("\\v(\\s*\\v)?");
-
- // Private constructor to prevent instantiation
- private NewlineNormalizer() {}
-
- /**
- * Replaces consecutive newlines with a single newline in the input text.
- */
- public static String normalizeNewlines(String text) {
- return MULTIPLE_NEWLINES.matcher(text).replaceAll("\n");
- }
-}
diff --git a/core/java/com/android/internal/util/NotificationBigTextNormalizer.java b/core/java/com/android/internal/util/NotificationBigTextNormalizer.java
new file mode 100644
index 000000000000..80d409500ef0
--- /dev/null
+++ b/core/java/com/android/internal/util/NotificationBigTextNormalizer.java
@@ -0,0 +1,123 @@
+/*
+ * 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.internal.util;
+
+
+import android.annotation.NonNull;
+import android.os.Trace;
+
+import java.util.regex.Pattern;
+
+/**
+ * Utility class that normalizes BigText style Notification content.
+ * @hide
+ */
+public class NotificationBigTextNormalizer {
+
+ private static final Pattern MULTIPLE_NEWLINES = Pattern.compile("\\v(\\s*\\v)?");
+ private static final Pattern HORIZONTAL_WHITESPACES = Pattern.compile("\\h+");
+
+ // Private constructor to prevent instantiation
+ private NotificationBigTextNormalizer() {}
+
+ /**
+ * Normalizes the given text by collapsing consecutive new lines into single one and cleaning
+ * up each line by removing zero-width characters, invisible formatting characters, and
+ * collapsing consecutive whitespace into single space.
+ */
+ @NonNull
+ public static String normalizeBigText(@NonNull String text) {
+ try {
+ Trace.beginSection("NotifBigTextNormalizer#normalizeBigText");
+ text = MULTIPLE_NEWLINES.matcher(text).replaceAll("\n");
+ text = HORIZONTAL_WHITESPACES.matcher(text).replaceAll(" ");
+ text = normalizeLines(text);
+ return text;
+ } finally {
+ Trace.endSection();
+ }
+ }
+
+ /**
+ * Normalizes lines in a text by removing zero-width characters, invisible formatting
+ * characters, and collapsing consecutive whitespace into single space.
+ *
+ * <p>
+ * This method processes the input text line by line. It eliminates zero-width
+ * characters (U+200B to U+200D, U+FEFF, U+034F), invisible formatting
+ * characters (U+2060 to U+2065, U+206A to U+206F, U+FFF9 to U+FFFB),
+ * and replaces any sequence of consecutive whitespace characters with a single space.
+ * </p>
+ *
+ * <p>
+ * Additionally, the method trims trailing whitespace from each line and removes any
+ * resulting empty lines.
+ * </p>
+ */
+ @NonNull
+ private static String normalizeLines(@NonNull String text) {
+ String[] lines = text.split("\n");
+ final StringBuilder textSB = new StringBuilder(text.length());
+ for (int i = 0; i < lines.length; i++) {
+ final String line = lines[i];
+ final StringBuilder lineSB = new StringBuilder(line.length());
+ boolean spaceSeen = false;
+ for (int j = 0; j < line.length(); j++) {
+ final char character = line.charAt(j);
+
+ // Skip ZERO WIDTH characters
+ if ((character >= '\u200B' && character <= '\u200D')
+ || character == '\uFEFF' || character == '\u034F') {
+ continue;
+ }
+ // Skip INVISIBLE_FORMATTING_CHARACTERS
+ if ((character >= '\u2060' && character <= '\u2065')
+ || (character >= '\u206A' && character <= '\u206F')
+ || (character >= '\uFFF9' && character <= '\uFFFB')) {
+ continue;
+ }
+
+ if (isSpace(character)) {
+ // eliminate consecutive spaces....
+ if (!spaceSeen) {
+ lineSB.append(" ");
+ }
+ spaceSeen = true;
+ } else {
+ spaceSeen = false;
+ lineSB.append(character);
+ }
+ }
+ // trim line.
+ final String currentLine = lineSB.toString().trim();
+
+ // don't add empty lines after trim.
+ if (currentLine.length() > 0) {
+ if (textSB.length() > 0) {
+ textSB.append("\n");
+ }
+ textSB.append(currentLine);
+ }
+ }
+
+ return textSB.toString();
+ }
+
+ private static boolean isSpace(char ch) {
+ return ch != '\n' && Character.isSpaceChar(ch);
+ }
+}
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index d48cdc4645c6..eaff7608ce3b 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -713,6 +713,19 @@ android_media_AudioSystem_getForceUse(JNIEnv *env, jobject thiz, jint usage)
AudioSystem::getForceUse(static_cast<audio_policy_force_use_t>(usage)));
}
+static jint android_media_AudioSystem_setDeviceAbsoluteVolumeEnabled(JNIEnv *env, jobject thiz,
+ jint device, jstring address,
+ jboolean enabled,
+ jint stream) {
+ const char *c_address = env->GetStringUTFChars(address, nullptr);
+ int state = check_AudioSystem_Command(
+ AudioSystem::setDeviceAbsoluteVolumeEnabled(static_cast<audio_devices_t>(device),
+ c_address, enabled,
+ static_cast<audio_stream_type_t>(stream)));
+ env->ReleaseStringUTFChars(address, c_address);
+ return state;
+}
+
static jint
android_media_AudioSystem_initStreamVolume(JNIEnv *env, jobject thiz, jint stream, jint indexMin, jint indexMax)
{
@@ -3373,6 +3386,7 @@ static const JNINativeMethod gMethods[] =
MAKE_AUDIO_SYSTEM_METHOD(setPhoneState),
MAKE_AUDIO_SYSTEM_METHOD(setForceUse),
MAKE_AUDIO_SYSTEM_METHOD(getForceUse),
+ MAKE_AUDIO_SYSTEM_METHOD(setDeviceAbsoluteVolumeEnabled),
MAKE_AUDIO_SYSTEM_METHOD(initStreamVolume),
MAKE_AUDIO_SYSTEM_METHOD(setStreamVolumeIndex),
MAKE_AUDIO_SYSTEM_METHOD(getStreamVolumeIndex),
diff --git a/core/proto/android/app/appexitinfo.proto b/core/proto/android/app/appexitinfo.proto
index 3abc462671a2..e560a944b94b 100644
--- a/core/proto/android/app/appexitinfo.proto
+++ b/core/proto/android/app/appexitinfo.proto
@@ -20,7 +20,7 @@ option java_multiple_files = true;
package android.app;
import "frameworks/base/core/proto/android/privacy.proto";
-import "frameworks/proto_logging/stats/enums/app/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/app_enums.proto";
/**
* An android.app.ApplicationExitInfo object.
diff --git a/core/proto/android/app/appstartinfo.proto b/core/proto/android/app/appstartinfo.proto
index d9ed911515ba..c13753343ba8 100644
--- a/core/proto/android/app/appstartinfo.proto
+++ b/core/proto/android/app/appstartinfo.proto
@@ -20,7 +20,7 @@ option java_multiple_files = true;
package android.app;
import "frameworks/base/core/proto/android/privacy.proto";
-import "frameworks/proto_logging/stats/enums/app/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/app_enums.proto";
/**
* An android.app.ApplicationStartInfo object.
diff --git a/core/proto/android/os/batterystats.proto b/core/proto/android/os/batterystats.proto
index 4c84944a7382..97f81484b84d 100644
--- a/core/proto/android/os/batterystats.proto
+++ b/core/proto/android/os/batterystats.proto
@@ -21,7 +21,7 @@ package android.os;
import "frameworks/base/core/proto/android/os/powermanager.proto";
import "frameworks/base/core/proto/android/privacy.proto";
-import "frameworks/proto_logging/stats/enums/app/job/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/job/job_enums.proto";
import "frameworks/proto_logging/stats/enums/telephony/enums.proto";
message BatteryStatsProto {
diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto
index e3a438da5abc..921c41c8e52b 100644
--- a/core/proto/android/server/activitymanagerservice.proto
+++ b/core/proto/android/server/activitymanagerservice.proto
@@ -35,7 +35,7 @@ import "frameworks/base/core/proto/android/server/intentresolver.proto";
import "frameworks/base/core/proto/android/server/windowmanagerservice.proto";
import "frameworks/base/core/proto/android/util/common.proto";
import "frameworks/base/core/proto/android/privacy.proto";
-import "frameworks/proto_logging/stats/enums/app/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/app_enums.proto";
option java_multiple_files = true;
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 00127c134ce6..a1e3dc1b9b4e 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -31,7 +31,7 @@ import "frameworks/base/core/proto/android/server/appstatetracker.proto";
import "frameworks/base/core/proto/android/server/statlogger.proto";
import "frameworks/base/core/proto/android/privacy.proto";
import "frameworks/base/core/proto/android/util/quotatracker.proto";
-import "frameworks/proto_logging/stats/enums/app/job/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/job/job_enums.proto";
import "frameworks/proto_logging/stats/enums/server/job/enums.proto";
message JobSchedulerServiceDumpProto {
diff --git a/core/proto/android/server/powermanagerservice.proto b/core/proto/android/server/powermanagerservice.proto
index 2f865afd28c7..593bbc6f5d0d 100644
--- a/core/proto/android/server/powermanagerservice.proto
+++ b/core/proto/android/server/powermanagerservice.proto
@@ -26,7 +26,7 @@ import "frameworks/base/core/proto/android/os/worksource.proto";
import "frameworks/base/core/proto/android/providers/settings.proto";
import "frameworks/base/core/proto/android/server/wirelesschargerdetector.proto";
import "frameworks/base/core/proto/android/privacy.proto";
-import "frameworks/proto_logging/stats/enums/app/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/app_enums.proto";
import "frameworks/proto_logging/stats/enums/os/enums.proto";
import "frameworks/proto_logging/stats/enums/view/enums.proto";
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
index 3735274c1a6c..c7060adc1ca1 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
@@ -23,6 +23,8 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.app.Activity;
@@ -39,7 +41,6 @@ import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
-import android.window.WindowContext;
import android.window.WindowContextInfo;
import androidx.test.filters.SmallTest;
@@ -73,8 +74,6 @@ public class ClientTransactionItemTest {
@Mock
private IBinder mWindowClientToken;
@Mock
- private WindowContext mWindowContext;
- @Mock
private IWindow mWindow;
// Can't mock final class.
@@ -176,4 +175,17 @@ public class ClientTransactionItemTest {
verify(mWindow).insetsControlChanged(mInsetsState, mActiveControls);
}
+
+ @Test
+ public void testWindowStateInsetsControlChangeItem_executeError() throws RemoteException {
+ doThrow(new RemoteException()).when(mWindow).insetsControlChanged(any(), any());
+
+ mActiveControls = spy(mActiveControls);
+ final WindowStateInsetsControlChangeItem item = WindowStateInsetsControlChangeItem.obtain(
+ mWindow, mInsetsState, mActiveControls);
+ item.mActiveControls = mActiveControls;
+ item.execute(mHandler, mPendingActions);
+
+ verify(mActiveControls).release();
+ }
}
diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java
index 1c1236279b61..98f8b7fc897c 100644
--- a/core/tests/coretests/src/android/text/LayoutTest.java
+++ b/core/tests/coretests/src/android/text/LayoutTest.java
@@ -39,6 +39,7 @@ import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.text.Layout.Alignment;
+import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import androidx.test.filters.SmallTest;
@@ -933,6 +934,83 @@ public class LayoutTest {
expect.that(numBackgroundsFound).isEqualTo(backgroundRectsDrawn);
}
+ @Test
+ @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public void highContrastTextEnabled_testDrawMulticolorText_drawsBlackAndWhiteBackgrounds() {
+ /*
+ Here's what the final render should look like:
+
+ Text | Background
+ ========================
+ al | BW
+ w | WW
+ ei | WW
+ \t; | WW
+ s | BB
+ df | BB
+ s | BB
+ df | BB
+ @ | BB
+ ------------------------
+ */
+
+ mTextPaint.setColor(Color.WHITE);
+
+ mSpannedText.setSpan(
+ // Can't use DKGREY because it is right on the cusp of clamping white
+ new ForegroundColorSpan(0xFF332211),
+ /* start= */ 1,
+ /* end= */ 6,
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+ );
+ mSpannedText.setSpan(
+ new ForegroundColorSpan(Color.LTGRAY),
+ /* start= */ 8,
+ /* end= */ 11,
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+ );
+ Layout layout = new StaticLayout(mSpannedText, mTextPaint, mWidth,
+ mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
+
+ final int width = 256;
+ final int height = 256;
+ MockCanvas c = new MockCanvas(width, height);
+ c.setHighContrastTextEnabled(true);
+ layout.draw(
+ c,
+ /* highlightPaths= */ null,
+ /* highlightPaints= */ null,
+ /* selectionPath= */ null,
+ /* selectionPaint= */ null,
+ /* cursorOffsetVertical= */ 0
+ );
+ List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
+ var highlightsDrawn = 0;
+ var numColorChangesWithinOneLine = 1;
+ var textsDrawn = STATIC_LINE_COUNT + numColorChangesWithinOneLine;
+ var backgroundRectsDrawn = STATIC_LINE_COUNT + numColorChangesWithinOneLine;
+ expect.withMessage("wrong number of drawCommands: " + drawCommands)
+ .that(drawCommands.size())
+ .isEqualTo(textsDrawn + backgroundRectsDrawn + highlightsDrawn);
+
+ var backgroundCommands = drawCommands.stream()
+ .filter(it -> it.rect != null)
+ .toList();
+
+ expect.that(backgroundCommands.get(0).paint.getColor()).isEqualTo(Color.BLACK);
+ expect.that(backgroundCommands.get(1).paint.getColor()).isEqualTo(Color.WHITE);
+ expect.that(backgroundCommands.get(2).paint.getColor()).isEqualTo(Color.WHITE);
+ expect.that(backgroundCommands.get(3).paint.getColor()).isEqualTo(Color.WHITE);
+ expect.that(backgroundCommands.get(4).paint.getColor()).isEqualTo(Color.WHITE);
+ expect.that(backgroundCommands.get(5).paint.getColor()).isEqualTo(Color.BLACK);
+ expect.that(backgroundCommands.get(6).paint.getColor()).isEqualTo(Color.BLACK);
+ expect.that(backgroundCommands.get(7).paint.getColor()).isEqualTo(Color.BLACK);
+ expect.that(backgroundCommands.get(8).paint.getColor()).isEqualTo(Color.BLACK);
+ expect.that(backgroundCommands.get(9).paint.getColor()).isEqualTo(Color.BLACK);
+
+ expect.that(backgroundCommands.size()).isEqualTo(backgroundRectsDrawn);
+ }
+
private static final class MockCanvas extends Canvas {
static class DrawCommand {
diff --git a/core/tests/coretests/src/android/text/SpanColorsTest.java b/core/tests/coretests/src/android/text/SpanColorsTest.java
new file mode 100644
index 000000000000..3d8d8f9c126d
--- /dev/null
+++ b/core/tests/coretests/src/android/text/SpanColorsTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.text;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Color;
+import android.graphics.drawable.ShapeDrawable;
+import android.platform.test.annotations.Presubmit;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.text.style.UnderlineSpan;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SpanColorsTest {
+ private final TextPaint mWorkPaint = new TextPaint();
+ private SpanColors mSpanColors;
+ private SpannableString mSpannedText;
+
+ @Before
+ public void setup() {
+ mSpanColors = new SpanColors();
+ mSpannedText = new SpannableString("Hello world! This is a test.");
+ mSpannedText.setSpan(new ForegroundColorSpan(Color.RED), 0, 4,
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ mSpannedText.setSpan(new ForegroundColorSpan(Color.GREEN), 6, 11,
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ mSpannedText.setSpan(new UnderlineSpan(), 5, 10, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ mSpannedText.setSpan(new ImageSpan(new ShapeDrawable()), 1, 2,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mSpannedText.setSpan(new ForegroundColorSpan(Color.BLUE), 12, 16,
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+
+ @Test
+ public void testNoColorFound() {
+ mSpanColors.init(mWorkPaint, mSpannedText, 25, 30); // Beyond the spans
+ assertThat(mSpanColors.getColorAt(27)).isEqualTo(SpanColors.NO_COLOR_FOUND);
+ }
+
+ @Test
+ public void testSingleColorSpan() {
+ mSpanColors.init(mWorkPaint, mSpannedText, 1, 4);
+ assertThat(mSpanColors.getColorAt(3)).isEqualTo(Color.RED);
+ }
+
+ @Test
+ public void testMultipleColorSpans() {
+ mSpanColors.init(mWorkPaint, mSpannedText, 0, mSpannedText.length());
+ assertThat(mSpanColors.getColorAt(2)).isEqualTo(Color.RED);
+ assertThat(mSpanColors.getColorAt(5)).isEqualTo(SpanColors.NO_COLOR_FOUND);
+ assertThat(mSpanColors.getColorAt(8)).isEqualTo(Color.GREEN);
+ assertThat(mSpanColors.getColorAt(13)).isEqualTo(Color.BLUE);
+ }
+}
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
index b5c264c4ae5e..5a6824bf0d7e 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
@@ -146,6 +146,10 @@ public class AccessibilityServiceConnectionImpl extends IAccessibilityServiceCon
public void setMagnificationCallbackEnabled(int displayId, boolean enabled) {}
+ public boolean isMagnificationSystemUIConnected() {
+ return false;
+ }
+
public boolean setSoftKeyboardShowMode(int showMode) {
return false;
}
diff --git a/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java b/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java
deleted file mode 100644
index bcdac610a49d..000000000000
--- a/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java
+++ /dev/null
@@ -1,71 +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.internal.util;
-
-import static junit.framework.Assert.assertEquals;
-
-
-import android.platform.test.annotations.DisabledOnRavenwood;
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test for {@link NewlineNormalizer}
- * @hide
- */
-@DisabledOnRavenwood(blockedBy = NewlineNormalizer.class)
-@RunWith(AndroidJUnit4.class)
-public class NewlineNormalizerTest {
-
- @Rule
- public final RavenwoodRule mRavenwood = new RavenwoodRule();
-
- @Test
- public void testEmptyInput() {
- assertEquals("", NewlineNormalizer.normalizeNewlines(""));
- }
-
- @Test
- public void testSingleNewline() {
- assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n"));
- }
-
- @Test
- public void testMultipleConsecutiveNewlines() {
- assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n\n\n\n\n"));
- }
-
- @Test
- public void testNewlinesWithSpacesAndTabs() {
- String input = "Line 1\n \n \t \n\tLine 2";
- // Adjusted expected output to include the tab character
- String expected = "Line 1\n\tLine 2";
- assertEquals(expected, NewlineNormalizer.normalizeNewlines(input));
- }
-
- @Test
- public void testMixedNewlineCharacters() {
- String input = "Line 1\r\nLine 2\u000BLine 3\fLine 4\u2028Line 5\u2029Line 6";
- String expected = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6";
- assertEquals(expected, NewlineNormalizer.normalizeNewlines(input));
- }
-}
diff --git a/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java b/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java
new file mode 100644
index 000000000000..1f2e24aa8c68
--- /dev/null
+++ b/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.internal.util;
+
+import static junit.framework.Assert.assertEquals;
+
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for {@link NotificationBigTextNormalizer}
+ * @hide
+ */
+@DisabledOnRavenwood(blockedBy = NotificationBigTextNormalizer.class)
+@RunWith(AndroidJUnit4.class)
+public class NotificationBigTextNormalizerTest {
+
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+
+ @Test
+ public void testEmptyInput() {
+ assertEquals("", NotificationBigTextNormalizer.normalizeBigText(""));
+ }
+
+ @Test
+ public void testSingleNewline() {
+ assertEquals("", NotificationBigTextNormalizer.normalizeBigText("\n"));
+ }
+
+ @Test
+ public void testMultipleConsecutiveNewlines() {
+ assertEquals("", NotificationBigTextNormalizer.normalizeBigText("\n\n\n\n\n"));
+ }
+
+ @Test
+ public void testNewlinesWithSpacesAndTabs() {
+ String input = "Line 1\n \n \t \n\tLine 2";
+ // Adjusted expected output to include the tab character
+ String expected = "Line 1\nLine 2";
+ assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input));
+ }
+
+ @Test
+ public void testMixedNewlineCharacters() {
+ String input = "Line 1\r\nLine 2\u000BLine 3\fLine 4\u2028Line 5\u2029Line 6";
+ String expected = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6";
+ assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input));
+ }
+
+ @Test
+ public void testConsecutiveSpaces() {
+ // Only spaces
+ assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This"
+ + " is a test."));
+ // Zero width characters bw spaces.
+ assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This"
+ + "\u200B \u200B \u200B \u200B \u200B \u200B \u200B \u200Bis\uFEFF \uFEFF \uFEFF"
+ + " \uFEFFa \u034F \u034F \u034F \u034F \u034F \u034Ftest."));
+
+ // Invisible formatting characters bw spaces.
+ assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This"
+ + "\u2061 \u2061 \u2061 \u2061 \u2061 \u2061 \u2061 \u2061is\u206E \u206E \u206E"
+ + " \u206Ea \uFFFB \uFFFB \uFFFB \uFFFB \uFFFB \uFFFBtest."));
+ // Non breakable spaces
+ assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This"
+ + "\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0is\u2005 \u2005 \u2005"
+ + " \u2005a\u2005\u2005\u2005 \u2005\u2005\u2005test."));
+ }
+
+ @Test
+ public void testZeroWidthCharRemoval() {
+ // Test each character individually
+ char[] zeroWidthChars = { '\u200B', '\u200C', '\u200D', '\uFEFF', '\u034F' };
+
+ for (char c : zeroWidthChars) {
+ String input = "Test" + c + "string";
+ String expected = "Teststring";
+ assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input));
+ }
+ }
+
+ @Test
+ public void testWhitespaceReplacement() {
+ assertEquals("This text has horizontal whitespace.",
+ NotificationBigTextNormalizer.normalizeBigText(
+ "This\ttext\thas\thorizontal\twhitespace."));
+ assertEquals("This text has mixed whitespace.",
+ NotificationBigTextNormalizer.normalizeBigText(
+ "This text has \u00A0 mixed\u2009whitespace."));
+ assertEquals("This text has leading and trailing whitespace.",
+ NotificationBigTextNormalizer.normalizeBigText(
+ "\t This text has leading and trailing whitespace. \n"));
+ }
+
+ @Test
+ public void testInvisibleFormattingCharacterRemoval() {
+ // Test each character individually
+ char[] invisibleFormattingChars = {
+ '\u2060', '\u2061', '\u2062', '\u2063', '\u2064', '\u2065',
+ '\u206A', '\u206B', '\u206C', '\u206D', '\u206E', '\u206F',
+ '\uFFF9', '\uFFFA', '\uFFFB'
+ };
+
+ for (char c : invisibleFormattingChars) {
+ String input = "Test " + c + "string";
+ String expected = "Test string";
+ assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input));
+ }
+ }
+ @Test
+ public void testNonBreakSpaceReplacement() {
+ // Test each character individually
+ char[] nonBreakSpaces = {
+ '\u00A0', '\u1680', '\u2000', '\u2001', '\u2002',
+ '\u2003', '\u2004', '\u2005', '\u2006', '\u2007',
+ '\u2008', '\u2009', '\u200A', '\u202F', '\u205F', '\u3000'
+ };
+
+ for (char c : nonBreakSpaces) {
+ String input = "Test" + c + "string";
+ String expected = "Test string";
+ assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input));
+ }
+ }
+}
diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
index d359a9050a0f..3cff91597939 100644
--- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java
+++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
@@ -1149,6 +1149,8 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu
/**
* Sets the serial number used for the certificate of the generated key pair.
+ * To ensure compatibility with devices and certificate parsers, the value
+ * should be 20 bytes or shorter (see RFC 5280 section 4.1.2.2).
*
* <p>By default, the serial number is {@code 1}.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 6224543516fa..6ade81c0f3a1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -1591,7 +1591,7 @@ public class Transitions implements RemoteCallable<Transitions>,
public void setHomeTransitionListener(IHomeTransitionListener listener) {
executeRemoteCallWithTaskPermission(mTransitions, "setHomeTransitionListener",
(transitions) -> {
- transitions.mHomeTransitionObserver.setHomeTransitionListener(mTransitions,
+ transitions.mHomeTransitionObserver.setHomeTransitionListener(transitions,
listener);
});
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 6901e75019bc..37cdbb47bfe8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -583,17 +583,20 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
} else if (ev.getAction() == ACTION_HOVER_MOVE
&& MaximizeMenu.Companion.isMaximizeMenuView(id)) {
decoration.onMaximizeMenuHoverMove(id, ev);
+ mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable);
} else if (ev.getAction() == ACTION_HOVER_EXIT) {
if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) {
decoration.onMaximizeWindowHoverExit();
- } else if (id == R.id.maximize_window || id == R.id.maximize_menu) {
+ } else if (id == R.id.maximize_window
+ || MaximizeMenu.Companion.isMaximizeMenuView(id)) {
// Close menu if not hovering over maximize menu or maximize button after a
// delay to give user a chance to re-enter view or to move from one maximize
// menu view to another.
mMainHandler.postDelayed(mCloseMaximizeWindowRunnable,
CLOSE_MAXIMIZE_MENU_DELAY_MS);
- } else if (MaximizeMenu.Companion.isMaximizeMenuView(id)) {
- decoration.onMaximizeMenuHoverExit(id, ev);
+ if (id != R.id.maximize_window) {
+ decoration.onMaximizeMenuHoverExit(id, ev);
+ }
}
return true;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
index 78f0ef7af45c..4f049015af99 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
@@ -88,7 +88,7 @@ class MaximizeButtonView(
}
fun cancelHoverAnimation() {
- hoverProgressAnimatorSet.removeAllListeners()
+ hoverProgressAnimatorSet.childAnimations.forEach { it.removeAllListeners() }
hoverProgressAnimatorSet.cancel()
progressBar.visibility = View.INVISIBLE
}
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 293c561f166c..d148afd3c15d 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -1764,6 +1764,10 @@ public class AudioSystem
public static native int getForceUse(int usage);
/** @hide */
@UnsupportedAppUsage
+ public static native int setDeviceAbsoluteVolumeEnabled(int nativeDeviceType,
+ @NonNull String address, boolean enabled, int streamToDriveAbs);
+ /** @hide */
+ @UnsupportedAppUsage
public static native int initStreamVolume(int stream, int indexMin, int indexMax);
@UnsupportedAppUsage
private static native int setStreamVolumeIndex(int stream, int index, int device);
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index a33e2252019b..055ccbced58a 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -27,6 +27,7 @@ package android.nfc {
field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String ACTION_REQUIRE_UNLOCK_FOR_NFC = "android.nfc.action.REQUIRE_UNLOCK_FOR_NFC";
field @FlaggedApi("android.nfc.enable_nfc_mainline") @RequiresPermission(android.Manifest.permission.SHOW_CUSTOMIZED_RESOLVER) public static final String ACTION_SHOW_NFC_RESOLVER = "android.nfc.action.SHOW_NFC_RESOLVER";
field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String EXTRA_RESOLVE_INFOS = "android.nfc.extra.RESOLVE_INFOS";
+ field @FlaggedApi("android.nfc.nfc_set_default_disc_tech") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static final int FLAG_SET_DEFAULT_TECH = 1073741824; // 0x40000000
field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int MESSAGE_TYPE_COMMAND = 1; // 0x1
field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_FAILED = 3; // 0x3
field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_MESSAGE_CORRUPTED = 2; // 0x2
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index 698df28129be..1dfc81e2108e 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -340,7 +340,8 @@ public final class NfcAdapter {
public static final int FLAG_READER_NFC_BARCODE = 0x10;
/** @hide */
- @IntDef(flag = true, prefix = {"FLAG_READER_"}, value = {
+ @IntDef(flag = true, value = {
+ FLAG_SET_DEFAULT_TECH,
FLAG_READER_KEEP,
FLAG_READER_DISABLE,
FLAG_READER_NFC_A,
@@ -438,7 +439,8 @@ public final class NfcAdapter {
public static final int FLAG_USE_ALL_TECH = 0xff;
/** @hide */
- @IntDef(flag = true, prefix = {"FLAG_LISTEN_"}, value = {
+ @IntDef(flag = true, value = {
+ FLAG_SET_DEFAULT_TECH,
FLAG_LISTEN_KEEP,
FLAG_LISTEN_DISABLE,
FLAG_LISTEN_NFC_PASSIVE_A,
@@ -449,6 +451,18 @@ public final class NfcAdapter {
public @interface ListenTechnology {}
/**
+ * Flag used in {@link #setDiscoveryTechnology(Activity, int, int)}.
+ * <p>
+ * Setting this flag changes the default listen or poll tech.
+ * Only available to privileged apps.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_SET_DEFAULT_DISC_TECH)
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ public static final int FLAG_SET_DEFAULT_TECH = 0x40000000;
+
+ /**
* @hide
* @removed
*/
@@ -1874,14 +1888,6 @@ public final class NfcAdapter {
public void setDiscoveryTechnology(@NonNull Activity activity,
@PollTechnology int pollTechnology, @ListenTechnology int listenTechnology) {
- // A special treatment of the _KEEP flags
- if ((listenTechnology & FLAG_LISTEN_KEEP) != 0) {
- listenTechnology = -1;
- }
- if ((pollTechnology & FLAG_READER_KEEP) != 0) {
- pollTechnology = -1;
- }
-
if (listenTechnology == FLAG_LISTEN_DISABLE) {
synchronized (sLock) {
if (!sHasNfcFeature) {
@@ -1901,7 +1907,25 @@ public final class NfcAdapter {
}
}
}
- mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology);
+ /*
+ * Privileged FLAG to set technology mask for all data processed by NFC controller
+ * Note: Use with caution! The app is responsible for ensuring that the discovery
+ * technology mask is returned to default.
+ * Note: FLAG_USE_ALL_TECH used with _KEEP flags will reset the technolody to android default
+ */
+ if (Flags.nfcSetDefaultDiscTech()
+ && ((pollTechnology & FLAG_SET_DEFAULT_TECH) == FLAG_SET_DEFAULT_TECH
+ || (listenTechnology & FLAG_SET_DEFAULT_TECH) == FLAG_SET_DEFAULT_TECH)) {
+ Binder token = new Binder();
+ try {
+ NfcAdapter.sService.updateDiscoveryTechnology(token,
+ pollTechnology, listenTechnology);
+ } catch (RemoteException e) {
+ attemptDeadServiceRecovery(e);
+ }
+ } else {
+ mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology);
+ }
}
/**
diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig
index cb2a48c2913f..b242a76ffae4 100644
--- a/nfc/java/android/nfc/flags.aconfig
+++ b/nfc/java/android/nfc/flags.aconfig
@@ -101,3 +101,12 @@ flag {
description: "Enable nfc state change API"
bug: "319934052"
}
+
+flag {
+ name: "nfc_set_default_disc_tech"
+ is_exported: true
+ namespace: "nfc"
+ description: "Flag for NFC set default disc tech API"
+ bug: "321311407"
+}
+
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
index b43b5f318cf1..373b3e8b068d 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
@@ -38,7 +38,6 @@ import com.android.credentialmanager.model.EntryInfo
import com.android.credentialmanager.model.creation.CreateOptionInfo
import com.android.credentialmanager.model.get.CredentialEntryInfo
import com.android.credentialmanager.model.get.ProviderInfo
-import java.lang.Exception
/**
* Aggregates common display information used for the Biometric Flow.
@@ -121,11 +120,11 @@ fun runBiometricFlowForGet(
getBiometricCancellationSignal: () -> CancellationSignal,
getRequestDisplayInfo: RequestDisplayInfo? = null,
getProviderInfoList: List<ProviderInfo>? = null,
- getProviderDisplayInfo: ProviderDisplayInfo? = null,
-) {
+ getProviderDisplayInfo: ProviderDisplayInfo? = null
+): Boolean {
if (getBiometricPromptState() != BiometricPromptState.INACTIVE) {
// Screen is already up, do not re-launch
- return
+ return false
}
onBiometricPromptStateChange(BiometricPromptState.PENDING)
val biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo(
@@ -137,7 +136,7 @@ fun runBiometricFlowForGet(
if (biometricDisplayInfo == null) {
onBiometricFailureFallback(BiometricFlowType.GET)
- return
+ return false
}
val callback: BiometricPrompt.AuthenticationCallback =
@@ -146,7 +145,7 @@ fun runBiometricFlowForGet(
getBiometricPromptState)
Log.d(TAG, "The BiometricPrompt API call begins for Get.")
- runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
+ return runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
onBiometricFailureFallback, BiometricFlowType.GET, onCancelFlowAndFinish,
getBiometricCancellationSignal)
}
@@ -169,11 +168,11 @@ fun runBiometricFlowForCreate(
getBiometricCancellationSignal: () -> CancellationSignal,
createRequestDisplayInfo: com.android.credentialmanager.createflow
.RequestDisplayInfo? = null,
- createProviderInfo: EnabledProviderInfo? = null,
-) {
+ createProviderInfo: EnabledProviderInfo? = null
+): Boolean {
if (getBiometricPromptState() != BiometricPromptState.INACTIVE) {
// Screen is already up, do not re-launch
- return
+ return false
}
onBiometricPromptStateChange(BiometricPromptState.PENDING)
val biometricDisplayInfo = validateAndRetrieveBiometricCreateDisplayInfo(
@@ -184,7 +183,7 @@ fun runBiometricFlowForCreate(
if (biometricDisplayInfo == null) {
onBiometricFailureFallback(BiometricFlowType.CREATE)
- return
+ return false
}
val callback: BiometricPrompt.AuthenticationCallback =
@@ -193,7 +192,7 @@ fun runBiometricFlowForCreate(
getBiometricPromptState)
Log.d(TAG, "The BiometricPrompt API call begins for Create.")
- runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
+ return runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
onBiometricFailureFallback, BiometricFlowType.CREATE, onCancelFlowAndFinish,
getBiometricCancellationSignal)
}
@@ -206,19 +205,19 @@ fun runBiometricFlowForCreate(
* only device credentials are requested.
*/
private fun runBiometricFlow(
- context: Context,
- biometricDisplayInfo: BiometricDisplayInfo,
- callback: BiometricPrompt.AuthenticationCallback,
- openMoreOptionsPage: () -> Unit,
- onBiometricFailureFallback: (BiometricFlowType) -> Unit,
- biometricFlowType: BiometricFlowType,
- onCancelFlowAndFinish: () -> Unit,
- getBiometricCancellationSignal: () -> CancellationSignal,
-) {
+ context: Context,
+ biometricDisplayInfo: BiometricDisplayInfo,
+ callback: BiometricPrompt.AuthenticationCallback,
+ openMoreOptionsPage: () -> Unit,
+ onBiometricFailureFallback: (BiometricFlowType) -> Unit,
+ biometricFlowType: BiometricFlowType,
+ onCancelFlowAndFinish: () -> Unit,
+ getBiometricCancellationSignal: () -> CancellationSignal
+): Boolean {
try {
if (!canCallBiometricPrompt(biometricDisplayInfo, context)) {
onBiometricFailureFallback(biometricFlowType)
- return
+ return false
}
val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo,
@@ -239,7 +238,9 @@ private fun runBiometricFlow(
} catch (e: IllegalArgumentException) {
Log.w(TAG, "Calling the biometric prompt API failed with: /n${e.localizedMessage}\n")
onBiometricFailureFallback(biometricFlowType)
+ return false
}
+ return true
}
private fun getCryptoOpId(biometricDisplayInfo: BiometricDisplayInfo): Int? {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 7d61f73a525b..4993a1fa0672 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -123,7 +123,8 @@ fun CreateCredentialScreen(
onBiometricPromptStateChange =
viewModel::onBiometricPromptStateChange,
getBiometricCancellationSignal =
- viewModel::getBiometricCancellationSignal
+ viewModel::getBiometricCancellationSignal,
+ onLog = { viewModel.logUiEvent(it) },
)
CreateScreenState.MORE_OPTIONS_SELECTION_ONLY -> MoreOptionsSelectionCard(
requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
@@ -642,12 +643,13 @@ internal fun BiometricSelectionPage(
getBiometricPromptState: () -> BiometricPromptState,
onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
getBiometricCancellationSignal: () -> CancellationSignal,
+ onLog: @Composable (UiEventEnum) -> Unit
) {
if (biometricEntry == null) {
fallbackToOriginalFlow(BiometricFlowType.CREATE)
return
}
- runBiometricFlowForCreate(
+ val biometricFlowCalled = runBiometricFlowForCreate(
biometricEntry = biometricEntry,
context = LocalContext.current,
openMoreOptionsPage = onMoreOptionSelected,
@@ -659,6 +661,9 @@ internal fun BiometricSelectionPage(
createProviderInfo = enabledProviderInfo,
onBiometricFailureFallback = fallbackToOriginalFlow,
onIllegalStateAndFinish = onIllegalScreenStateAndFinish,
- getBiometricCancellationSignal = getBiometricCancellationSignal,
+ getBiometricCancellationSignal = getBiometricCancellationSignal
)
+ if (biometricFlowCalled) {
+ onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_BIOMETRIC_FLOW_LAUNCHED)
+ }
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index ba61b90fa4dc..517ad0069f85 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -166,7 +166,8 @@ fun GetCredentialScreen(
onBiometricPromptStateChange =
viewModel::onBiometricPromptStateChange,
getBiometricCancellationSignal =
- viewModel::getBiometricCancellationSignal
+ viewModel::getBiometricCancellationSignal,
+ onLog = { viewModel.logUiEvent(it) },
)
} else if (credmanBiometricApiEnabled() &&
getCredentialUiState.currentScreenState
@@ -260,12 +261,13 @@ internal fun BiometricSelectionPage(
getBiometricPromptState: () -> BiometricPromptState,
onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
getBiometricCancellationSignal: () -> CancellationSignal,
+ onLog: @Composable (UiEventEnum) -> Unit,
) {
if (biometricEntry == null) {
fallbackToOriginalFlow(BiometricFlowType.GET)
return
}
- runBiometricFlowForGet(
+ val biometricFlowCalled = runBiometricFlowForGet(
biometricEntry = biometricEntry,
context = LocalContext.current,
openMoreOptionsPage = onMoreOptionSelected,
@@ -280,6 +282,9 @@ internal fun BiometricSelectionPage(
onBiometricFailureFallback = fallbackToOriginalFlow,
getBiometricCancellationSignal = getBiometricCancellationSignal
)
+ if (biometricFlowCalled) {
+ onLog(GetCredentialEvent.CREDMAN_GET_CRED_BIOMETRIC_FLOW_LAUNCHED)
+ }
}
/** Draws the primary credential selection page, used in Android U. */
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt b/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt
index daa42be020ce..39f2fcee6a0a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt
@@ -17,6 +17,7 @@ package com.android.credentialmanager.logging
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
enum class CreateCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum {
@@ -52,7 +53,10 @@ enum class CreateCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnu
CREDMAN_CREATE_CRED_EXTERNAL_ONLY_SELECTION(1327),
@UiEvent(doc = "The more about passkeys intro card is visible on screen.")
- CREDMAN_CREATE_CRED_MORE_ABOUT_PASSKEYS_INTRO(1328);
+ CREDMAN_CREATE_CRED_MORE_ABOUT_PASSKEYS_INTRO(1328),
+
+ @UiEvent(doc = "The single tap biometric flow is launched.")
+ CREDMAN_CREATE_CRED_BIOMETRIC_FLOW_LAUNCHED(RESERVE_NEW_UI_EVENT_ID);
override fun getId(): Int {
return this.id
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt b/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt
index 8de8895e8ffc..89fd72cdc8cc 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt
@@ -17,6 +17,7 @@ package com.android.credentialmanager.logging
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
enum class GetCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum {
@@ -54,7 +55,10 @@ enum class GetCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum {
CREDMAN_GET_CRED_PRIMARY_SELECTION_CARD(1341),
@UiEvent(doc = "The all sign in option card is visible on screen.")
- CREDMAN_GET_CRED_ALL_SIGN_IN_OPTION_CARD(1342);
+ CREDMAN_GET_CRED_ALL_SIGN_IN_OPTION_CARD(1342),
+
+ @UiEvent(doc = "The single tap biometric flow is launched.")
+ CREDMAN_GET_CRED_BIOMETRIC_FLOW_LAUNCHED(RESERVE_NEW_UI_EVENT_ID);
override fun getId(): Int {
return this.id
diff --git a/packages/SettingsLib/Color/res/values/colors.xml b/packages/SettingsLib/Color/res/values/colors.xml
index ef0dd1b654b9..b0b9b10952b8 100644
--- a/packages/SettingsLib/Color/res/values/colors.xml
+++ b/packages/SettingsLib/Color/res/values/colors.xml
@@ -17,7 +17,6 @@
<resources>
<!-- Dynamic colors-->
- <color name="settingslib_color_blue700">#0B57D0</color>
<color name="settingslib_color_blue600">#1a73e8</color>
<color name="settingslib_color_blue400">#669df6</color>
<color name="settingslib_color_blue300">#8ab4f8</color>
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
index bc3488fc31fb..0447ef8357eb 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
@@ -56,9 +56,6 @@ public class LottieColorUtils {
".black",
android.R.color.white);
map.put(
- ".blue200",
- R.color.settingslib_color_blue700);
- map.put(
".blue400",
R.color.settingslib_color_blue600);
map.put(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 9dd3d39cb040..1f7f07bb072d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -28,7 +28,9 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -445,6 +447,14 @@ private fun BoxScope.CommunalHubLazyGrid(
val selected by
remember(index) { derivedStateOf { list[index].key == selectedKey.value } }
DraggableItem(
+ modifier =
+ if (dragDropState.draggingItemIndex == index) {
+ Modifier
+ } else {
+ Modifier.animateItem(
+ placementSpec = spring(stiffness = Spring.StiffnessMediumLow)
+ )
+ },
dragDropState = dragDropState,
selected = selected,
enabled = list[index].isWidgetContent(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt
index ac5004e16a3b..580aba586ee1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt
@@ -16,6 +16,7 @@
package com.android.systemui.volume.panel.ui.composable
+import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -56,17 +57,19 @@ fun VolumePanelComposeScope.HorizontalVolumePanelContent(
with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) }
}
}
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(spacing),
- ) {
- for (component in layout.footerComponents) {
- AnimatedVisibility(
- visible = component.isVisible,
- modifier = Modifier.weight(1f),
- ) {
- with(component.component as ComposeVolumePanelUiComponent) {
- Content(Modifier)
+ AnimatedContent(
+ targetState = layout.footerComponents,
+ label = "FooterComponentAnimation",
+ ) { footerComponents ->
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(spacing),
+ ) {
+ for (component in footerComponents) {
+ if (component.isVisible) {
+ with(component.component as ComposeVolumePanelUiComponent) {
+ Content(Modifier.weight(1f))
+ }
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
index 9ea20b9da4b6..6349c1406a12 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
@@ -16,6 +16,7 @@
package com.android.systemui.volume.panel.ui.composable
+import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -50,26 +51,27 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent(
with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) }
}
}
- if (layout.footerComponents.isNotEmpty()) {
+
+ AnimatedContent(
+ targetState = layout.footerComponents,
+ label = "FooterComponentAnimation",
+ ) { footerComponents ->
Row(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
horizontalArrangement = Arrangement.spacedBy(if (isLargeScreen) 28.dp else 20.dp),
) {
val visibleComponentsCount =
- layout.footerComponents.fastSumBy { if (it.isVisible) 1 else 0 }
+ footerComponents.fastSumBy { if (it.isVisible) 1 else 0 }
// Center footer component if there is only one present
if (visibleComponentsCount == 1) {
Spacer(modifier = Modifier.weight(0.5f))
}
- for (component in layout.footerComponents) {
- AnimatedVisibility(
- visible = component.isVisible,
- modifier = Modifier.weight(1f),
- ) {
+ for (component in footerComponents) {
+ if (component.isVisible) {
with(component.component as ComposeVolumePanelUiComponent) {
- Content(Modifier)
+ Content(Modifier.weight(1f))
}
}
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java b/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java
index 165e9728b9d7..de9baa59b2b7 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java
+++ b/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java
@@ -79,6 +79,21 @@ public class Assert {
}
}
+ /**
+ * Asserts that the current thread is the same as the given thread, or that the current thread
+ * is the test thread.
+ * @param expected The looper we expected to be running on
+ */
+ public static void isCurrentThread(Looper expected) {
+ if (!expected.isCurrentThread()
+ && (sTestThread == null || sTestThread != Thread.currentThread())) {
+ throw new IllegalStateException("Called on wrong thread thread."
+ + " wanted " + expected.getThread().getName()
+ + " but instead got Thread.currentThread()="
+ + Thread.currentThread().getName());
+ }
+ }
+
public static void isNotMainThread() {
if (sMainLooper.isCurrentThread()
&& (sTestThread == null || sTestThread == Thread.currentThread())) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt
new file mode 100644
index 000000000000..7934b02126bd
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.ui.navigation
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.uiEventLoggerFake
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.domain.model.VolumePanelRoute
+import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor
+import com.android.systemui.volume.panel.ui.viewmodel.volumePanelViewModelFactory
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class VolumeNavigatorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+
+ private val underTest: VolumeNavigator =
+ with(kosmos) {
+ VolumeNavigator(
+ testScope.backgroundScope,
+ testDispatcher,
+ mock {},
+ activityStarter,
+ volumePanelViewModelFactory,
+ mock {
+ on { create(any(), anyInt(), anyBoolean(), any()) }.thenReturn(mock {})
+ on { applicationContext }.thenReturn(context)
+ },
+ uiEventLoggerFake,
+ volumePanelGlobalStateInteractor,
+ )
+ }
+
+ @Test
+ fun showNewVolumePanel_keyguardLocked_notShown() =
+ with(kosmos) {
+ testScope.runTest {
+ val panelState by collectLastValue(volumePanelGlobalStateInteractor.globalState)
+
+ underTest.openVolumePanel(VolumePanelRoute.COMPOSE_VOLUME_PANEL)
+ runCurrent()
+
+ assertThat(panelState!!.isVisible).isFalse()
+ }
+ }
+
+ @Test
+ fun showNewVolumePanel_keyguardUnlocked_shown() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(activityStarter.dismissKeyguardThenExecute(any(), any(), anyBoolean()))
+ .then { (it.arguments[0] as ActivityStarter.OnDismissAction).onDismiss() }
+ val panelState by collectLastValue(volumePanelGlobalStateInteractor.globalState)
+
+ underTest.openVolumePanel(VolumePanelRoute.COMPOSE_VOLUME_PANEL)
+ runCurrent()
+
+ assertThat(panelState!!.isVisible).isTrue()
+ }
+ }
+}
diff --git a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
index 292e49610e2a..06d1bf4c01cb 100644
--- a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
+++ b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
@@ -5,9 +5,9 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <LinearLayout
+ <FrameLayout
android:id="@+id/shortcut_helper_sheet"
- style="@style/Widget.Material3.BottomSheet"
+ style="@style/ShortcutHelperBottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@@ -19,13 +19,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
- <TextView
+ <androidx.compose.ui.platform.ComposeView
+ android:id="@+id/shortcut_helper_compose_container"
android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:gravity="center"
- android:textAppearance="?textAppearanceDisplayLarge"
- android:background="?colorTertiaryContainer"
- android:text="Shortcut Helper Content" />
- </LinearLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
+ android:layout_height="match_parent" />
+ </FrameLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/raw/face_dialog_authenticating.json b/packages/SystemUI/res/raw/face_dialog_authenticating.json
deleted file mode 100644
index 4e25e6d933c4..000000000000
--- a/packages/SystemUI/res/raw/face_dialog_authenticating.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.7.13","fr":60,"ip":0,"op":61,"w":64,"h":64,"nm":"face_scanning 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".blue200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[32,32,0],"ix":2,"l":2},"a":{"a":0,"k":[27.25,27.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[95,95,100]},{"t":60,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-1.243],[-1.244,0],[0,1.243],[1.242,0]],"o":[[0,1.243],[1.242,0],[0,-1.243],[-1.244,0]],"v":[[-2.249,0.001],[0.001,2.251],[2.249,0.001],[0.001,-2.251]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[15.1,20.495],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-1.243],[-1.242,0],[0,1.243],[1.242,0]],"o":[[0,1.243],[1.242,0],[0,-1.243],[-1.242,0]],"v":[[-2.249,0],[0.001,2.25],[2.249,0],[0.001,-2.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[39.4,20.495],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[2.814,3.523],[-2.814,3.523],[-2.814,1.363],[0.652,1.363],[0.652,-3.523],[2.814,-3.523]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.791,28.479],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.154,0.15],[0,0],[0.117,-0.095],[0,0],[0.228,-0.121],[0.358,-0.103],[0.922,0.261],[0.3,0.16],[0.24,0.185],[0.14,0.139],[0.178,0.261],[0.143,0.451],[0,0],[0,0.494],[0,0],[-0.214,-0.676],[-0.392,-0.572],[-0.323,-0.317],[-0.228,-0.177],[-0.333,-0.179],[-0.503,-0.145],[-0.662,0],[-0.653,0.184],[-0.437,0.233],[-0.336,0.258],[0,0],[0,0]],"o":[[0,0],[-0.107,0.106],[0,0],[-0.24,0.185],[-0.301,0.16],[-0.92,0.261],[-0.357,-0.103],[-0.228,-0.121],[-0.158,-0.122],[-0.225,-0.221],[-0.272,-0.393],[0,0],[-0.147,-0.466],[0,0],[0,0.716],[0.206,0.656],[0.256,0.372],[0.204,0.201],[0.336,0.258],[0.436,0.233],[0.655,0.184],[0.662,0],[0.503,-0.145],[0.332,-0.179],[0,0],[0,0],[0.165,-0.136]],"v":[[6.094,1.465],[4.579,-0.076],[4.242,0.225],[4.124,0.315],[3.43,0.771],[2.439,1.165],[-0.342,1.165],[-1.331,0.771],[-2.027,0.315],[-2.48,-0.075],[-3.087,-0.801],[-3.712,-2.075],[-3.712,-2.075],[-3.934,-3.523],[-6.094,-3.523],[-5.771,-1.424],[-4.868,0.424],[-3.995,1.465],[-3.344,2.027],[-2.35,2.676],[-0.934,3.243],[1.049,3.523],[3.031,3.243],[4.449,2.676],[5.441,2.027],[5.482,1.997],[5.615,1.895]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[26.201,40.411],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13.398,0],[0,-13.4],[13.398,0],[0,13.4]],"o":[[13.398,0],[0,13.4],[-13.398,0],[0,-13.4]],"v":[[0,-24.3],[24.3,0],[0,24.3],[-24.3,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[14.904,0],[0,-14.904],[-14.904,0],[0,14.904]],"o":[[-14.904,0],[0,14.904],[14.904,0],[0,-14.904]],"v":[[0,-27],[-27,0],[0,27],[27,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.25,27.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":4,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1200,"st":0,"bm":0}],"markers":[]} \ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index c038a8207d43..1226bbf21a0f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3489,6 +3489,45 @@
<!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
+ <!-- Title of the keyboard shortcut helper category "System". The helper is a component that
+ shows the user which keyboard shortcuts they can use. The "System" shortcuts are for
+ example "Take a screenshot" or "Go back". [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_category_system">System</string>
+ <!-- Title of the keyboard shortcut helper category "Multitasking". The helper is a component
+ that shows the user which keyboard shortcuts they can use. The "Multitasking" shortcuts are
+ for example "Enter split screen". [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_category_multitasking">Multitasking</string>
+ <!-- Title of the keyboard shortcut helper category "Input". The helper is a component
+ that shows the user which keyboard shortcuts they can use. The "Input" shortcuts are
+ the ones provided by the keyboard. Examples are "Access emoji" or "Switch to next language"
+ [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_category_input">Input</string>
+ <!-- Title of the keyboard shortcut helper category "App shortcuts". The helper is a component
+ that shows the user which keyboard shortcuts they can use. The "App shortcuts" are
+ for example "Open browser" or "Open calculator". [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_category_app_shortcuts">App shortcuts</string>
+ <!-- Title of the keyboard shortcut helper category "Accessibility". The helper is a component
+ that shows the user which keyboard shortcuts they can use. The "Accessibility" shortcuts
+ are for example "Turn on talkback". [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_category_a11y">Accessibility</string>
+ <!-- Title at the top of the keyboard shortcut helper UI. The helper is a component
+ that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_title">Keyboard shortcuts</string>
+ <!-- Placeholder text shown in the search box of the keyboard shortcut helper, when the user
+ hasn't typed in anything in the search box yet. The helper is a component that shows the
+ user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_search_placeholder">Search shortcuts</string>
+ <!-- Content description of the icon that allows to collapse a keyboard shortcut helper category
+ panel. The helper is a component that shows the user which keyboard shortcuts they can
+ use. The helper shows shortcuts in categories, which can be collapsed or expanded.
+ [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_content_description_collapse_icon">Collapse icon</string>
+ <!-- Content description of the icon that allows to expand a keyboard shortcut helper category
+ panel. The helper is a component that shows the user which keyboard shortcuts they can
+ use. The helper shows shortcuts in categories, which can be collapsed or expanded.
+ [CHAR LIMIT=NONE] -->
+ <string name="shortcut_helper_content_description_expand_icon">Expand icon</string>
+
<!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] -->
<string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
<!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 64717fcc8c5d..1e0adec4e84f 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1665,6 +1665,10 @@
<item name="android:colorBackground">@color/transparent</item>
</style>
+ <style name="ShortcutHelperBottomSheet" parent="@style/Widget.Material3.BottomSheet">
+ <item name="backgroundTint">?colorSurfaceContainer</item>
+ </style>
+
<style name="ShortcutHelperAnimation" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
<item name="android:taskOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
index a74b0b07299c..b8ff3bb43203 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
@@ -98,11 +98,11 @@ constructor(
) { unscaledSensorLocation, scale ->
val sensorLocation =
SensorLocation(
- unscaledSensorLocation.sensorLocationX,
- unscaledSensorLocation.sensorLocationY,
- unscaledSensorLocation.sensorRadius,
+ naturalCenterX = unscaledSensorLocation.sensorLocationX,
+ naturalCenterY = unscaledSensorLocation.sensorLocationY,
+ naturalRadius = unscaledSensorLocation.sensorRadius,
+ scale = scale
)
- sensorLocation.scale = scale
sensorLocation
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt
index dddadbd5e036..2f2f3a35dbaa 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt
@@ -16,18 +16,18 @@
package com.android.systemui.biometrics.shared.model
-/** Provides current sensor location information in the current screen resolution [scale]. */
+/**
+ * Provides current sensor location information in the current screen resolution [scale].
+ *
+ * @property scale Scale to apply to the sensor location's natural parameters to support different
+ * screen resolutions.
+ */
data class SensorLocation(
private val naturalCenterX: Int,
private val naturalCenterY: Int,
- private val naturalRadius: Int
+ private val naturalRadius: Int,
+ private val scale: Float = 1f
) {
- /**
- * Scale to apply to the sensor location's natural parameters to support different screen
- * resolutions.
- */
- var scale: Float = 1f
-
val centerX: Float
get() {
return naturalCenterX * scale
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
index fcc69927c2b3..9e836c31c177 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
@@ -17,7 +17,9 @@
package com.android.systemui.biometrics.ui.binder
+import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
+import android.graphics.drawable.Drawable
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
@@ -28,8 +30,8 @@ import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel
import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType
import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.res.R
import com.android.systemui.util.kotlin.Utils.Companion.toQuad
+import com.android.systemui.util.kotlin.Utils.Companion.toQuint
import com.android.systemui.util.kotlin.Utils.Companion.toTriple
import com.android.systemui.util.kotlin.sample
import kotlinx.coroutines.flow.combine
@@ -61,6 +63,16 @@ object PromptIconViewBinder {
}
var faceIcon: AnimatedVectorDrawable? = null
+ val faceIconCallback =
+ object : Animatable2.AnimationCallback() {
+ override fun onAnimationStart(drawable: Drawable) {
+ viewModel.onAnimationStart()
+ }
+
+ override fun onAnimationEnd(drawable: Drawable) {
+ viewModel.onAnimationEnd()
+ }
+ }
if (!constraintBp()) {
launch {
@@ -126,13 +138,19 @@ object PromptIconViewBinder {
combine(
viewModel.activeAuthType,
viewModel.shouldAnimateIconView,
+ viewModel.shouldRepeatAnimation,
viewModel.showingError,
- ::Triple
+ ::toQuad
),
- ::toQuad
+ ::toQuint
)
- .collect { (iconAsset, activeAuthType, shouldAnimateIconView, showingError)
- ->
+ .collect {
+ (
+ iconAsset,
+ activeAuthType,
+ shouldAnimateIconView,
+ shouldRepeatAnimation,
+ showingError) ->
if (iconAsset != -1) {
when (activeAuthType) {
AuthType.Fingerprint,
@@ -145,27 +163,21 @@ object PromptIconViewBinder {
}
}
AuthType.Face -> {
- // TODO(b/318569643): Consolidate logic once all face auth
- // assets are migrated from drawable to json
- if (iconAsset == R.raw.face_dialog_authenticating) {
- iconView.setAnimation(iconAsset)
- iconView.frame = 0
-
+ faceIcon?.apply {
+ unregisterAnimationCallback(faceIconCallback)
+ stop()
+ }
+ faceIcon =
+ iconView.context.getDrawable(iconAsset)
+ as AnimatedVectorDrawable
+ faceIcon?.apply {
+ iconView.setImageDrawable(this)
if (shouldAnimateIconView) {
- iconView.playAnimation()
- iconView.loop(true)
- }
- } else {
- faceIcon?.apply { stop() }
- faceIcon =
- iconView.context.getDrawable(iconAsset)
- as AnimatedVectorDrawable
- faceIcon?.apply {
- iconView.setImageDrawable(this)
- if (shouldAnimateIconView) {
- forceAnimationOnUI()
- start()
+ forceAnimationOnUI()
+ if (shouldRepeatAnimation) {
+ registerAnimationCallback(faceIconCallback)
}
+ start()
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
index 901d7517c5e9..bde3e992a295 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
@@ -21,6 +21,7 @@ import android.annotation.DrawableRes
import android.annotation.RawRes
import android.content.res.Configuration
import android.graphics.Rect
+import android.hardware.face.Face
import android.util.RotationUtils
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
@@ -31,10 +32,12 @@ import com.android.systemui.res.R
import com.android.systemui.util.kotlin.combine
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
/**
* Models UI of [BiometricPromptLayout.iconView] and [BiometricPromptLayout.biometric_icon_overlay]
@@ -55,8 +58,11 @@ constructor(
}
/**
- * Indicates what auth type the UI currently displays. Fingerprint-only auth -> Fingerprint
- * Face-only auth -> Face Co-ex auth, implicit flow -> Face Co-ex auth, explicit flow -> Coex
+ * Indicates what auth type the UI currently displays.
+ * Fingerprint-only auth -> Fingerprint
+ * Face-only auth -> Face
+ * Co-ex auth, implicit flow -> Face
+ * Co-ex auth, explicit flow -> Coex
*/
val activeAuthType: Flow<AuthType> =
combine(
@@ -113,6 +119,35 @@ constructor(
_previousIconOverlayWasError.value = previousIconOverlayWasError
}
+ /** Called when iconView begins animating. */
+ fun onAnimationStart() {
+ _animationEnded.value = false
+ }
+
+ /** Called when iconView ends animating. */
+ fun onAnimationEnd() {
+ _animationEnded.value = true
+ }
+
+ private val _animationEnded: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+ /**
+ * Whether a face iconView should pulse (i.e. while isAuthenticating and previous animation
+ * ended).
+ */
+ val shouldPulseAnimation: Flow<Boolean> =
+ combine(_animationEnded, promptViewModel.isAuthenticating) {
+ animationEnded,
+ isAuthenticating ->
+ animationEnded && isAuthenticating
+ }
+ .distinctUntilChanged()
+
+ private val _lastPulseLightToDark: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+ /** Tracks whether a face iconView last pulsed light to dark (vs. dark to light) */
+ val lastPulseLightToDark: Flow<Boolean> = _lastPulseLightToDark.asStateFlow()
+
val iconSize: Flow<Pair<Int, Int>> =
combine(
promptViewModel.position,
@@ -160,22 +195,35 @@ constructor(
}
}
AuthType.Face ->
- combine(
- promptViewModel.isAuthenticated.distinctUntilChanged(),
- promptViewModel.isAuthenticating.distinctUntilChanged(),
- promptViewModel.isPendingConfirmation.distinctUntilChanged(),
- promptViewModel.showingError.distinctUntilChanged()
- ) {
- authState: PromptAuthState,
- isAuthenticating: Boolean,
- isPendingConfirmation: Boolean,
- showingError: Boolean ->
- getFaceIconViewAsset(
- authState,
- isAuthenticating,
- isPendingConfirmation,
- showingError
- )
+ shouldPulseAnimation.flatMapLatest { shouldPulseAnimation: Boolean ->
+ if (shouldPulseAnimation) {
+ val iconAsset =
+ if (_lastPulseLightToDark.value) {
+ R.drawable.face_dialog_pulse_dark_to_light
+ } else {
+ R.drawable.face_dialog_pulse_light_to_dark
+ }
+ _lastPulseLightToDark.value = !_lastPulseLightToDark.value
+ flowOf(iconAsset)
+ } else {
+ combine(
+ promptViewModel.isAuthenticated.distinctUntilChanged(),
+ promptViewModel.isAuthenticating.distinctUntilChanged(),
+ promptViewModel.isPendingConfirmation.distinctUntilChanged(),
+ promptViewModel.showingError.distinctUntilChanged()
+ ) {
+ authState: PromptAuthState,
+ isAuthenticating: Boolean,
+ isPendingConfirmation: Boolean,
+ showingError: Boolean ->
+ getFaceIconViewAsset(
+ authState,
+ isAuthenticating,
+ isPendingConfirmation,
+ showingError
+ )
+ }
+ }
}
AuthType.Coex ->
combine(
@@ -279,7 +327,8 @@ constructor(
} else if (authState.isAuthenticated) {
R.drawable.face_dialog_dark_to_checkmark
} else if (isAuthenticating) {
- R.raw.face_dialog_authenticating
+ _lastPulseLightToDark.value = false
+ R.drawable.face_dialog_pulse_dark_to_light
} else if (showingError) {
R.drawable.face_dialog_dark_to_error
} else if (_previousIconWasError.value) {
@@ -654,6 +703,16 @@ constructor(
}
}
+ /** Whether the current BiometricPromptLayout.iconView asset animation should be repeated. */
+ val shouldRepeatAnimation: Flow<Boolean> =
+ activeAuthType.flatMapLatest { activeAuthType: AuthType ->
+ when (activeAuthType) {
+ AuthType.Fingerprint,
+ AuthType.Coex -> flowOf(false)
+ AuthType.Face -> promptViewModel.isAuthenticating.map { it }
+ }
+ }
+
/** Called on configuration changes */
fun onConfigurationChanged(newConfig: Configuration) {
displayStateInteractor.onConfigurationChanged(newConfig)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
new file mode 100644
index 000000000000..52ccc219353e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -0,0 +1,413 @@
+/*
+ * 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.keyboard.shortcut.ui.composable
+
+import androidx.annotation.StringRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.OpenInNew
+import androidx.compose.material.icons.filled.Accessibility
+import androidx.compose.material.icons.filled.Apps
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material.icons.filled.Keyboard
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Tv
+import androidx.compose.material.icons.filled.VerticalSplit
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationDrawerItemColors
+import androidx.compose.material3.NavigationDrawerItemDefaults
+import androidx.compose.material3.SearchBar
+import androidx.compose.material3.SearchBarDefaults
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
+import com.android.compose.windowsizeclass.LocalWindowSizeClass
+import com.android.systemui.res.R
+
+@Composable
+fun ShortcutHelper(modifier: Modifier = Modifier, onKeyboardSettingsClicked: () -> Unit) {
+ if (shouldUseSinglePane()) {
+ ShortcutHelperSinglePane(modifier, categories, onKeyboardSettingsClicked)
+ } else {
+ ShortcutHelperTwoPane(modifier, categories, onKeyboardSettingsClicked)
+ }
+}
+
+@Composable
+private fun shouldUseSinglePane() =
+ LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact
+
+@Composable
+private fun ShortcutHelperSinglePane(
+ modifier: Modifier = Modifier,
+ categories: List<ShortcutHelperCategory>,
+ onKeyboardSettingsClicked: () -> Unit,
+) {
+ Column(
+ modifier =
+ modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(start = 16.dp, end = 16.dp, top = 26.dp)
+ ) {
+ TitleBar()
+ Spacer(modifier = Modifier.height(6.dp))
+ ShortcutsSearchBar()
+ Spacer(modifier = Modifier.height(16.dp))
+ CategoriesPanelSinglePane(categories)
+ Spacer(modifier = Modifier.weight(1f))
+ KeyboardSettings(onClick = onKeyboardSettingsClicked)
+ }
+}
+
+@Composable
+private fun CategoriesPanelSinglePane(
+ categories: List<ShortcutHelperCategory>,
+) {
+ var expandedCategory by remember { mutableStateOf<ShortcutHelperCategory?>(null) }
+ Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ categories.fastForEachIndexed { index, category ->
+ val isExpanded = expandedCategory == category
+ val itemShape =
+ if (index == 0) {
+ ShortcutHelper.Shapes.singlePaneFirstCategory
+ } else if (index == categories.lastIndex) {
+ ShortcutHelper.Shapes.singlePaneLastCategory
+ } else {
+ ShortcutHelper.Shapes.singlePaneCategory
+ }
+ CategoryItemSinglePane(
+ category = category,
+ isExpanded = isExpanded,
+ onClick = {
+ expandedCategory =
+ if (isExpanded) {
+ null
+ } else {
+ category
+ }
+ },
+ shape = itemShape,
+ )
+ }
+ }
+}
+
+@Composable
+private fun CategoryItemSinglePane(
+ category: ShortcutHelperCategory,
+ isExpanded: Boolean,
+ onClick: () -> Unit,
+ shape: Shape,
+) {
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceBright,
+ shape = shape,
+ onClick = onClick,
+ ) {
+ Column {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp)
+ ) {
+ Icon(category.icon, contentDescription = null)
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(stringResource(category.labelResId))
+ Spacer(modifier = Modifier.weight(1f))
+ RotatingExpandCollapseIcon(isExpanded)
+ }
+ AnimatedVisibility(visible = isExpanded) { ShortcutCategoryDetailsSinglePane(category) }
+ }
+ }
+}
+
+@Composable
+private fun RotatingExpandCollapseIcon(isExpanded: Boolean) {
+ val expandIconRotationDegrees by
+ animateFloatAsState(
+ targetValue =
+ if (isExpanded) {
+ 180f
+ } else {
+ 0f
+ },
+ label = "Expand icon rotation animation"
+ )
+ Icon(
+ modifier =
+ Modifier.background(
+ color = MaterialTheme.colorScheme.surfaceContainerHigh,
+ shape = CircleShape
+ )
+ .graphicsLayer { rotationZ = expandIconRotationDegrees },
+ imageVector = Icons.Default.ExpandMore,
+ contentDescription =
+ if (isExpanded) {
+ stringResource(R.string.shortcut_helper_content_description_collapse_icon)
+ } else {
+ stringResource(R.string.shortcut_helper_content_description_expand_icon)
+ },
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+}
+
+@Composable
+private fun ShortcutCategoryDetailsSinglePane(category: ShortcutHelperCategory) {
+ Box(modifier = Modifier.fillMaxWidth().heightIn(min = 300.dp)) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = stringResource(category.labelResId),
+ )
+ }
+}
+
+@Composable
+private fun ShortcutHelperTwoPane(
+ modifier: Modifier = Modifier,
+ categories: List<ShortcutHelperCategory>,
+ onKeyboardSettingsClicked: () -> Unit,
+) {
+ Column(modifier = modifier.fillMaxSize().padding(start = 24.dp, end = 24.dp, top = 26.dp)) {
+ TitleBar()
+ Spacer(modifier = Modifier.height(12.dp))
+ Row(Modifier.fillMaxWidth()) {
+ StartSidePanel(
+ modifier = Modifier.fillMaxWidth(fraction = 0.32f),
+ categories = categories,
+ onKeyboardSettingsClicked = onKeyboardSettingsClicked,
+ )
+ Spacer(modifier = Modifier.width(24.dp))
+ EndSidePanel(Modifier.fillMaxSize())
+ }
+ }
+}
+
+@Composable
+private fun StartSidePanel(
+ modifier: Modifier,
+ categories: List<ShortcutHelperCategory>,
+ onKeyboardSettingsClicked: () -> Unit,
+) {
+ Column(modifier) {
+ ShortcutsSearchBar()
+ Spacer(modifier = Modifier.heightIn(16.dp))
+ CategoriesPanelTwoPane(categories)
+ Spacer(modifier = Modifier.weight(1f))
+ KeyboardSettings(onKeyboardSettingsClicked)
+ }
+}
+
+@Composable
+private fun CategoriesPanelTwoPane(categories: List<ShortcutHelperCategory>) {
+ var selected by remember { mutableStateOf(categories.first()) }
+ Column {
+ categories.fastForEach {
+ CategoryItemTwoPane(
+ label = stringResource(it.labelResId),
+ icon = it.icon,
+ selected = selected == it,
+ onClick = { selected = it }
+ )
+ }
+ }
+}
+
+@Composable
+private fun CategoryItemTwoPane(
+ label: String,
+ icon: ImageVector,
+ selected: Boolean,
+ onClick: () -> Unit,
+ colors: NavigationDrawerItemColors =
+ NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent),
+) {
+ Surface(
+ selected = selected,
+ onClick = onClick,
+ modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 72.dp).fillMaxWidth(),
+ shape = RoundedCornerShape(28.dp),
+ color = colors.containerColor(selected).value,
+ ) {
+ Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ modifier = Modifier.size(24.dp),
+ imageVector = icon,
+ contentDescription = null,
+ tint = colors.iconColor(selected).value
+ )
+ Spacer(Modifier.width(12.dp))
+ Box(Modifier.weight(1f)) {
+ Text(
+ fontSize = 18.sp,
+ color = colors.textColor(selected).value,
+ style = MaterialTheme.typography.headlineSmall,
+ text = label
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun EndSidePanel(modifier: Modifier) {
+ Surface(
+ modifier = modifier,
+ shape = RoundedCornerShape(28.dp),
+ color = MaterialTheme.colorScheme.surfaceBright
+ ) {}
+}
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun TitleBar() {
+ CenterAlignedTopAppBar(
+ colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color.Transparent),
+ title = {
+ Text(
+ text = stringResource(R.string.shortcut_helper_title),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.headlineSmall
+ )
+ }
+ )
+}
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun ShortcutsSearchBar() {
+ var query by remember { mutableStateOf("") }
+ SearchBar(
+ colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceBright),
+ query = query,
+ active = false,
+ onActiveChange = {},
+ onQueryChange = { query = it },
+ onSearch = {},
+ leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
+ placeholder = { Text(text = stringResource(R.string.shortcut_helper_search_placeholder)) },
+ content = {}
+ )
+}
+
+@Composable
+private fun KeyboardSettings(onClick: () -> Unit) {
+ Surface(
+ onClick = onClick,
+ shape = RoundedCornerShape(24.dp),
+ color = Color.Transparent,
+ modifier = Modifier.semantics { role = Role.Button }.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ "Keyboard Settings",
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ fontSize = 16.sp
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.OpenInNew,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+}
+
+/** Temporary data class just to populate the UI. */
+private data class ShortcutHelperCategory(
+ @StringRes val labelResId: Int,
+ val icon: ImageVector,
+)
+
+// Temporarily populating the categories directly in the UI.
+private val categories =
+ listOf(
+ ShortcutHelperCategory(R.string.shortcut_helper_category_system, Icons.Default.Tv),
+ ShortcutHelperCategory(
+ R.string.shortcut_helper_category_multitasking,
+ Icons.Default.VerticalSplit
+ ),
+ ShortcutHelperCategory(R.string.shortcut_helper_category_input, Icons.Default.Keyboard),
+ ShortcutHelperCategory(R.string.shortcut_helper_category_app_shortcuts, Icons.Default.Apps),
+ ShortcutHelperCategory(R.string.shortcut_helper_category_a11y, Icons.Default.Accessibility),
+ )
+
+object ShortcutHelper {
+
+ object Shapes {
+ val singlePaneFirstCategory =
+ RoundedCornerShape(
+ topStart = Dimensions.SinglePaneCategoryCornerRadius,
+ topEnd = Dimensions.SinglePaneCategoryCornerRadius
+ )
+ val singlePaneLastCategory =
+ RoundedCornerShape(
+ bottomStart = Dimensions.SinglePaneCategoryCornerRadius,
+ bottomEnd = Dimensions.SinglePaneCategoryCornerRadius
+ )
+ val singlePaneCategory = RectangleShape
+ }
+
+ object Dimensions {
+ val SinglePaneCategoryCornerRadius = 28.dp
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
index ef4156da4f7b..1e8d23918964 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
@@ -23,9 +23,12 @@ import android.view.WindowInsets
import androidx.activity.BackEventCompat
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
+import androidx.compose.ui.platform.ComposeView
import androidx.core.view.updatePadding
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
+import com.android.compose.theme.PlatformTheme
+import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper
import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
import com.android.systemui.res.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -58,14 +61,30 @@ constructor(
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_keyboard_shortcut_helper)
setUpBottomSheetWidth()
+ expandBottomSheet()
setUpInsets()
setUpPredictiveBack()
setUpSheetDismissListener()
setUpDismissOnTouchOutside()
+ setUpComposeView()
observeFinishRequired()
viewModel.onViewOpened()
}
+ private fun setUpComposeView() {
+ requireViewById<ComposeView>(R.id.shortcut_helper_compose_container).apply {
+ setContent {
+ PlatformTheme {
+ ShortcutHelper(
+ onKeyboardSettingsClicked = ::onKeyboardSettingsClicked,
+ )
+ }
+ }
+ }
+ }
+
+ private fun onKeyboardSettingsClicked() {}
+
override fun onDestroy() {
super.onDestroy()
if (isFinishing) {
@@ -101,7 +120,8 @@ constructor(
bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets ->
val safeDrawingInsets = insets.safeDrawing
// Make sure the bottom sheet is not covered by the status bar.
- bottomSheetContainer.updatePadding(top = safeDrawingInsets.top)
+ bottomSheetBehavior.maxHeight =
+ resources.displayMetrics.heightPixels - safeDrawingInsets.top
// Make sure the contents inside of the bottom sheet are not hidden by system bars, or
// cutouts.
bottomSheet.updatePadding(
@@ -171,7 +191,6 @@ constructor(
private val WindowInsets.safeDrawing
get() =
getInsets(WindowInsets.Type.systemBars())
- .union(getInsets(WindowInsets.Type.ime()))
.union(getInsets(WindowInsets.Type.displayCutout()))
private fun Insets.union(insets: Insets): Insets = Insets.max(this, insets)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index c5fab8f57822..e01f0a152b37 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -147,8 +147,9 @@ constructor(
deviceEntryIconViewModel.get().udfpsLocation.value?.let { udfpsLocation ->
Log.d(
"DeviceEntrySection",
- "udfpsLocation=$udfpsLocation" +
- " unusedAuthController=${authController.udfpsLocation}"
+ "udfpsLocation=$udfpsLocation, " +
+ "scaledLocation=(${udfpsLocation.centerX},${udfpsLocation.centerY}), " +
+ "unusedAuthController=${authController.udfpsLocation}"
)
centerIcon(
Point(udfpsLocation.centerX.toInt(), udfpsLocation.centerY.toInt()),
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt
index e0c54190283a..9c29bab80d14 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt
@@ -43,6 +43,7 @@ interface MediaDomainModule {
@IntoMap
@ClassKey(MediaDataProcessor::class)
fun bindMediaDataProcessor(interactor: MediaDataProcessor): CoreStartable
+
companion object {
@Provides
@@ -52,7 +53,7 @@ interface MediaDomainModule {
newProvider: Provider<MediaCarouselInteractor>,
mediaFlags: MediaFlags,
): MediaDataManager {
- return if (mediaFlags.isMediaControlsRefactorEnabled()) {
+ return if (mediaFlags.isSceneContainerEnabled()) {
newProvider.get()
} else {
legacyProvider.get()
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index eed775242d1f..8e985e11732f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -269,7 +269,7 @@ class MediaDataProcessor(
}
override fun start() {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (!mediaFlags.isSceneContainerEnabled()) {
return
}
@@ -746,8 +746,7 @@ class MediaDataProcessor(
notif.extras.getParcelable(
Notification.EXTRA_BUILDER_APPLICATION_INFO,
ApplicationInfo::class.java
- )
- ?: getAppInfoFromPackage(sbn.packageName)
+ ) ?: getAppInfoFromPackage(sbn.packageName)
// App name
val appName = getAppName(sbn, appInfo)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
index 9e6230012760..b4bd4fd2c266 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
@@ -36,8 +36,8 @@ import com.android.systemui.media.controls.domain.pipeline.MediaSessionBasedFilt
import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener
import com.android.systemui.media.controls.domain.resume.MediaResumeListener
import com.android.systemui.media.controls.shared.model.MediaCommonModel
-import com.android.systemui.media.controls.util.MediaControlsRefactorFlag
import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -127,7 +127,7 @@ constructor(
val currentMedia: StateFlow<List<MediaCommonModel>> = mediaFilterRepository.currentMedia
override fun start() {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (!mediaFlags.isSceneContainerEnabled()) {
return
}
@@ -256,8 +256,6 @@ constructor(
companion object {
val unsupported: Nothing
get() =
- error(
- "Code path not supported when ${MediaControlsRefactorFlag.FLAG_NAME} is enabled"
- )
+ error("Code path not supported when ${SceneContainerFlag.DESCRIPTION} is enabled")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index 19e3e0715989..8316b3aba73e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -217,7 +217,7 @@ constructor(
private val animationScaleObserver: ContentObserver =
object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (!mediaFlags.isSceneContainerEnabled()) {
MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() }
} else {
controllerByViewModel.values.forEach { it.updateAnimatorDurationScale() }
@@ -347,7 +347,7 @@ constructor(
inflateSettingsButton()
mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
configurationController.addCallback(configListener)
- if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (!mediaFlags.isSceneContainerEnabled()) {
setUpListeners()
} else {
val visualStabilityCallback = OnReorderingAllowedListener {
@@ -389,7 +389,7 @@ constructor(
listenForAnyStateToLockscreenTransition(this)
listenForLockscreenSettingChanges(this)
- if (!mediaFlags.isMediaControlsRefactorEnabled()) return@repeatOnLifecycle
+ if (!mediaFlags.isSceneContainerEnabled()) return@repeatOnLifecycle
listenForMediaItemsChanges(this)
}
}
@@ -882,8 +882,7 @@ constructor(
val previousVisibleIndex =
MediaPlayerData.playerKeys().indexOfFirst { key -> it == key }
mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex)
- }
- ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
+ } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
}
} else if (isRtl && mediaContent.childCount > 0) {
// In RTL, Scroll to the first player as it is the rightmost player in media carousel.
@@ -1092,7 +1091,7 @@ constructor(
}
private fun updatePlayers(recreateMedia: Boolean) {
- if (mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (mediaFlags.isSceneContainerEnabled()) {
updateMediaPlayers(recreateMedia)
return
}
@@ -1192,7 +1191,7 @@ constructor(
currentStartLocation = startLocation
currentEndLocation = endLocation
currentTransitionProgress = progress
- if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (!mediaFlags.isSceneContainerEnabled()) {
for (mediaPlayer in MediaPlayerData.players()) {
updateViewControllerToState(mediaPlayer.mediaViewController, immediately)
}
@@ -1254,7 +1253,7 @@ constructor(
/** Update listening to seekbar. */
private fun updateSeekbarListening(visibleToUser: Boolean) {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (!mediaFlags.isSceneContainerEnabled()) {
for (player in MediaPlayerData.players()) {
player.setListening(visibleToUser && currentlyExpanded)
}
@@ -1269,7 +1268,7 @@ constructor(
private fun updateCarouselDimensions() {
var width = 0
var height = 0
- if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (!mediaFlags.isSceneContainerEnabled()) {
for (mediaPlayer in MediaPlayerData.players()) {
val controller = mediaPlayer.mediaViewController
// When transitioning the view to gone, the view gets smaller, but the translation
@@ -1361,7 +1360,7 @@ constructor(
!mediaManager.hasActiveMediaOrRecommendation() &&
desiredHostState.showsOnlyActiveMedia
- if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (!mediaFlags.isSceneContainerEnabled()) {
for (mediaPlayer in MediaPlayerData.players()) {
if (animate) {
mediaPlayer.mediaViewController.animatePendingStateChange(
@@ -1401,7 +1400,7 @@ constructor(
}
fun closeGuts(immediate: Boolean = true) {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (!mediaFlags.isSceneContainerEnabled()) {
MediaPlayerData.players().forEach { it.closeGuts(immediate) }
} else {
controllerByViewModel.values.forEach { it.closeGuts(immediate) }
@@ -1544,7 +1543,7 @@ constructor(
@VisibleForTesting
fun onSwipeToDismiss() {
- if (mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (mediaFlags.isSceneContainerEnabled()) {
mediaCarouselViewModel.onSwipeToDismiss()
return
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
index 38377088a2d7..9d0723211d4b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
@@ -203,7 +203,7 @@ constructor(
private val scrubbingChangeListener =
object : SeekBarViewModel.ScrubbingChangeListener {
override fun onScrubbingChanged(scrubbing: Boolean) {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+ if (!mediaFlags.isSceneContainerEnabled()) return
if (isScrubbing == scrubbing) return
isScrubbing = scrubbing
updateDisplayForScrubbingChange()
@@ -213,7 +213,7 @@ constructor(
private val enabledChangeListener =
object : SeekBarViewModel.EnabledChangeListener {
override fun onEnabledChanged(enabled: Boolean) {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+ if (!mediaFlags.isSceneContainerEnabled()) return
if (isSeekBarEnabled == enabled) return
isSeekBarEnabled = enabled
MediaControlViewBinder.updateSeekBarVisibility(expandedLayout, isSeekBarEnabled)
@@ -229,7 +229,7 @@ constructor(
* @param listening True when player should be active. Otherwise, false.
*/
fun setListening(listening: Boolean) {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+ if (!mediaFlags.isSceneContainerEnabled()) return
seekBarViewModel.listening = listening
}
@@ -263,7 +263,7 @@ constructor(
)
)
}
- if (mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (mediaFlags.isSceneContainerEnabled()) {
if (
this@MediaViewController::recsConfigurationChangeListener.isInitialized
) {
@@ -305,6 +305,7 @@ constructor(
*/
var collapsedLayout = ConstraintSet()
@VisibleForTesting set
+
/**
* The expanded constraint set used to render a collapsed player. If it is modified, make sure
* to call [refreshState]
@@ -334,7 +335,7 @@ constructor(
* Notify this controller that the view has been removed and all listeners should be destroyed
*/
fun onDestroy() {
- if (mediaFlags.isMediaControlsRefactorEnabled()) {
+ if (mediaFlags.isSceneContainerEnabled()) {
if (this::seekBarObserver.isInitialized) {
seekBarViewModel.progress.removeObserver(seekBarObserver)
}
@@ -657,7 +658,7 @@ constructor(
}
fun attachPlayer(mediaViewHolder: MediaViewHolder) {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+ if (!mediaFlags.isSceneContainerEnabled()) return
this.mediaViewHolder = mediaViewHolder
// Setting up seek bar.
@@ -731,7 +732,7 @@ constructor(
}
fun updateAnimatorDurationScale() {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+ if (!mediaFlags.isSceneContainerEnabled()) return
if (this::seekBarObserver.isInitialized) {
seekBarObserver.animationEnabled =
globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f
@@ -787,7 +788,7 @@ constructor(
}
fun attachRecommendations(recommendationViewHolder: RecommendationViewHolder) {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+ if (!mediaFlags.isSceneContainerEnabled()) return
this.recommendationViewHolder = recommendationViewHolder
attach(recommendationViewHolder.recommendations, TYPE.RECOMMENDATION)
@@ -796,13 +797,13 @@ constructor(
}
fun bindSeekBar(onSeek: () -> Unit, onBindSeekBar: (SeekBarViewModel) -> Unit) {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+ if (!mediaFlags.isSceneContainerEnabled()) return
seekBarViewModel.logSeek = onSeek
onBindSeekBar.invoke(seekBarViewModel)
}
fun setUpTurbulenceNoise() {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+ if (!mediaFlags.isSceneContainerEnabled()) return
if (!this::turbulenceNoiseAnimationConfig.isInitialized) {
turbulenceNoiseAnimationConfig =
createTurbulenceNoiseConfig(
@@ -1153,13 +1154,13 @@ constructor(
}
fun setUpPrevButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+ if (!mediaFlags.isSceneContainerEnabled()) return
isPrevButtonAvailable = isAvailable
prevNotVisibleValue = notVisibleValue
}
fun setUpNextButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) {
- if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+ if (!mediaFlags.isSceneContainerEnabled()) return
isNextButtonAvailable = isAvailable
nextNotVisibleValue = notVisibleValue
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt
deleted file mode 100644
index 2850b4bb2358..000000000000
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media.controls.util
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the media_controls_refactor flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object MediaControlsRefactorFlag {
- /** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_MEDIA_CONTROLS_REFACTOR
-
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
-
- /** Is the flag enabled? */
- @JvmStatic
- inline val isEnabled
- get() = Flags.mediaControlsRefactor()
-
- /**
- * 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/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 1e7bc0cacf1d..21c311191710 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -52,8 +52,4 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlagsClass
/** Check whether to use scene framework */
fun isSceneContainerEnabled() = SceneContainerFlag.isEnabled
-
- /** Check whether to use media refactor code */
- fun isMediaControlsRefactorEnabled() =
- MediaControlsRefactorFlag.isEnabled && SceneContainerFlag.isEnabled
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index b208434c3218..18358a79cbca 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -36,11 +36,12 @@ import androidx.dynamicanimation.animation.DynamicAnimation
import com.android.internal.jank.Cuj
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.util.LatencyTracker
-import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.NavigationEdgeBackPlugin
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.ViewController
+import com.android.systemui.util.concurrency.BackPanelUiThread
+import com.android.systemui.util.concurrency.UiThreadContext
import com.android.systemui.util.time.SystemClock
import java.io.PrintWriter
import javax.inject.Inject
@@ -84,11 +85,11 @@ internal constructor(
context: Context,
private val windowManager: WindowManager,
private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
+ private val mainHandler: Handler,
private val systemClock: SystemClock,
private val vibratorHelper: VibratorHelper,
private val configurationController: ConfigurationController,
- private val latencyTracker: LatencyTracker,
+ latencyTracker: LatencyTracker,
private val interactionJankMonitor: InteractionJankMonitor,
) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
@@ -103,7 +104,7 @@ internal constructor(
constructor(
private val windowManager: WindowManager,
private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
+ @BackPanelUiThread private val uiThreadContext: UiThreadContext,
private val systemClock: SystemClock,
private val vibratorHelper: VibratorHelper,
private val configurationController: ConfigurationController,
@@ -112,20 +113,19 @@ internal constructor(
) {
/** Construct a [BackPanelController]. */
fun create(context: Context): BackPanelController {
- val backPanelController =
- BackPanelController(
+ uiThreadContext.isCurrentThread()
+ return BackPanelController(
context,
windowManager,
viewConfiguration,
- mainHandler,
+ uiThreadContext.handler,
systemClock,
vibratorHelper,
configurationController,
latencyTracker,
interactionJankMonitor
)
- backPanelController.init()
- return backPanelController
+ .also { it.init() }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index d0f8412c85b2..2dc09e5ab478 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -44,8 +44,6 @@ import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.input.InputManager;
import android.icu.text.SimpleDateFormat;
-import android.os.Handler;
-import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
@@ -55,7 +53,6 @@ import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
-import android.view.Choreographer;
import android.view.ISystemGestureExclusionListener;
import android.view.IWindowManager;
import android.view.InputDevice;
@@ -75,7 +72,6 @@ import androidx.annotation.DimenRes;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.policy.GestureNavigationSettingsObserver;
import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.plugins.FalsingManager;
@@ -94,7 +90,8 @@ import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.phone.LightBarController;
-import com.android.systemui.util.Assert;
+import com.android.systemui.util.concurrency.BackPanelUiThread;
+import com.android.systemui.util.concurrency.UiThreadContext;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.desktopmode.DesktopMode;
import com.android.wm.shell.pip.Pip;
@@ -136,7 +133,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
public void onSystemGestureExclusionChanged(int displayId,
Region systemGestureExclusion, Region unrestrictedOrNull) {
if (displayId == mDisplayId) {
- mMainExecutor.execute(() -> {
+ mUiThreadContext.getExecutor().execute(() -> {
mExcludeRegion.set(systemGestureExclusion);
mUnrestrictedExcludeRegion.set(unrestrictedOrNull != null
? unrestrictedOrNull : systemGestureExclusion);
@@ -215,8 +212,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
private final Point mDisplaySize = new Point();
private final int mDisplayId;
- private final Executor mMainExecutor;
- private final Handler mMainHandler;
+ private final UiThreadContext mUiThreadContext;
private final Executor mBackgroundExecutor;
private final Rect mPipExcludedBounds = new Rect();
@@ -411,8 +407,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
OverviewProxyService overviewProxyService,
SysUiState sysUiState,
PluginManager pluginManager,
- @Main Executor executor,
- @Main Handler handler,
+ @BackPanelUiThread UiThreadContext uiThreadContext,
@Background Executor backgroundExecutor,
UserTracker userTracker,
NavigationModeController navigationModeController,
@@ -428,8 +423,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
Provider<LightBarController> lightBarControllerProvider) {
mContext = context;
mDisplayId = context.getDisplayId();
- mMainExecutor = executor;
- mMainHandler = handler;
+ mUiThreadContext = uiThreadContext;
mBackgroundExecutor = backgroundExecutor;
mUserTracker = userTracker;
mOverviewProxyService = overviewProxyService;
@@ -478,7 +472,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
ViewConfiguration.getLongPressTimeout());
mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver(
- mMainHandler, mContext, this::onNavigationSettingsChanged);
+ mUiThreadContext.getHandler(), mContext, this::onNavigationSettingsChanged);
updateCurrentUserResources();
}
@@ -564,13 +558,15 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
mIsAttached = true;
mOverviewProxyService.addCallback(mQuickSwitchListener);
mSysUiState.addCallback(mSysUiStateCallback);
- mInputManager.registerInputDeviceListener(mInputDeviceListener, mMainHandler);
- int [] inputDevices = mInputManager.getInputDeviceIds();
+ mInputManager.registerInputDeviceListener(
+ mInputDeviceListener,
+ mUiThreadContext.getHandler());
+ int[] inputDevices = mInputManager.getInputDeviceIds();
for (int inputDeviceId : inputDevices) {
mInputDeviceListener.onInputDeviceAdded(inputDeviceId);
}
updateIsEnabled();
- mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
+ mUserTracker.addCallback(mUserChangedCallback, mUiThreadContext.getExecutor());
}
/**
@@ -617,6 +613,10 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
}
private void updateIsEnabled() {
+ mUiThreadContext.runWithScissors(this::updateIsEnabledInner);
+ }
+
+ private void updateIsEnabledInner() {
try {
Trace.beginSection("EdgeBackGestureHandler#updateIsEnabled");
@@ -661,12 +661,12 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
TaskStackChangeListeners.getInstance().registerTaskStackListener(
mTaskStackListener);
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
- mMainExecutor::execute, mOnPropertiesChangedListener);
+ mUiThreadContext.getExecutor()::execute, mOnPropertiesChangedListener);
mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener(
mOnIsInPipStateChangedListener));
mDesktopModeOptional.ifPresent(
dm -> dm.addDesktopGestureExclusionRegionListener(
- mDesktopCornersChangedListener, mMainExecutor));
+ mDesktopCornersChangedListener, mUiThreadContext.getExecutor()));
try {
mWindowManagerService.registerSystemGestureExclusionListener(
@@ -677,8 +677,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
// Register input event receiver
mInputMonitor = new InputMonitorCompat("edge-swipe", mDisplayId);
- mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(),
- Choreographer.getInstance(), this::onInputEvent);
+ mInputEventReceiver = mInputMonitor.getInputReceiver(mUiThreadContext.getLooper(),
+ mUiThreadContext.getChoreographer(), this::onInputEvent);
// Add a nav bar panel window
resetEdgeBackPlugin();
@@ -773,7 +773,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
mUseMLModel = newState;
if (mUseMLModel) {
- Assert.isMainThread();
+ mUiThreadContext.isCurrentThread();
if (mMLModelIsLoading) {
Log.d(TAG, "Model tried to load while already loading.");
return;
@@ -804,12 +804,13 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
}
BackGestureTfClassifierProvider finalProvider = provider;
Map<String, Integer> finalVocab = vocab;
- mMainExecutor.execute(() -> onMLModelLoadFinished(finalProvider, finalVocab, threshold));
+ mUiThreadContext.getExecutor().execute(
+ () -> onMLModelLoadFinished(finalProvider, finalVocab, threshold));
}
private void onMLModelLoadFinished(BackGestureTfClassifierProvider provider,
Map<String, Integer> vocab, float threshold) {
- Assert.isMainThread();
+ mUiThreadContext.isCurrentThread();
mMLModelIsLoading = false;
if (!mUseMLModel) {
// This can happen if the user disables Gesture Nav while the model is loading.
@@ -1291,7 +1292,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
updateBackAnimationThresholds();
if (mLightBarControllerProvider.get() != null) {
mBackAnimation.setStatusBarCustomizer((appearance) -> {
- mMainExecutor.execute(() ->
+ mUiThreadContext.getExecutor().execute(() ->
mLightBarControllerProvider.get()
.customizeStatusBarAppearance(appearance));
});
@@ -1308,8 +1309,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
private final OverviewProxyService mOverviewProxyService;
private final SysUiState mSysUiState;
private final PluginManager mPluginManager;
- private final Executor mExecutor;
- private final Handler mHandler;
+ private final UiThreadContext mUiThreadContext;
private final Executor mBackgroundExecutor;
private final UserTracker mUserTracker;
private final NavigationModeController mNavigationModeController;
@@ -1327,29 +1327,27 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
@Inject
public Factory(OverviewProxyService overviewProxyService,
- SysUiState sysUiState,
- PluginManager pluginManager,
- @Main Executor executor,
- @Main Handler handler,
- @Background Executor backgroundExecutor,
- UserTracker userTracker,
- NavigationModeController navigationModeController,
- BackPanelController.Factory backPanelControllerFactory,
- ViewConfiguration viewConfiguration,
- WindowManager windowManager,
- IWindowManager windowManagerService,
- InputManager inputManager,
- Optional<Pip> pipOptional,
- Optional<DesktopMode> desktopModeOptional,
- FalsingManager falsingManager,
- Provider<BackGestureTfClassifierProvider>
- backGestureTfClassifierProviderProvider,
- Provider<LightBarController> lightBarControllerProvider) {
+ SysUiState sysUiState,
+ PluginManager pluginManager,
+ @BackPanelUiThread UiThreadContext uiThreadContext,
+ @Background Executor backgroundExecutor,
+ UserTracker userTracker,
+ NavigationModeController navigationModeController,
+ BackPanelController.Factory backPanelControllerFactory,
+ ViewConfiguration viewConfiguration,
+ WindowManager windowManager,
+ IWindowManager windowManagerService,
+ InputManager inputManager,
+ Optional<Pip> pipOptional,
+ Optional<DesktopMode> desktopModeOptional,
+ FalsingManager falsingManager,
+ Provider<BackGestureTfClassifierProvider>
+ backGestureTfClassifierProviderProvider,
+ Provider<LightBarController> lightBarControllerProvider) {
mOverviewProxyService = overviewProxyService;
mSysUiState = sysUiState;
mPluginManager = pluginManager;
- mExecutor = executor;
- mHandler = handler;
+ mUiThreadContext = uiThreadContext;
mBackgroundExecutor = backgroundExecutor;
mUserTracker = userTracker;
mNavigationModeController = navigationModeController;
@@ -1367,26 +1365,26 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack
/** Construct a {@link EdgeBackGestureHandler}. */
public EdgeBackGestureHandler create(Context context) {
- return new EdgeBackGestureHandler(
- context,
- mOverviewProxyService,
- mSysUiState,
- mPluginManager,
- mExecutor,
- mHandler,
- mBackgroundExecutor,
- mUserTracker,
- mNavigationModeController,
- mBackPanelControllerFactory,
- mViewConfiguration,
- mWindowManager,
- mWindowManagerService,
- mInputManager,
- mPipOptional,
- mDesktopModeOptional,
- mFalsingManager,
- mBackGestureTfClassifierProviderProvider,
- mLightBarControllerProvider);
+ return mUiThreadContext.runWithScissors(
+ () -> new EdgeBackGestureHandler(
+ context,
+ mOverviewProxyService,
+ mSysUiState,
+ mPluginManager,
+ mUiThreadContext,
+ mBackgroundExecutor,
+ mUserTracker,
+ mNavigationModeController,
+ mBackPanelControllerFactory,
+ mViewConfiguration,
+ mWindowManager,
+ mWindowManagerService,
+ mInputManager,
+ mPipOptional,
+ mDesktopModeOptional,
+ mFalsingManager,
+ mBackGestureTfClassifierProviderProvider,
+ mLightBarControllerProvider));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index d00916a1c1a8..c742f6413022 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -610,7 +610,7 @@ public final class KeyboardShortcuts {
keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
calendarIcon,
- KeyEvent.KEYCODE_L,
+ KeyEvent.KEYCODE_K,
KeyEvent.META_META_ON));
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index 43258972ea34..1449e535c279 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -23,6 +23,7 @@ import android.telephony.satellite.NtnSignalStrengthCallback
import android.telephony.satellite.SatelliteManager
import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS
import android.telephony.satellite.SatelliteModemStateCallback
+import android.telephony.satellite.SatelliteSupportedStateCallback
import androidx.annotation.VisibleForTesting
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
@@ -35,6 +36,7 @@ import com.android.systemui.log.core.MessagePrinter
import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog
import com.android.systemui.statusbar.pipeline.dagger.VerboseDeviceBasedSatelliteInputLog
import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported
@@ -162,60 +164,6 @@ constructor(
@get:VisibleForTesting
val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown)
- init {
- satelliteManager = satelliteManagerOpt.getOrNull()
-
- isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
-
- if (satelliteManager != null) {
- // First, check that satellite is supported on this device
- scope.launch {
- val waitTime = ensureMinUptime(systemClock, MIN_UPTIME)
- if (waitTime > 0) {
- logBuffer.i({ long1 = waitTime }) {
- "Waiting $long1 ms before checking for satellite support"
- }
- delay(waitTime)
- }
-
- satelliteSupport.value = satelliteManager.checkSatelliteSupported()
-
- logBuffer.i(
- { str1 = satelliteSupport.value.toString() },
- { "Checked for system support. support=$str1" },
- )
-
- // We only need to check location availability if this mode is supported
- if (satelliteSupport.value is Supported) {
- isSatelliteAllowedForCurrentLocation.subscriptionCount
- .map { it > 0 }
- .distinctUntilChanged()
- .collectLatest { hasSubscribers ->
- if (hasSubscribers) {
- /*
- * As there is no listener available for checking satellite allowed,
- * we must poll. Defaulting to polling at most once every hour while
- * active. Subsequent OOS events will restart the job, so a flaky
- * connection might cause more frequent checks.
- */
- while (true) {
- logBuffer.i {
- "requestIsCommunicationAllowedForCurrentLocation"
- }
- checkIsSatelliteAllowed()
- delay(POLLING_INTERVAL_MS)
- }
- }
- }
- }
- }
- } else {
- logBuffer.i { "Satellite manager is null" }
-
- satelliteSupport.value = NotSupported
- }
- }
-
/**
* Note that we are given an "unbound" [TelephonyManager] (meaning it was not created with a
* specific `subscriptionId`). Therefore this is the radio power state of the
@@ -269,6 +217,134 @@ constructor(
}
.onStart { emit(Unit) }
+ init {
+ satelliteManager = satelliteManagerOpt.getOrNull()
+
+ isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
+
+ if (satelliteManager != null) {
+ // Outer scope launch allows us to delay until MIN_UPTIME
+ scope.launch {
+ // First, check that satellite is supported on this device
+ satelliteSupport.value = checkSatelliteSupportAfterMinUptime(satelliteManager)
+ logBuffer.i(
+ { str1 = satelliteSupport.value.toString() },
+ { "Checked for system support. support=$str1" },
+ )
+
+ // Second, launch a job to poll for service availability based on location
+ scope.launch { pollForAvailabilityBasedOnLocation() }
+
+ // Third, register a listener to let us know if there are changes to support
+ scope.launch { listenForChangesToSatelliteSupport(satelliteManager) }
+ }
+ } else {
+ logBuffer.i { "Satellite manager is null" }
+ satelliteSupport.value = NotSupported
+ }
+ }
+
+ private suspend fun checkSatelliteSupportAfterMinUptime(
+ sm: SatelliteManager
+ ): SatelliteSupport {
+ val waitTime = ensureMinUptime(systemClock, MIN_UPTIME)
+ if (waitTime > 0) {
+ logBuffer.i({ long1 = waitTime }) {
+ "Waiting $long1 ms before checking for satellite support"
+ }
+ delay(waitTime)
+ }
+
+ return sm.checkSatelliteSupported()
+ }
+
+ /*
+ * As there is no listener available for checking satellite allowed, we must poll the service.
+ * Defaulting to polling at most once every 20m while active. Subsequent OOS events will restart
+ * the job, so a flaky connection might cause more frequent checks.
+ */
+ private suspend fun pollForAvailabilityBasedOnLocation() {
+ satelliteSupport
+ .whenSupported(
+ supported = ::isSatelliteAllowedHasListener,
+ orElse = flowOf(false),
+ retrySignal = telephonyProcessCrashedEvent,
+ )
+ .collectLatest { hasSubscribers ->
+ if (hasSubscribers) {
+ while (true) {
+ logBuffer.i { "requestIsCommunicationAllowedForCurrentLocation" }
+ checkIsSatelliteAllowed()
+ delay(POLLING_INTERVAL_MS)
+ }
+ }
+ }
+ }
+
+ /**
+ * Register a callback with [SatelliteManager] to let us know if there is a change in satellite
+ * support. This job restarts if there is a crash event detected.
+ *
+ * Note that the structure of this method looks similar to [whenSupported], but since we want
+ * this callback registered even when it is [NotSupported], we just mimic the structure here.
+ */
+ private suspend fun listenForChangesToSatelliteSupport(sm: SatelliteManager) {
+ telephonyProcessCrashedEvent.collectLatest {
+ satelliteIsSupportedCallback.collect { supported ->
+ if (supported) {
+ satelliteSupport.value = Supported(sm)
+ } else {
+ satelliteSupport.value = NotSupported
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback version of [checkSatelliteSupported]. This flow should be retried on the same
+ * [telephonyProcessCrashedEvent] signal, but does not require a [SupportedSatelliteManager],
+ * since it specifically watches for satellite support.
+ */
+ private val satelliteIsSupportedCallback: Flow<Boolean> =
+ if (satelliteManager == null) {
+ flowOf(false)
+ } else {
+ conflatedCallbackFlow {
+ val callback = SatelliteSupportedStateCallback { supported ->
+ logBuffer.i {
+ "onSatelliteSupportedStateChanged: " +
+ "${if (supported) "supported" else "not supported"}"
+ }
+ trySend(supported)
+ }
+
+ var registered = false
+ try {
+ satelliteManager.registerForSupportedStateChanged(
+ bgDispatcher.asExecutor(),
+ callback
+ )
+ registered = true
+ } catch (e: Exception) {
+ logBuffer.e("error registering for supported state change", e)
+ }
+
+ awaitClose {
+ if (registered) {
+ satelliteManager.unregisterForSupportedStateChanged(callback)
+ }
+ }
+ }
+ }
+
+ /**
+ * Signal that we should start polling [checkIsSatelliteAllowed]. We only need to poll if there
+ * are active listeners to [isSatelliteAllowedForCurrentLocation]
+ */
+ @SuppressWarnings("unused")
+ private fun isSatelliteAllowedHasListener(sm: SupportedSatelliteManager): Flow<Boolean> =
+ isSatelliteAllowedForCurrentLocation.subscriptionCount.map { it > 0 }.distinctUntilChanged()
+
override val connectionState =
satelliteSupport
.whenSupported(
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt
index 83e3428bb95f..a7abb6b5f1d3 100644
--- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt
@@ -19,6 +19,7 @@ import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.os.Process
+import android.view.Choreographer
import com.android.systemui.Dependency
import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
@@ -31,6 +32,12 @@ import dagger.Module
import dagger.Provides
import java.util.concurrent.Executor
import javax.inject.Named
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class BackPanelUiThread
/** Dagger Module for classes found within the concurrent package. */
@Module
@@ -106,6 +113,39 @@ object SysUIConcurrencyModule {
return looper
}
+ @Provides
+ @SysUISingleton
+ @BackPanelUiThread
+ fun provideBackPanelUiThreadContext(
+ @Main mainLooper: Looper,
+ @Main mainHandler: Handler,
+ @Main mainExecutor: Executor
+ ): UiThreadContext {
+ return if (Flags.edgeBackGestureHandlerThread()) {
+ val thread =
+ HandlerThread("BackPanelUiThread", Process.THREAD_PRIORITY_DISPLAY).apply {
+ start()
+ looper.setSlowLogThresholdMs(
+ LONG_SLOW_DISPATCH_THRESHOLD,
+ LONG_SLOW_DELIVERY_THRESHOLD
+ )
+ }
+ UiThreadContext(
+ thread.looper,
+ thread.threadHandler,
+ thread.threadExecutor,
+ thread.threadHandler.runWithScissors { Choreographer.getInstance() }
+ )
+ } else {
+ UiThreadContext(
+ mainLooper,
+ mainHandler,
+ mainExecutor,
+ mainHandler.runWithScissors { Choreographer.getInstance() }
+ )
+ }
+ }
+
/**
* Background Handler.
*
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt
new file mode 100644
index 000000000000..8c8c686dddcc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.util.concurrency
+
+import android.os.Handler
+import android.os.Looper
+import android.view.Choreographer
+import com.android.systemui.util.Assert
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicReference
+
+private const val DEFAULT_TIMEOUT = 150L
+
+class UiThreadContext(
+ val looper: Looper,
+ val handler: Handler,
+ val executor: Executor,
+ val choreographer: Choreographer
+) {
+ fun isCurrentThread() {
+ Assert.isCurrentThread(looper)
+ }
+
+ fun <T> runWithScissors(block: () -> T): T {
+ return handler.runWithScissors(block)
+ }
+
+ fun runWithScissors(block: Runnable) {
+ handler.runWithScissors(block, DEFAULT_TIMEOUT)
+ }
+}
+
+fun <T> Handler.runWithScissors(block: () -> T): T {
+ val returnedValue = AtomicReference<T>()
+ runWithScissors({ returnedValue.set(block()) }, DEFAULT_TIMEOUT)
+ return returnedValue.get()!!
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
index c6b0dc542087..e0f64b4f7dc2 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
@@ -18,8 +18,6 @@ package com.android.systemui.volume.domain.interactor
import android.bluetooth.BluetoothAdapter
import android.media.AudioDeviceInfo
-import android.media.AudioDeviceInfo.TYPE_WIRED_HEADPHONES
-import android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.media.BluetoothMediaDevice
@@ -81,30 +79,32 @@ constructor(
val isInAudioSharing: Flow<Boolean> = audioSharingRepository.inAudioSharing
private fun AudioDeviceInfo.toAudioOutputDevice(): AudioOutputDevice {
- if (type == TYPE_WIRED_HEADPHONES || type == TYPE_WIRED_HEADSET) {
- return AudioOutputDevice.Wired(
- name = productName.toString(),
- icon = deviceIconInteractor.loadIcon(type),
- )
- }
- val cachedBluetoothDevice: CachedBluetoothDevice? =
- if (address.isEmpty() || localBluetoothManager == null || bluetoothAdapter == null) {
- null
- } else {
- val remoteDevice = bluetoothAdapter.getRemoteDevice(address)
- localBluetoothManager.cachedDeviceManager.findDevice(remoteDevice)
+ if (
+ BluetoothAdapter.checkBluetoothAddress(address) &&
+ localBluetoothManager != null &&
+ bluetoothAdapter != null
+ ) {
+ val remoteDevice = bluetoothAdapter.getRemoteDevice(address)
+ localBluetoothManager.cachedDeviceManager.findDevice(remoteDevice)?.let {
+ device: CachedBluetoothDevice ->
+ return AudioOutputDevice.Bluetooth(
+ name = device.name,
+ icon = deviceIconInteractor.loadIcon(device),
+ cachedBluetoothDevice = device,
+ )
}
- return cachedBluetoothDevice?.let {
- AudioOutputDevice.Bluetooth(
- name = it.name,
- icon = deviceIconInteractor.loadIcon(it),
- cachedBluetoothDevice = it,
- )
}
- ?: AudioOutputDevice.BuiltIn(
+ // Built-in device has an empty address
+ if (address.isNotEmpty()) {
+ return AudioOutputDevice.Wired(
name = productName.toString(),
icon = deviceIconInteractor.loadIcon(type),
)
+ }
+ return AudioOutputDevice.BuiltIn(
+ name = productName.toString(),
+ icon = deviceIconInteractor.loadIcon(type),
+ )
}
private fun MediaDevice.toAudioOutputDevice(): AudioOutputDevice {
@@ -115,7 +115,8 @@ constructor(
icon = icon,
cachedBluetoothDevice = cachedDevice,
)
- deviceType == MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE ->
+ deviceType == MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE ||
+ deviceType == MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE ->
AudioOutputDevice.Wired(
name = name,
icon = icon,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt
index 99f956489bc3..3da725b9a51f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt
@@ -98,12 +98,12 @@ constructor(
private fun showNewVolumePanel() {
activityStarter.dismissKeyguardThenExecute(
- {
+ /* action = */ {
volumePanelGlobalStateInteractor.setVisible(true)
false
},
- {},
- true
+ /* cancel = */ {},
+ /* afterKeyguardGone = */ true,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 1167fce7524b..f46cfdc280fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -383,11 +383,25 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
}
if (testCase.isFaceOnly) {
- val expectedIconAsset = R.raw.face_dialog_authenticating
+ val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation)
+ val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+ val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark)
+
+ val expectedIconAsset =
+ if (shouldPulseAnimation!!) {
+ if (lastPulseLightToDark!!) {
+ R.drawable.face_dialog_pulse_dark_to_light
+ } else {
+ R.drawable.face_dialog_pulse_light_to_dark
+ }
+ } else {
+ R.drawable.face_dialog_pulse_dark_to_light
+ }
assertThat(iconAsset).isEqualTo(expectedIconAsset)
assertThat(iconContentDescriptionId)
.isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating)
assertThat(shouldAnimateIconView).isEqualTo(true)
+ assertThat(shouldRepeatAnimation).isEqualTo(true)
}
if (testCase.isCoex) {
@@ -409,11 +423,26 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
}
} else {
// implicit flow
- val expectedIconAsset = R.raw.face_dialog_authenticating
+ val shouldRepeatAnimation by
+ collectLastValue(iconViewModel.shouldRepeatAnimation)
+ val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+ val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark)
+
+ val expectedIconAsset =
+ if (shouldPulseAnimation!!) {
+ if (lastPulseLightToDark!!) {
+ R.drawable.face_dialog_pulse_dark_to_light
+ } else {
+ R.drawable.face_dialog_pulse_light_to_dark
+ }
+ } else {
+ R.drawable.face_dialog_pulse_dark_to_light
+ }
assertThat(iconAsset).isEqualTo(expectedIconAsset)
assertThat(iconContentDescriptionId)
.isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating)
assertThat(shouldAnimateIconView).isEqualTo(true)
+ assertThat(shouldRepeatAnimation).isEqualTo(true)
}
}
}
@@ -503,9 +532,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
}
if (testCase.isFaceOnly) {
+ val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation)
+ val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+
+ assertThat(shouldPulseAnimation!!).isEqualTo(false)
assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_error)
assertThat(iconContentDescriptionId).isEqualTo(R.string.keyguard_face_failed)
assertThat(shouldAnimateIconView).isEqualTo(true)
+ assertThat(shouldRepeatAnimation).isEqualTo(false)
// Clear error, go to idle
errorJob.join()
@@ -514,6 +548,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
assertThat(iconContentDescriptionId)
.isEqualTo(R.string.biometric_dialog_face_icon_description_idle)
assertThat(shouldAnimateIconView).isEqualTo(true)
+ assertThat(shouldRepeatAnimation).isEqualTo(false)
}
if (testCase.isCoex) {
@@ -596,10 +631,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
// If co-ex, using implicit flow (explicit flow always requires confirmation)
if (testCase.isFaceOnly || testCase.isCoex) {
+ val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation)
+ val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+
+ assertThat(shouldPulseAnimation!!).isEqualTo(false)
assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark)
assertThat(iconContentDescriptionId)
.isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
assertThat(shouldAnimateIconView).isEqualTo(true)
+ assertThat(shouldRepeatAnimation).isEqualTo(false)
}
}
}
@@ -621,10 +661,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
)
if (testCase.isFaceOnly) {
+ val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation)
+ val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+
+ assertThat(shouldPulseAnimation!!).isEqualTo(false)
assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_wink_from_dark)
assertThat(iconContentDescriptionId)
.isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
assertThat(shouldAnimateIconView).isEqualTo(true)
+ assertThat(shouldRepeatAnimation).isEqualTo(false)
}
// explicit flow because confirmation requested
@@ -666,10 +711,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
viewModel.confirmAuthenticated()
if (testCase.isFaceOnly) {
+ val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation)
+ val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+
+ assertThat(shouldPulseAnimation!!).isEqualTo(false)
assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark)
assertThat(iconContentDescriptionId)
.isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed)
assertThat(shouldAnimateIconView).isEqualTo(true)
+ assertThat(shouldRepeatAnimation).isEqualTo(false)
}
// explicit flow because confirmation requested
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 3bf4173cd7c7..3906c40593f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -191,7 +191,7 @@ class MediaDataProcessorTest : SysuiTestCase() {
@Before
fun setup() {
- whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+ whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
staticMockSession =
ExtendedMockito.mockitoSession()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt
index e5d3082bb245..80ebe56453ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt
@@ -376,7 +376,7 @@ class MediaViewControllerTest : SysuiTestCase() {
@Test
fun attachPlayer_seekBarDisabled_seekBarVisibilityIsSetToInvisible() {
- whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+ whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
mediaViewController.attachPlayer(viewHolder)
getEnabledChangeListener().onEnabledChanged(enabled = true)
@@ -388,7 +388,7 @@ class MediaViewControllerTest : SysuiTestCase() {
@Test
fun attachPlayer_seekBarEnabled_seekBarVisible() {
- whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+ whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
mediaViewController.attachPlayer(viewHolder)
getEnabledChangeListener().onEnabledChanged(enabled = true)
@@ -399,7 +399,7 @@ class MediaViewControllerTest : SysuiTestCase() {
@Test
fun attachPlayer_seekBarStatusUpdate_seekBarVisibilityChanges() {
- whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+ whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
mediaViewController.attachPlayer(viewHolder)
getEnabledChangeListener().onEnabledChanged(enabled = true)
@@ -415,7 +415,7 @@ class MediaViewControllerTest : SysuiTestCase() {
@Test
fun attachPlayer_notScrubbing_scrubbingViewsGone() {
- whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+ whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
mediaViewController.attachPlayer(viewHolder)
mediaViewController.canShowScrubbingTime = true
@@ -435,7 +435,7 @@ class MediaViewControllerTest : SysuiTestCase() {
@Test
fun setIsScrubbing_noSemanticActions_scrubbingViewsGone() {
- whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+ whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
mediaViewController.attachPlayer(viewHolder)
mediaViewController.canShowScrubbingTime = false
@@ -454,7 +454,7 @@ class MediaViewControllerTest : SysuiTestCase() {
@Test
fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() {
- whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+ whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
mediaViewController.attachPlayer(viewHolder)
mediaViewController.setUpNextButtonInfo(true)
@@ -476,7 +476,7 @@ class MediaViewControllerTest : SysuiTestCase() {
@Test
fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() {
- whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+ whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
mediaViewController.attachPlayer(viewHolder)
mediaViewController.setUpNextButtonInfo(false)
@@ -498,7 +498,7 @@ class MediaViewControllerTest : SysuiTestCase() {
@Test
fun setIsScrubbing_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() {
- whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+ whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
mediaViewController.attachPlayer(viewHolder)
mediaViewController.setUpNextButtonInfo(true)
@@ -524,7 +524,7 @@ class MediaViewControllerTest : SysuiTestCase() {
@Test
fun setIsScrubbing_trueThenFalse_reservePrevAndNextButtons() {
- whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+ whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
mediaViewController.attachPlayer(viewHolder)
mediaViewController.setUpNextButtonInfo(true, ConstraintSet.INVISIBLE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 02f53b6846e8..d24d87c6f57a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -34,6 +34,7 @@ import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAI
import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
import android.telephony.satellite.SatelliteManager.SatelliteException
import android.telephony.satellite.SatelliteModemStateCallback
+import android.telephony.satellite.SatelliteSupportedStateCallback
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -327,7 +328,6 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() {
@Test
fun satelliteNotSupported_listenersAreNotRegistered() =
testScope.runTest {
- setupDefaultRepo()
// GIVEN satellite is not supported
setUpRepo(
uptime = MIN_UPTIME,
@@ -345,6 +345,110 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() {
}
@Test
+ fun satelliteSupported_registersCallbackForStateChanges() =
+ testScope.runTest {
+ // GIVEN a supported satellite manager.
+ setupDefaultRepo()
+ runCurrent()
+
+ // THEN the repo registers for state changes of satellite support
+ verify(satelliteManager, times(1)).registerForSupportedStateChanged(any(), any())
+ }
+
+ @Test
+ fun satelliteNotSupported_registersCallbackForStateChanges() =
+ testScope.runTest {
+ // GIVEN satellite is not supported
+ setUpRepo(
+ uptime = MIN_UPTIME,
+ satMan = satelliteManager,
+ satelliteSupported = false,
+ )
+
+ runCurrent()
+ // THEN the repo registers for state changes of satellite support
+ verify(satelliteManager, times(1)).registerForSupportedStateChanged(any(), any())
+ }
+
+ @Test
+ fun satelliteSupportedStateChangedCallbackThrows_doesNotCrash() =
+ testScope.runTest {
+ // GIVEN, satellite manager throws when registering for supported state changes
+ whenever(satelliteManager.registerForSupportedStateChanged(any(), any()))
+ .thenThrow(IllegalStateException())
+
+ // GIVEN a supported satellite manager.
+ setupDefaultRepo()
+ runCurrent()
+
+ // THEN a listener for satellite supported changed can attempt to register,
+ // with no crash
+ verify(satelliteManager).registerForSupportedStateChanged(any(), any())
+ }
+
+ @Test
+ fun satelliteSupported_supportIsLost_unregistersListeners() =
+ testScope.runTest {
+ // GIVEN a supported satellite manager.
+ setupDefaultRepo()
+ runCurrent()
+
+ val callback =
+ withArgCaptor<SatelliteSupportedStateCallback> {
+ verify(satelliteManager).registerForSupportedStateChanged(any(), capture())
+ }
+
+ // WHEN data is requested from the repo
+ val connectionState by collectLastValue(underTest.connectionState)
+ val signalStrength by collectLastValue(underTest.signalStrength)
+
+ // THEN the listeners are registered
+ verify(satelliteManager, times(1)).registerForModemStateChanged(any(), any())
+ verify(satelliteManager, times(1)).registerForNtnSignalStrengthChanged(any(), any())
+
+ // WHEN satellite support turns off
+ callback.onSatelliteSupportedStateChanged(false)
+ runCurrent()
+
+ // THEN listeners are unregistered
+ verify(satelliteManager, times(1)).unregisterForModemStateChanged(any())
+ verify(satelliteManager, times(1)).unregisterForNtnSignalStrengthChanged(any())
+ }
+
+ @Test
+ fun satelliteNotSupported_supportShowsUp_registersListeners() =
+ testScope.runTest {
+ // GIVEN satellite is not supported
+ setUpRepo(
+ uptime = MIN_UPTIME,
+ satMan = satelliteManager,
+ satelliteSupported = false,
+ )
+ runCurrent()
+
+ val callback =
+ withArgCaptor<SatelliteSupportedStateCallback> {
+ verify(satelliteManager).registerForSupportedStateChanged(any(), capture())
+ }
+
+ // WHEN data is requested from the repo
+ val connectionState by collectLastValue(underTest.connectionState)
+ val signalStrength by collectLastValue(underTest.signalStrength)
+
+ // THEN the listeners are not yet registered
+ verify(satelliteManager, times(0)).registerForModemStateChanged(any(), any())
+ verify(satelliteManager, times(0)).registerForNtnSignalStrengthChanged(any(), any())
+
+ // WHEN satellite support turns on
+ callback.onSatelliteSupportedStateChanged(true)
+ runCurrent()
+
+ // THEN listeners are registered
+ verify(satelliteManager, times(1)).registerForModemStateChanged(any(), any())
+ verify(satelliteManager, times(1)).registerForNtnSignalStrengthChanged(any(), any())
+ }
+
+ @Test
fun repoDoesNotCheckForSupportUntilMinUptime() =
testScope.runTest {
// GIVEN we init 100ms after sysui starts up
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt
index 3ac712918100..15ef26d58ece 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt
@@ -33,19 +33,22 @@ object TestAudioDevicesFactory {
)
}
- fun wiredDevice(deviceName: String = "wired"): AudioDeviceInfo {
+ fun wiredDevice(
+ deviceName: String = "wired",
+ deviceAddress: String = "card=1;device=0",
+ ): AudioDeviceInfo {
return AudioDeviceInfo(
AudioDevicePort.createForTesting(
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
deviceName,
- "",
+ deviceAddress,
)
)
}
fun bluetoothDevice(
deviceName: String = "bt",
- deviceAddress: String = "test_address",
+ deviceAddress: String = "00:43:A8:23:10:F0",
): AudioDeviceInfo {
return AudioDeviceInfo(
AudioDevicePort.createForTesting(
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 42f168bb4a6b..77decb6a52fa 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -1379,6 +1379,30 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ
}
}
+ @RequiresNoPermission
+ @Override
+ public boolean isMagnificationSystemUIConnected() {
+ if (svcConnTracingEnabled()) {
+ logTraceSvcConn("isMagnificationSystemUIConnected", "");
+ }
+ synchronized (mLock) {
+ if (!hasRightsToCurrentUserLocked()) {
+ return false;
+ }
+ if (!mSecurityPolicy.canControlMagnification(this)) {
+ return false;
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ MagnificationProcessor magnificationProcessor =
+ mSystemSupport.getMagnificationProcessor();
+ return magnificationProcessor.isMagnificationSystemUIConnected();
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
public boolean isMagnificationCallbackEnabled(int displayId) {
return mInvocationHandler.isMagnificationCallbackEnabled(displayId);
}
@@ -1925,6 +1949,11 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ
InvocationHandler.MSG_CLEAR_ACCESSIBILITY_CACHE);
}
+ public void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) {
+ mInvocationHandler
+ .notifyMagnificationSystemUIConnectionChangedLocked(connected);
+ }
+
public void notifyMagnificationChangedLocked(int displayId, @NonNull Region region,
@NonNull MagnificationConfig config) {
mInvocationHandler
@@ -1976,6 +2005,21 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ
return (mGenericMotionEventSources & eventSourceWithoutClass) != 0;
}
+ /**
+ * Called by the invocation handler to notify the service that the
+ * magnification systemui connection has changed.
+ */
+ private void notifyMagnificationSystemUIConnectionChangedInternal(boolean connected) {
+ final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
+ if (listener != null) {
+ try {
+ listener.onMagnificationSystemUIConnectionChanged(connected);
+ } catch (RemoteException re) {
+ Slog.e(LOG_TAG,
+ "Error sending magnification sysui connection changes to " + mService, re);
+ }
+ }
+ }
/**
* Called by the invocation handler to notify the service that the
@@ -2372,6 +2416,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ
private static final int MSG_BIND_INPUT = 12;
private static final int MSG_UNBIND_INPUT = 13;
private static final int MSG_START_INPUT = 14;
+ private static final int MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED = 15;
/** List of magnification callback states, mapping from displayId -> Boolean */
@GuardedBy("mlock")
@@ -2398,6 +2443,13 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ
notifyClearAccessibilityCacheInternal();
} break;
+ case MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED: {
+ final SomeArgs args = (SomeArgs) message.obj;
+ final boolean connected = args.argi1 == 1;
+ notifyMagnificationSystemUIConnectionChangedInternal(connected);
+ args.recycle();
+ } break;
+
case MSG_ON_MAGNIFICATION_CHANGED: {
final SomeArgs args = (SomeArgs) message.obj;
final Region region = (Region) args.arg1;
@@ -2455,6 +2507,15 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ
}
}
+ public void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.argi1 = connected ? 1 : 0;
+
+ final Message msg =
+ obtainMessage(MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED, args);
+ msg.sendToTarget();
+ }
+
public void notifyMagnificationChangedLocked(int displayId, @NonNull Region region,
@NonNull MagnificationConfig config) {
synchronized (mLock) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 20b727cd6f09..fe083389fa77 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1812,6 +1812,17 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
/**
+ * Called by the MagnificationController when the magnification systemui connection changes.
+ *
+ * @param connected Whether the connection is ready.
+ */
+ public void notifyMagnificationSystemUIConnectionChanged(boolean connected) {
+ synchronized (mLock) {
+ notifyMagnificationSystemUIConnectionChangedLocked(connected);
+ }
+ }
+
+ /**
* Called by the MagnificationController when the state of display
* magnification changes.
*
@@ -2243,6 +2254,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
mProxyManager.clearCacheLocked();
}
+ private void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) {
+ final AccessibilityUserState state = getCurrentUserStateLocked();
+ for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
+ final AccessibilityServiceConnection service = state.mBoundServices.get(i);
+ service.notifyMagnificationSystemUIConnectionChangedLocked(connected);
+ }
+ }
+
private void notifyMagnificationChangedLocked(int displayId, @NonNull Region region,
@NonNull MagnificationConfig config) {
final AccessibilityUserState state = getCurrentUserStateLocked();
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
index 4cb3d247edb0..420bac759ea6 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
@@ -489,6 +489,14 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon
/** @throws UnsupportedOperationException since a proxy does not need magnification */
@RequiresNoPermission
@Override
+ public boolean isMagnificationSystemUIConnected() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("isMagnificationSystemUIConnected is not"
+ + " supported");
+ }
+
+ /** @throws UnsupportedOperationException since a proxy does not need magnification */
+ @RequiresNoPermission
+ @Override
public boolean isMagnificationCallbackEnabled(int displayId) {
throw new UnsupportedOperationException("isMagnificationCallbackEnabled is not supported");
}
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java
index 0719ebaba707..7f4c808b2251 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java
@@ -126,6 +126,7 @@ public class MagnificationConnectionManager implements
@ConnectionState
private int mConnectionState = DISCONNECTED;
+ ConnectionStateChangedCallback mConnectionStateChangedCallback = null;
private static final int WAIT_CONNECTION_TIMEOUT_MILLIS = 100;
@@ -264,6 +265,9 @@ public class MagnificationConnectionManager implements
}
}
}
+ if (mConnectionStateChangedCallback != null) {
+ mConnectionStateChangedCallback.onConnectionStateChanged(connection != null);
+ }
}
/**
@@ -271,7 +275,7 @@ public class MagnificationConnectionManager implements
*/
public boolean isConnected() {
synchronized (mLock) {
- return mConnectionWrapper != null;
+ return mConnectionWrapper != null && mConnectionState == CONNECTED;
}
}
@@ -1344,4 +1348,8 @@ public class MagnificationConnectionManager implements
}
}
}
+
+ interface ConnectionStateChangedCallback {
+ void onConnectionStateChanged(boolean connected);
+ }
}
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
index 76367a2b11c3..9b7884711a6d 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
@@ -828,6 +828,8 @@ public class MagnificationController implements MagnificationConnectionManager.C
mMagnificationConnectionManager = new MagnificationConnectionManager(mContext,
mLock, this, mAms.getTraceManager(),
mScaleProvider);
+ mMagnificationConnectionManager.mConnectionStateChangedCallback =
+ mAms::notifyMagnificationSystemUIConnectionChanged;
}
return mMagnificationConnectionManager;
}
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
index ed8f1ab3a1b2..603683906d06 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
@@ -147,6 +147,10 @@ public class MagnificationProcessor {
return false;
}
+ public boolean isMagnificationSystemUIConnected() {
+ return mController.getMagnificationConnectionManager().isConnected();
+ }
+
private boolean setScaleAndCenterForFullScreenMagnification(int displayId, float scale,
float centerX, float centerY, boolean animate, int id) {
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 215f6402fa76..4a9900763a94 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -29,6 +29,7 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOAR
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS;
import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery;
+import static android.companion.virtualdevice.flags.Flags.intentInterceptionActionMatchingFix;
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
@@ -1478,7 +1479,13 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
synchronized (mVirtualDeviceLock) {
boolean hasInterceptedIntent = false;
for (Map.Entry<IBinder, IntentFilter> interceptor : mIntentInterceptors.entrySet()) {
- if (interceptor.getValue().match(
+ IntentFilter intentFilter = interceptor.getValue();
+ // Explicitly match the actions because the intent filter will match any intent
+ // without an explicit action. If the intent has no action, then require that there
+ // are no actions specified in the filter either.
+ boolean explicitActionMatch = !intentInterceptionActionMatchingFix()
+ || intent.getAction() != null || intentFilter.countActions() == 0;
+ if (explicitActionMatch && intentFilter.match(
intent.getAction(), intent.getType(), intent.getScheme(), intent.getData(),
intent.getCategories(), TAG) >= 0) {
try {
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index 966478e33c73..a61925732256 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -138,12 +138,6 @@ public class PackageWatchdog {
static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10);
- // Time needed to apply mitigation
- private static final String MITIGATION_WINDOW_MS =
- "persist.device_config.configuration.mitigation_window_ms";
- @VisibleForTesting
- static final long DEFAULT_MITIGATION_WINDOW_MS = TimeUnit.SECONDS.toMillis(5);
-
// Threshold level at which or above user might experience significant disruption.
private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
"persist.device_config.configuration.major_user_impact_level_threshold";
@@ -216,9 +210,6 @@ public class PackageWatchdog {
@GuardedBy("mLock")
private boolean mSyncRequired = false;
- @GuardedBy("mLock")
- private long mLastMitigation = -1000000;
-
@FunctionalInterface
@VisibleForTesting
interface SystemClock {
@@ -409,16 +400,6 @@ public class PackageWatchdog {
Slog.w(TAG, "Could not resolve a list of failing packages");
return;
}
- synchronized (mLock) {
- final long now = mSystemClock.uptimeMillis();
- if (Flags.recoverabilityDetection()) {
- if (now >= mLastMitigation
- && (now - mLastMitigation) < getMitigationWindowMs()) {
- Slog.i(TAG, "Skipping onPackageFailure mitigation");
- return;
- }
- }
- }
mLongTaskHandler.post(() -> {
synchronized (mLock) {
if (mAllObservers.isEmpty()) {
@@ -519,17 +500,10 @@ public class PackageWatchdog {
int currentObserverImpact,
int mitigationCount) {
if (currentObserverImpact < getUserImpactLevelLimit()) {
- synchronized (mLock) {
- mLastMitigation = mSystemClock.uptimeMillis();
- }
currentObserverToNotify.execute(versionedPackage, failureReason, mitigationCount);
}
}
- private long getMitigationWindowMs() {
- return SystemProperties.getLong(MITIGATION_WINDOW_MS, DEFAULT_MITIGATION_WINDOW_MS);
- }
-
/**
* Called when the system server boots. If the system server is detected to be in a boot loop,
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 8d7a1c9f8228..8eef71e603b2 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -22,6 +22,8 @@ import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT;
import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
+import static android.os.Process.ROOT_UID;
+import static android.os.Process.SYSTEM_UID;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -422,6 +424,10 @@ public final class PendingIntentRecord extends IIntentSender.Stub {
})
public static BackgroundStartPrivileges getDefaultBackgroundStartPrivileges(
int callingUid, @Nullable String callingPackage) {
+ if (callingUid == ROOT_UID || callingUid == SYSTEM_UID) {
+ // root and system must always opt in explicitly
+ return BackgroundStartPrivileges.ALLOW_FGS;
+ }
boolean isChangeEnabledForApp = callingPackage != null ? CompatChanges.isChangeEnabled(
DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingPackage,
UserHandle.getUserHandleForUid(callingUid)) : CompatChanges.isChangeEnabled(
diff --git a/services/core/java/com/android/server/audio/AudioPolicyFacade.java b/services/core/java/com/android/server/audio/AudioPolicyFacade.java
index 02e80d611f3f..f652b33b3fd3 100644
--- a/services/core/java/com/android/server/audio/AudioPolicyFacade.java
+++ b/services/core/java/com/android/server/audio/AudioPolicyFacade.java
@@ -16,12 +16,14 @@
package com.android.server.audio;
+import com.android.media.permission.INativePermissionController;
/**
* Facade to IAudioPolicyService which fulfills AudioService dependencies.
* See @link{IAudioPolicyService.aidl}
*/
public interface AudioPolicyFacade {
-
public boolean isHotwordStreamSupported(boolean lookbackAudio);
+ public INativePermissionController getPermissionController();
+ public void registerOnStartTask(Runnable r);
}
diff --git a/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java
new file mode 100644
index 000000000000..5ea3c4bf538d
--- /dev/null
+++ b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java
@@ -0,0 +1,149 @@
+/*
+ * 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.audio;
+
+import android.annotation.Nullable;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.media.permission.INativePermissionController;
+import com.android.media.permission.UidPackageState;
+import com.android.server.pm.pkg.PackageState;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+/** Responsible for synchronizing system server permission state to the native audioserver. */
+public class AudioServerPermissionProvider {
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private INativePermissionController mDest;
+
+ @GuardedBy("mLock")
+ private final Map<Integer, Set<String>> mPackageMap;
+
+ /**
+ * @param appInfos - PackageState for all apps on the device, used to populate init state
+ */
+ public AudioServerPermissionProvider(Collection<PackageState> appInfos) {
+ // Initialize the package state
+ mPackageMap = generatePackageMappings(appInfos);
+ }
+
+ /**
+ * Called whenever audioserver starts (or started before us)
+ *
+ * @param pc - The permission controller interface from audioserver, which we push updates to
+ */
+ public void onServiceStart(@Nullable INativePermissionController pc) {
+ if (pc == null) return;
+ synchronized (mLock) {
+ mDest = pc;
+ resetNativePackageState();
+ }
+ }
+
+ /**
+ * Called when a package is added or removed
+ *
+ * @param uid - uid of modified package (only app-id matters)
+ * @param packageName - the (new) packageName
+ * @param isRemove - true if the package is being removed, false if it is being added
+ */
+ public void onModifyPackageState(int uid, String packageName, boolean isRemove) {
+ // No point in maintaining package mappings for uids of different users
+ uid = UserHandle.getAppId(uid);
+ synchronized (mLock) {
+ // Update state
+ Set<String> packages;
+ if (!isRemove) {
+ packages = mPackageMap.computeIfAbsent(uid, unused -> new ArraySet(1));
+ packages.add(packageName);
+ } else {
+ packages = mPackageMap.get(uid);
+ if (packages != null) {
+ packages.remove(packageName);
+ if (packages.isEmpty()) mPackageMap.remove(uid);
+ }
+ }
+ // Push state to destination
+ if (mDest == null) {
+ return;
+ }
+ var state = new UidPackageState();
+ state.uid = uid;
+ state.packageNames = packages != null ? List.copyOf(packages) : Collections.emptyList();
+ try {
+ mDest.updatePackagesForUid(state);
+ } catch (RemoteException e) {
+ // We will re-init the state when the service comes back up
+ mDest = null;
+ }
+ }
+ }
+
+ /** Called when full syncing package state to audioserver. */
+ @GuardedBy("mLock")
+ private void resetNativePackageState() {
+ if (mDest == null) return;
+ List<UidPackageState> states =
+ mPackageMap.entrySet().stream()
+ .map(
+ entry -> {
+ UidPackageState state = new UidPackageState();
+ state.uid = entry.getKey();
+ state.packageNames = List.copyOf(entry.getValue());
+ return state;
+ })
+ .toList();
+ try {
+ mDest.populatePackagesForUids(states);
+ } catch (RemoteException e) {
+ // We will re-init the state when the service comes back up
+ mDest = null;
+ }
+ }
+
+ /**
+ * Aggregation operation on all package states list: groups by states by app-id and merges the
+ * packageName for each state into an ArraySet.
+ */
+ private static Map<Integer, Set<String>> generatePackageMappings(
+ Collection<PackageState> appInfos) {
+ Collector<PackageState, Object, Set<String>> reducer =
+ Collectors.mapping(
+ (PackageState p) -> p.getPackageName(),
+ Collectors.toCollection(() -> new ArraySet(1)));
+
+ return appInfos.stream()
+ .collect(
+ Collectors.groupingBy(
+ /* predicate */ (PackageState p) -> p.getAppId(),
+ /* factory */ HashMap::new,
+ /* downstream collector */ reducer));
+ }
+}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 2a23b9ca522e..ef65b2523024 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -31,6 +31,10 @@ import static android.Manifest.permission.MODIFY_PHONE_STATE;
import static android.Manifest.permission.QUERY_AUDIO_STATE;
import static android.Manifest.permission.WRITE_SETTINGS;
import static android.app.BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT;
+import static android.content.Intent.ACTION_PACKAGE_ADDED;
+import static android.content.Intent.ACTION_PACKAGE_REMOVED;
+import static android.content.Intent.EXTRA_ARCHIVAL;
+import static android.content.Intent.EXTRA_REPLACING;
import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET;
import static android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER;
import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP;
@@ -57,7 +61,9 @@ import static android.provider.Settings.Secure.VOLUME_HUSH_OFF;
import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.media.audio.Flags.absVolumeIndexFix;
import static com.android.media.audio.Flags.alarmMinVolumeZero;
+import static com.android.media.audio.Flags.audioserverPermissions;
import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
import static com.android.media.audio.Flags.setStreamVolumeOrder;
@@ -239,15 +245,18 @@ import com.android.internal.os.SomeArgs;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.Preconditions;
import com.android.server.EventLogTags;
+import com.android.server.LocalManagerRegistry;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.audio.AudioServiceEvents.DeviceVolumeEvent;
import com.android.server.audio.AudioServiceEvents.PhoneStateEvent;
import com.android.server.audio.AudioServiceEvents.VolChangedBroadcastEvent;
import com.android.server.audio.AudioServiceEvents.VolumeEvent;
+import com.android.server.pm.PackageManagerLocal;
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.UserManagerInternal.UserRestrictionsListener;
import com.android.server.pm.UserManagerService;
+import com.android.server.pm.pkg.PackageState;
import com.android.server.utils.EventLogger;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -272,6 +281,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
@@ -302,6 +312,8 @@ public class AudioService extends IAudioService.Stub
private final SettingsAdapter mSettings;
private final AudioPolicyFacade mAudioPolicy;
+ private final AudioServerPermissionProvider mPermissionProvider;
+
private final MusicFxHelper mMusicFxHelper;
/** Debug audio mode */
@@ -632,6 +644,17 @@ public class AudioService extends IAudioService.Stub
// If absolute volume is supported in AVRCP device
private volatile boolean mAvrcpAbsVolSupported = false;
+ private final Object mCachedAbsVolDrivingStreamsLock = new Object();
+ // Contains for all the device types which support absolute volume the current streams that
+ // are driving the volume changes
+ @GuardedBy("mCachedAbsVolDrivingStreamsLock")
+ private final HashMap<Integer, Integer> mCachedAbsVolDrivingStreams = new HashMap<>(
+ Map.of(AudioSystem.DEVICE_OUT_BLE_HEADSET, AudioSystem.STREAM_MUSIC,
+ AudioSystem.DEVICE_OUT_BLE_SPEAKER, AudioSystem.STREAM_MUSIC,
+ AudioSystem.DEVICE_OUT_BLE_BROADCAST, AudioSystem.STREAM_MUSIC,
+ AudioSystem.DEVICE_OUT_HEARING_AID, AudioSystem.STREAM_MUSIC
+ ));
+
/**
* Default stream type used for volume control in the absence of playback
* e.g. user on homescreen, no app playing anything, presses hardware volume buttons, this
@@ -1009,14 +1032,22 @@ public class AudioService extends IAudioService.Stub
public Lifecycle(Context context) {
super(context);
+ var audioserverLifecycleExecutor = Executors.newSingleThreadExecutor();
+ var audioPolicyFacade = new DefaultAudioPolicyFacade(audioserverLifecycleExecutor);
mService = new AudioService(context,
AudioSystemAdapter.getDefaultAdapter(),
SystemServerAdapter.getDefaultAdapter(context),
SettingsAdapter.getDefaultAdapter(),
new AudioVolumeGroupHelper(),
- new DefaultAudioPolicyFacade(r -> r.run()),
- null);
-
+ audioPolicyFacade,
+ null,
+ context.getSystemService(AppOpsManager.class),
+ PermissionEnforcer.fromContext(context),
+ audioserverPermissions() ?
+ initializeAudioServerPermissionProvider(
+ context, audioPolicyFacade, audioserverLifecycleExecutor) :
+ null
+ );
}
@Override
@@ -1093,25 +1124,6 @@ public class AudioService extends IAudioService.Stub
/**
* @param context
* @param audioSystem Adapter for {@link AudioSystem}
- * @param systemServer Adapter for privileged functionality for system server components
- * @param settings Adapter for {@link Settings}
- * @param audioVolumeGroupHelper Adapter for {@link AudioVolumeGroup}
- * @param audioPolicy Interface of a facade to IAudioPolicyManager
- * @param looper Looper to use for the service's message handler. If this is null, an
- * {@link AudioSystemThread} is created as the messaging thread instead.
- */
- public AudioService(Context context, AudioSystemAdapter audioSystem,
- SystemServerAdapter systemServer, SettingsAdapter settings,
- AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy,
- @Nullable Looper looper) {
- this (context, audioSystem, systemServer, settings, audioVolumeGroupHelper,
- audioPolicy, looper, context.getSystemService(AppOpsManager.class),
- PermissionEnforcer.fromContext(context));
- }
-
- /**
- * @param context
- * @param audioSystem Adapter for {@link AudioSystem}
* @param systemServer Adapter for privilieged functionality for system server components
* @param settings Adapter for {@link Settings}
* @param audioVolumeGroupHelper Adapter for {@link AudioVolumeGroup}
@@ -1125,13 +1137,16 @@ public class AudioService extends IAudioService.Stub
public AudioService(Context context, AudioSystemAdapter audioSystem,
SystemServerAdapter systemServer, SettingsAdapter settings,
AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy,
- @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer) {
+ @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer,
+ /* @NonNull */ AudioServerPermissionProvider permissionProvider) {
super(enforcer);
sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()"));
mContext = context;
mContentResolver = context.getContentResolver();
mAppOps = appOps;
+ mPermissionProvider = permissionProvider;
+
mAudioSystem = audioSystem;
mSystemServer = systemServer;
mAudioVolumeGroupHelper = audioVolumeGroupHelper;
@@ -1472,6 +1487,13 @@ public class AudioService extends IAudioService.Stub
// check on volume initialization
checkVolumeRangeInitialization("AudioService()");
+
+ synchronized (mCachedAbsVolDrivingStreamsLock) {
+ mCachedAbsVolDrivingStreams.forEach((dev, stream) -> {
+ mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", /*enabled=*/true,
+ stream);
+ });
+ }
}
private SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionChangedListener =
@@ -1911,6 +1933,14 @@ public class AudioService extends IAudioService.Stub
}
onIndicateSystemReady();
+
+ synchronized (mCachedAbsVolDrivingStreamsLock) {
+ mCachedAbsVolDrivingStreams.forEach((dev, stream) -> {
+ mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", /*enabled=*/true,
+ stream);
+ });
+ }
+
// indicate the end of reconfiguration phase to audio HAL
AudioSystem.setParameters("restarting=false");
@@ -3737,8 +3767,10 @@ public class AudioService extends IAudioService.Stub
int newIndex = mStreamStates[streamType].getIndex(device);
+ int streamToDriveAbsVol = absVolumeIndexFix() ? getBluetoothContextualVolumeStream() :
+ AudioSystem.STREAM_MUSIC;
// Check if volume update should be send to AVRCP
- if (streamTypeAlias == AudioSystem.STREAM_MUSIC
+ if (streamTypeAlias == streamToDriveAbsVol
&& AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
&& (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
if (DEBUG_VOL) {
@@ -4530,6 +4562,8 @@ public class AudioService extends IAudioService.Stub
+ featureSpatialAudioHeadtrackingLowLatency());
pw.println("\tandroid.media.audio.focusFreezeTestApi:"
+ focusFreezeTestApi());
+ pw.println("\tcom.android.media.audio.audioserverPermissions:"
+ + audioserverPermissions());
pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:"
+ disablePrescaleAbsoluteVolume());
pw.println("\tcom.android.media.audio.setStreamVolumeOrder:"
@@ -4540,6 +4574,8 @@ public class AudioService extends IAudioService.Stub
+ scoManagedByAudio());
pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:"
+ vgsVssSyncMuteOrder());
+ pw.println("\tcom.android.media.audio.absVolumeIndexFix:"
+ + absVolumeIndexFix());
}
private void dumpAudioMode(PrintWriter pw) {
@@ -4735,7 +4771,9 @@ public class AudioService extends IAudioService.Stub
}
}
- if (streamTypeAlias == AudioSystem.STREAM_MUSIC
+ int streamToDriveAbsVol = absVolumeIndexFix() ? getBluetoothContextualVolumeStream() :
+ AudioSystem.STREAM_MUSIC;
+ if (streamTypeAlias == streamToDriveAbsVol
&& AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
&& (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
if (DEBUG_VOL) {
@@ -6184,6 +6222,17 @@ public class AudioService extends IAudioService.Stub
setLeAudioVolumeOnModeUpdate(mode, device, streamAlias, index, maxIndex);
+ synchronized (mCachedAbsVolDrivingStreamsLock) {
+ mCachedAbsVolDrivingStreams.replaceAll((absDev, stream) -> {
+ int streamToDriveAbs = getBluetoothContextualVolumeStream();
+ if (stream != streamToDriveAbs) {
+ mAudioSystem.setDeviceAbsoluteVolumeEnabled(absDev, /*address=*/
+ "", /*enabled*/true, streamToDriveAbs);
+ }
+ return streamToDriveAbs;
+ });
+ }
+
// when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
// connections not started by the application changing the mode when pid changes
mDeviceBroker.postSetModeOwner(mode, pid, uid);
@@ -8105,6 +8154,10 @@ public class AudioService extends IAudioService.Stub
return mAudioVolumeGroup.name();
}
+ public int getId() {
+ return mAudioVolumeGroup.getId();
+ }
+
/**
* Volume group with non null minimum index are considered as non mutable, thus
* bijectivity is broken with potential associated stream type.
@@ -8755,24 +8808,30 @@ public class AudioService extends IAudioService.Stub
}
private int getAbsoluteVolumeIndex(int index) {
- /* Special handling for Bluetooth Absolute Volume scenario
- * If we send full audio gain, some accessories are too loud even at its lowest
- * volume. We are not able to enumerate all such accessories, so here is the
- * workaround from phone side.
- * Pre-scale volume at lowest volume steps 1 2 and 3.
- * For volume step 0, set audio gain to 0 as some accessories won't mute on their end.
- */
- if (index == 0) {
- // 0% for volume 0
- index = 0;
- } else if (!disablePrescaleAbsoluteVolume() && index > 0 && index <= 3) {
- // Pre-scale for volume steps 1 2 and 3
- index = (int) (mIndexMax * mPrescaleAbsoluteVolume[index - 1]) / 10;
+ if (absVolumeIndexFix()) {
+ // The attenuation is applied in the APM. No need to manipulate the index here
+ return index;
} else {
- // otherwise, full gain
- index = (mIndexMax + 5) / 10;
+ /* Special handling for Bluetooth Absolute Volume scenario
+ * If we send full audio gain, some accessories are too loud even at its lowest
+ * volume. We are not able to enumerate all such accessories, so here is the
+ * workaround from phone side.
+ * Pre-scale volume at lowest volume steps 1 2 and 3.
+ * For volume step 0, set audio gain to 0 as some accessories won't mute on their
+ * end.
+ */
+ if (index == 0) {
+ // 0% for volume 0
+ index = 0;
+ } else if (!disablePrescaleAbsoluteVolume() && index > 0 && index <= 3) {
+ // Pre-scale for volume steps 1 2 and 3
+ index = (int) (mIndexMax * mPrescaleAbsoluteVolume[index - 1]) / 10;
+ } else {
+ // otherwise, full gain
+ index = (mIndexMax + 5) / 10;
+ }
+ return index;
}
- return index;
}
private void setStreamVolumeIndex(int index, int device) {
@@ -8783,6 +8842,11 @@ public class AudioService extends IAudioService.Stub
&& !isFullyMuted()) {
index = 1;
}
+
+ if (DEBUG_VOL) {
+ Log.d(TAG, "setStreamVolumeIndexAS(" + mStreamType + ", " + index + ", " + device
+ + ")");
+ }
mAudioSystem.setStreamVolumeIndexAS(mStreamType, index, device);
}
@@ -8794,14 +8858,24 @@ public class AudioService extends IAudioService.Stub
} else if (isAbsoluteVolumeDevice(device)
|| isA2dpAbsoluteVolumeDevice(device)
|| AudioSystem.isLeAudioDeviceType(device)) {
- index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
+ // do not change the volume logic for dynamic abs behavior devices like HDMI
+ if (absVolumeIndexFix() && isAbsoluteVolumeDevice(device)) {
+ index = getAbsoluteVolumeIndex((mIndexMax + 5) / 10);
+ } else {
+ index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10);
+ }
} else if (isFullVolumeDevice(device)) {
index = (mIndexMax + 5)/10;
} else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) {
- index = (mIndexMax + 5)/10;
+ if (absVolumeIndexFix()) {
+ index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10);
+ } else {
+ index = (mIndexMax + 5) / 10;
+ }
} else {
index = (getIndex(device) + 5)/10;
}
+
setStreamVolumeIndex(index, device);
}
@@ -8819,11 +8893,22 @@ public class AudioService extends IAudioService.Stub
|| isA2dpAbsoluteVolumeDevice(device)
|| AudioSystem.isLeAudioDeviceType(device)) {
isAbsoluteVolume = true;
- index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
+ // do not change the volume logic for dynamic abs behavior devices
+ // like HDMI
+ if (absVolumeIndexFix() && isAbsoluteVolumeDevice(device)) {
+ index = getAbsoluteVolumeIndex((mIndexMax + 5) / 10);
+ } else {
+ index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10);
+ }
} else if (isFullVolumeDevice(device)) {
index = (mIndexMax + 5)/10;
} else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) {
- index = (mIndexMax + 5)/10;
+ if (absVolumeIndexFix()) {
+ isAbsoluteVolume = true;
+ index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10);
+ } else {
+ index = (mIndexMax + 5) / 10;
+ }
} else {
index = (mIndexMap.valueAt(i) + 5)/10;
}
@@ -9820,6 +9905,27 @@ public class AudioService extends IAudioService.Stub
/*package*/ void setAvrcpAbsoluteVolumeSupported(boolean support) {
mAvrcpAbsVolSupported = support;
+ if (absVolumeIndexFix()) {
+ int a2dpDev = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP;
+ synchronized (mCachedAbsVolDrivingStreamsLock) {
+ mCachedAbsVolDrivingStreams.compute(a2dpDev, (dev, stream) -> {
+ if (stream != null && !mAvrcpAbsVolSupported) {
+ mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/
+ "", /*enabled*/false, AudioSystem.DEVICE_NONE);
+ return null;
+ }
+ // For A2DP and AVRCP we need to set the driving stream based on the
+ // BT contextual stream. Hence, we need to make sure in adjustStreamVolume
+ // and setStreamVolume that the driving abs volume stream is consistent.
+ int streamToDriveAbs = getBluetoothContextualVolumeStream();
+ if (stream == null || stream != streamToDriveAbs) {
+ mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/
+ "", /*enabled*/true, streamToDriveAbs);
+ }
+ return streamToDriveAbs;
+ });
+ }
+ }
sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE,
AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, 0,
mStreamStates[AudioSystem.STREAM_MUSIC], 0);
@@ -11836,6 +11942,45 @@ public class AudioService extends IAudioService.Stub
private static final String mMetricsId = MediaMetrics.Name.AUDIO_SERVICE
+ MediaMetrics.SEPARATOR;
+ private static AudioServerPermissionProvider initializeAudioServerPermissionProvider(
+ Context context, AudioPolicyFacade audioPolicy, Executor audioserverExecutor) {
+ Collection<PackageState> packageStates = null;
+ try (PackageManagerLocal.UnfilteredSnapshot snapshot =
+ LocalManagerRegistry.getManager(PackageManagerLocal.class)
+ .withUnfilteredSnapshot()) {
+ packageStates = snapshot.getPackageStates().values();
+ }
+ var provider = new AudioServerPermissionProvider(packageStates);
+ audioPolicy.registerOnStartTask(() -> {
+ provider.onServiceStart(audioPolicy.getPermissionController());
+ });
+
+ // Set up event listeners
+ IntentFilter packageUpdateFilter = new IntentFilter();
+ packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED);
+ packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED);
+ packageUpdateFilter.addDataScheme("package");
+
+ context.registerReceiverForAllUsers(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ String pkgName = intent.getData().getEncodedSchemeSpecificPart();
+ int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID);
+ if (intent.getBooleanExtra(EXTRA_REPLACING, false) ||
+ intent.getBooleanExtra(EXTRA_ARCHIVAL, false)) return;
+ if (action.equals(ACTION_PACKAGE_ADDED)) {
+ audioserverExecutor.execute(() ->
+ provider.onModifyPackageState(uid, pkgName, false /* isRemoved */));
+ } else if (action.equals(ACTION_PACKAGE_REMOVED)) {
+ audioserverExecutor.execute(() ->
+ provider.onModifyPackageState(uid, pkgName, true /* isRemoved */));
+ }
+ }
+ }, packageUpdateFilter, null, null); // main thread is fine, since dispatch on executor
+ return provider;
+ }
+
// Inform AudioFlinger of our device's low RAM attribute
private static void readAndSetLowRamDevice()
{
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 7202fa286453..7f4bc74bd59e 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -598,6 +598,21 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback,
}
/**
+ * Same as {@link AudioSystem#setDeviceAbsoluteVolumeEnabled(int, String, boolean, int)}
+ * @param nativeDeviceType the internal device type for which absolute volume is
+ * enabled/disabled
+ * @param address the address of the device for which absolute volume is enabled/disabled
+ * @param enabled whether the absolute volume is enabled/disabled
+ * @param streamToDriveAbs the stream that is controlling the absolute volume
+ * @return status of indicating the success of this operation
+ */
+ public int setDeviceAbsoluteVolumeEnabled(int nativeDeviceType, @NonNull String address,
+ boolean enabled, int streamToDriveAbs) {
+ return AudioSystem.setDeviceAbsoluteVolumeEnabled(nativeDeviceType, address, enabled,
+ streamToDriveAbs);
+ }
+
+ /**
* Same as {@link AudioSystem#registerPolicyMixes(ArrayList, boolean)}
* @param mixes
* @param register
diff --git a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
index 75febbc1cf9c..09701e49a8ac 100644
--- a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
+++ b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
@@ -16,10 +16,15 @@
package com.android.server.audio;
+import android.annotation.Nullable;
import android.media.IAudioPolicyService;
+import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
+import com.android.media.permission.INativePermissionController;
+
+import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Function;
@@ -43,6 +48,7 @@ public class DefaultAudioPolicyFacade implements AudioPolicyFacade {
(Function<IBinder, IAudioPolicyService>)
IAudioPolicyService.Stub::asInterface,
e);
+ mServiceHolder.registerOnStartTask(i -> Binder.allowBlocking(i.asBinder()));
}
@Override
@@ -55,4 +61,23 @@ public class DefaultAudioPolicyFacade implements AudioPolicyFacade {
throw new IllegalStateException();
}
}
+
+ @Override
+ public @Nullable INativePermissionController getPermissionController() {
+ IAudioPolicyService ap = mServiceHolder.checkService();
+ if (ap == null) return null;
+ try {
+ var res = Objects.requireNonNull(ap.getPermissionController());
+ Binder.allowBlocking(res.asBinder());
+ return res;
+ } catch (RemoteException e) {
+ mServiceHolder.attemptClear(ap.asBinder());
+ return null;
+ }
+ }
+
+ @Override
+ public void registerOnStartTask(Runnable task) {
+ mServiceHolder.registerOnStartTask(unused -> task.run());
+ }
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index dbd1e65e8b0f..6e027c6d44c4 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1029,6 +1029,10 @@ public class HdmiControlService extends SystemService {
/** Helper method for sending feature discovery command */
private void reportFeatures(boolean isTvDeviceSetting) {
+ // <Report Features> should only be sent for HDMI 2.0
+ if (getCecVersion() < HdmiControlManager.HDMI_CEC_VERSION_2_0) {
+ return;
+ }
// check if tv device is enabled for tv device specific RC profile setting
if (isTvDeviceSetting) {
if (isTvDeviceEnabled()) {
diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
index dd6433d98553..82ecb4acb197 100644
--- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
+++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
@@ -16,12 +16,16 @@
package com.android.server.inputmethod;
+import android.annotation.AnyThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.annotation.WorkerThread;
import android.content.Context;
import android.content.pm.UserInfo;
import android.os.Handler;
+import android.os.Process;
+import android.util.IntArray;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
@@ -29,6 +33,10 @@ import com.android.internal.inputmethod.DirectBootAwareness;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
+import java.util.ArrayList;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
/**
* Provides accesses to per-user additional {@link android.view.inputmethod.InputMethodSubtype}
* persistent storages.
@@ -38,6 +46,152 @@ final class AdditionalSubtypeMapRepository {
@NonNull
private static final SparseArray<AdditionalSubtypeMap> sPerUserMap = new SparseArray<>();
+ record WriteTask(@UserIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap,
+ @NonNull InputMethodMap inputMethodMap) {
+ }
+
+ static final class SingleThreadedBackgroundWriter {
+ /**
+ * A {@link ReentrantLock} used to guard {@link #mPendingTasks} and {@link #mRemovedUsers}.
+ */
+ @NonNull
+ private final ReentrantLock mLock = new ReentrantLock();
+ /**
+ * A {@link Condition} associated with {@link #mLock} for producer to unblock consumer.
+ */
+ @NonNull
+ private final Condition mLockNotifier = mLock.newCondition();
+
+ @GuardedBy("mLock")
+ @NonNull
+ private final SparseArray<WriteTask> mPendingTasks = new SparseArray<>();
+
+ @GuardedBy("mLock")
+ private final IntArray mRemovedUsers = new IntArray();
+
+ @NonNull
+ private final Thread mWriterThread = new Thread("android.ime.as") {
+
+ /**
+ * Waits until the next data has come then return the result after filtering out any
+ * already removed users.
+ *
+ * @return A list of {@link WriteTask} to be written into persistent storage
+ */
+ @WorkerThread
+ private ArrayList<WriteTask> fetchNextTasks() {
+ final SparseArray<WriteTask> tasks;
+ final IntArray removedUsers;
+ mLock.lock();
+ try {
+ while (true) {
+ if (mPendingTasks.size() != 0) {
+ tasks = mPendingTasks.clone();
+ mPendingTasks.clear();
+ if (mRemovedUsers.size() == 0) {
+ removedUsers = null;
+ } else {
+ removedUsers = mRemovedUsers.clone();
+ }
+ break;
+ }
+ mLockNotifier.awaitUninterruptibly();
+ }
+ } finally {
+ mLock.unlock();
+ }
+ final int size = tasks.size();
+ final ArrayList<WriteTask> result = new ArrayList<>(size);
+ for (int i = 0; i < size; ++i) {
+ final int userId = tasks.keyAt(i);
+ if (removedUsers != null && removedUsers.contains(userId)) {
+ continue;
+ }
+ result.add(tasks.valueAt(i));
+ }
+ return result;
+ }
+
+ @WorkerThread
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ while (true) {
+ final ArrayList<WriteTask> tasks = fetchNextTasks();
+ tasks.forEach(task -> AdditionalSubtypeUtils.save(
+ task.subtypeMap, task.inputMethodMap, task.userId));
+ }
+ }
+ };
+
+ /**
+ * Schedules a write operation
+ *
+ * @param userId the target user ID of this operation
+ * @param subtypeMap {@link AdditionalSubtypeMap} to be saved
+ * @param inputMethodMap {@link InputMethodMap} to be used to filter our {@code subtypeMap}
+ */
+ @AnyThread
+ void scheduleWriteTask(@UserIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap,
+ @NonNull InputMethodMap inputMethodMap) {
+ final var task = new WriteTask(userId, subtypeMap, inputMethodMap);
+ mLock.lock();
+ try {
+ if (mRemovedUsers.contains(userId)) {
+ return;
+ }
+ mPendingTasks.put(userId, task);
+ mLockNotifier.signalAll();
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ /**
+ * Called back when a user is being created.
+ *
+ * @param userId The user ID to be created
+ */
+ @AnyThread
+ void onUserCreated(@UserIdInt int userId) {
+ mLock.lock();
+ try {
+ for (int i = mRemovedUsers.size() - 1; i >= 0; --i) {
+ if (mRemovedUsers.get(i) == userId) {
+ mRemovedUsers.remove(i);
+ }
+ }
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ /**
+ * Called back when a user is being removed. Any pending task will be effectively canceled
+ * if the user is removed before the task is fulfilled.
+ *
+ * @param userId The user ID to be removed
+ */
+ @AnyThread
+ void onUserRemoved(@UserIdInt int userId) {
+ mLock.lock();
+ try {
+ mRemovedUsers.add(userId);
+ mPendingTasks.remove(userId);
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ void startThread() {
+ mWriterThread.start();
+ }
+ }
+
+ private static final SingleThreadedBackgroundWriter sWriter =
+ new SingleThreadedBackgroundWriter();
+
/**
* Not intended to be instantiated.
*/
@@ -64,9 +218,11 @@ final class AdditionalSubtypeMapRepository {
return;
}
sPerUserMap.put(userId, map);
- // TODO: Offload this to a background thread.
- // TODO: Skip if the previous data is exactly the same as new one.
- AdditionalSubtypeUtils.save(map, inputMethodMap, userId);
+ sWriter.scheduleWriteTask(userId, map, inputMethodMap);
+ }
+
+ static void startWriterThread() {
+ sWriter.startThread();
}
static void initialize(@NonNull Handler handler, @NonNull Context context) {
@@ -78,6 +234,7 @@ final class AdditionalSubtypeMapRepository {
@Override
public void onUserCreated(UserInfo user, @Nullable Object token) {
final int userId = user.id;
+ sWriter.onUserCreated(userId);
handler.post(() -> {
synchronized (ImfLock.class) {
if (!sPerUserMap.contains(userId)) {
@@ -99,6 +256,7 @@ final class AdditionalSubtypeMapRepository {
@Override
public void onUserRemoved(UserInfo user) {
final int userId = user.id;
+ sWriter.onUserRemoved(userId);
handler.post(() -> {
synchronized (ImfLock.class) {
sPerUserMap.remove(userId);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 5843d72f346a..7513c40a1f90 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1547,6 +1547,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
getPackageManagerForUser(mContext, currentUserId),
newSettings.getEnabledInputMethodList());
+
+ final var unused = SystemServerInitThreadPool.submit(
+ AdditionalSubtypeMapRepository::startWriterThread,
+ "Start AdditionalSubtypeMapRepository's writer thread");
}
}
}
diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java
index 363684f618cc..09605fefe80e 100644
--- a/services/core/java/com/android/server/media/MediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java
@@ -59,7 +59,7 @@ abstract class MediaRoute2Provider {
public abstract void requestCreateSession(
long requestId,
String packageName,
- String routeId,
+ String routeOriginalId,
@Nullable Bundle sessionHints,
@RoutingSessionInfo.TransferReason int transferReason,
@NonNull UserHandle transferInitiatorUserHandle,
@@ -77,13 +77,15 @@ abstract class MediaRoute2Provider {
long requestId,
@NonNull UserHandle transferInitiatorUserHandle,
@NonNull String transferInitiatorPackageName,
- String sessionId,
- String routeId,
+ String sessionOriginalId,
+ String routeOriginalId,
@RoutingSessionInfo.TransferReason int transferReason);
- public abstract void setRouteVolume(long requestId, String routeId, int volume);
- public abstract void setSessionVolume(long requestId, String sessionId, int volume);
- public abstract void prepareReleaseSession(@NonNull String sessionId);
+ public abstract void setRouteVolume(long requestId, String routeOriginalId, int volume);
+
+ public abstract void setSessionVolume(long requestId, String sessionOriginalId, int volume);
+
+ public abstract void prepareReleaseSession(@NonNull String sessionUniqueId);
@NonNull
public String getUniqueId() {
@@ -197,8 +199,8 @@ abstract class MediaRoute2Provider {
*/
public final long mRequestId;
- /** The {@link MediaRoute2Info#getId() id} of the target route. */
- @NonNull public final String mTargetRouteId;
+ /** The {@link MediaRoute2Info#getOriginalId()} original id} of the target route. */
+ @NonNull public final String mTargetOriginalRouteId;
@RoutingSessionInfo.TransferReason public final int mTransferReason;
@@ -209,23 +211,23 @@ abstract class MediaRoute2Provider {
SessionCreationOrTransferRequest(
long requestId,
- @NonNull String routeId,
+ @NonNull String targetOriginalRouteId,
@RoutingSessionInfo.TransferReason int transferReason,
@NonNull UserHandle transferInitiatorUserHandle,
@NonNull String transferInitiatorPackageName) {
mRequestId = requestId;
- mTargetRouteId = routeId;
+ mTargetOriginalRouteId = targetOriginalRouteId;
mTransferReason = transferReason;
mTransferInitiatorUserHandle = transferInitiatorUserHandle;
mTransferInitiatorPackageName = transferInitiatorPackageName;
}
public boolean isTargetRoute(@Nullable MediaRoute2Info route2Info) {
- return route2Info != null && mTargetRouteId.equals(route2Info.getId());
+ return route2Info != null && mTargetOriginalRouteId.equals(route2Info.getOriginalId());
}
- public boolean isTargetRouteIdInList(@NonNull List<String> routesList) {
- return routesList.stream().anyMatch(mTargetRouteId::equals);
+ public boolean isTargetRouteIdInList(@NonNull List<String> routeOriginalIdList) {
+ return routeOriginalIdList.stream().anyMatch(mTargetOriginalRouteId::equals);
}
}
}
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
index 386657e99e36..71cbcb91100f 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
@@ -103,13 +103,14 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider
public void requestCreateSession(
long requestId,
String packageName,
- String routeId,
+ String routeOriginalId,
Bundle sessionHints,
@RoutingSessionInfo.TransferReason int transferReason,
@NonNull UserHandle transferInitiatorUserHandle,
@NonNull String transferInitiatorPackageName) {
if (mConnectionReady) {
- mActiveConnection.requestCreateSession(requestId, packageName, routeId, sessionHints);
+ mActiveConnection.requestCreateSession(
+ requestId, packageName, routeOriginalId, sessionHints);
updateBinding();
}
}
@@ -153,35 +154,35 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider
long requestId,
@NonNull UserHandle transferInitiatorUserHandle,
@NonNull String transferInitiatorPackageName,
- String sessionId,
- String routeId,
+ String sessionOriginalId,
+ String routeOriginalId,
@RoutingSessionInfo.TransferReason int transferReason) {
if (mConnectionReady) {
- mActiveConnection.transferToRoute(requestId, sessionId, routeId);
+ mActiveConnection.transferToRoute(requestId, sessionOriginalId, routeOriginalId);
}
}
@Override
- public void setRouteVolume(long requestId, String routeId, int volume) {
+ public void setRouteVolume(long requestId, String routeOriginalId, int volume) {
if (mConnectionReady) {
- mActiveConnection.setRouteVolume(requestId, routeId, volume);
+ mActiveConnection.setRouteVolume(requestId, routeOriginalId, volume);
updateBinding();
}
}
@Override
- public void setSessionVolume(long requestId, String sessionId, int volume) {
+ public void setSessionVolume(long requestId, String sessionOriginalId, int volume) {
if (mConnectionReady) {
- mActiveConnection.setSessionVolume(requestId, sessionId, volume);
+ mActiveConnection.setSessionVolume(requestId, sessionOriginalId, volume);
updateBinding();
}
}
@Override
- public void prepareReleaseSession(@NonNull String sessionId) {
+ public void prepareReleaseSession(@NonNull String sessionUniqueId) {
synchronized (mLock) {
for (RoutingSessionInfo session : mSessionInfos) {
- if (TextUtils.equals(session.getId(), sessionId)) {
+ if (TextUtils.equals(session.getId(), sessionUniqueId)) {
mSessionInfos.remove(session);
mReleasingSessions.add(session);
break;
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 76930a003e46..6b409ee6f482 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -158,20 +158,20 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
public void requestCreateSession(
long requestId,
String packageName,
- String routeId,
+ String routeOriginalId,
Bundle sessionHints,
@RoutingSessionInfo.TransferReason int transferReason,
@NonNull UserHandle transferInitiatorUserHandle,
@NonNull String transferInitiatorPackageName) {
// Assume a router without MODIFY_AUDIO_ROUTING permission can't request with
// a route ID different from the default route ID. The service should've filtered.
- if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
+ if (TextUtils.equals(routeOriginalId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
mCallback.onSessionCreated(this, requestId, mDefaultSessionInfo);
return;
}
if (!Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
- if (TextUtils.equals(routeId, mSelectedRouteId)) {
+ if (TextUtils.equals(routeOriginalId, mSelectedRouteId)) {
RoutingSessionInfo currentSessionInfo;
synchronized (mLock) {
currentSessionInfo = mSessionInfos.get(0);
@@ -192,7 +192,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
mPendingSessionCreationOrTransferRequest =
new SessionCreationOrTransferRequest(
requestId,
- routeId,
+ routeOriginalId,
RoutingSessionInfo.TRANSFER_REASON_FALLBACK,
transferInitiatorUserHandle,
transferInitiatorPackageName);
@@ -204,7 +204,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
transferInitiatorUserHandle,
transferInitiatorPackageName,
SYSTEM_SESSION_ID,
- routeId,
+ routeOriginalId,
transferReason);
}
@@ -234,15 +234,15 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
long requestId,
@NonNull UserHandle transferInitiatorUserHandle,
@NonNull String transferInitiatorPackageName,
- String sessionId,
- String routeId,
+ String sessionOriginalId,
+ String routeOriginalId,
@RoutingSessionInfo.TransferReason int transferReason) {
String selectedDeviceRouteId = mDeviceRouteController.getSelectedRoute().getId();
- if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
+ if (TextUtils.equals(routeOriginalId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
// Transfer to the default route (which is the selected route). We replace the id to
// be the selected route id so that the transfer reason gets updated.
- routeId = selectedDeviceRouteId;
+ routeOriginalId = selectedDeviceRouteId;
} else {
Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT);
return;
@@ -254,18 +254,18 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
mPendingTransferRequest =
new SessionCreationOrTransferRequest(
requestId,
- routeId,
+ routeOriginalId,
transferReason,
transferInitiatorUserHandle,
transferInitiatorPackageName);
}
}
- String finalRouteId = routeId; // Make a final copy to use it in the lambda.
+ String finalRouteId = routeOriginalId; // Make a final copy to use it in the lambda.
boolean isAvailableDeviceRoute =
mDeviceRouteController.getAvailableRoutes().stream()
.anyMatch(it -> it.getId().equals(finalRouteId));
- boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRouteId);
+ boolean isSelectedDeviceRoute = TextUtils.equals(routeOriginalId, selectedDeviceRouteId);
if (isSelectedDeviceRoute || isAvailableDeviceRoute) {
// The requested route is managed by the device route controller. Note that the selected
@@ -273,12 +273,12 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
// of the routing session). If the selected device route is transferred to, we need to
// make the bluetooth routes inactive so that the device route becomes the selected
// route of the routing session.
- mDeviceRouteController.transferTo(routeId);
+ mDeviceRouteController.transferTo(routeOriginalId);
mBluetoothRouteController.transferTo(null);
} else {
// The requested route is managed by the bluetooth route controller.
mDeviceRouteController.transferTo(null);
- mBluetoothRouteController.transferTo(routeId);
+ mBluetoothRouteController.transferTo(routeOriginalId);
}
if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
@@ -288,20 +288,20 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
}
@Override
- public void setRouteVolume(long requestId, String routeId, int volume) {
- if (!TextUtils.equals(routeId, mSelectedRouteId)) {
+ public void setRouteVolume(long requestId, String routeOriginalId, int volume) {
+ if (!TextUtils.equals(routeOriginalId, mSelectedRouteId)) {
return;
}
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
}
@Override
- public void setSessionVolume(long requestId, String sessionId, int volume) {
+ public void setSessionVolume(long requestId, String sessionOriginalId, int volume) {
// Do nothing since we don't support grouping volume yet.
}
@Override
- public void prepareReleaseSession(String sessionId) {
+ public void prepareReleaseSession(String sessionUniqueId) {
// Do nothing since the system session persists.
}
@@ -503,12 +503,13 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
}
long pendingRequestId = mPendingSessionCreationOrTransferRequest.mRequestId;
- if (mPendingSessionCreationOrTransferRequest.mTargetRouteId.equals(mSelectedRouteId)) {
+ if (mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId.equals(
+ mSelectedRouteId)) {
if (DEBUG) {
Slog.w(
TAG,
"Session creation success to route "
- + mPendingSessionCreationOrTransferRequest.mTargetRouteId);
+ + mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId);
}
mPendingSessionCreationOrTransferRequest = null;
mCallback.onSessionCreated(this, pendingRequestId, newSessionInfo);
@@ -520,7 +521,8 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
Slog.w(
TAG,
"Session creation failed to route "
- + mPendingSessionCreationOrTransferRequest.mTargetRouteId);
+ + mPendingSessionCreationOrTransferRequest
+ .mTargetOriginalRouteId);
}
mPendingSessionCreationOrTransferRequest = null;
mCallback.onRequestFailed(
@@ -529,7 +531,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
Slog.w(
TAG,
"Session creation waiting state to route "
- + mPendingSessionCreationOrTransferRequest.mTargetRouteId);
+ + mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId);
}
}
}
@@ -541,7 +543,8 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
// See b/307723189 for context
for (MediaRoute2Info btRoute : mBluetoothRouteController.getAllBluetoothRoutes()) {
if (TextUtils.equals(
- btRoute.getId(), mPendingSessionCreationOrTransferRequest.mTargetRouteId)) {
+ btRoute.getId(),
+ mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId)) {
return true;
}
}
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index dec97fb588a5..0d1095f5656d 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -704,7 +704,8 @@ public class PackageArchiver {
return false;
}
- if (isAppOptedOutOfArchiving(packageName, ps.getAppId())) {
+ if (isAppOptedOutOfArchiving(packageName,
+ UserHandle.getUid(userId, ps.getAppId()))) {
return false;
}
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index e00e81371853..f4b61e7b2d4a 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -72,7 +72,6 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
-import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.Xml;
@@ -88,6 +87,7 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.server.SystemService;
import com.android.server.servicewatcher.CurrentUserServiceSupplier;
import com.android.server.servicewatcher.ServiceWatcher;
+import com.android.server.utils.Slogf;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -98,7 +98,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
-
+import java.util.Objects;
/**
* Manages trust agents and trust listeners.
@@ -362,6 +362,13 @@ public class TrustManagerService extends SystemService {
}
private void scheduleTrustTimeout(boolean override, boolean isTrustableTimeout) {
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "scheduleTrustTimeout(override=%s, isTrustable=%s)",
+ override,
+ isTrustableTimeout);
+ }
int shouldOverride = override ? 1 : 0;
int trustableTimeout = isTrustableTimeout ? 1 : 0;
mHandler.obtainMessage(MSG_SCHEDULE_TRUST_TIMEOUT, shouldOverride,
@@ -370,6 +377,13 @@ public class TrustManagerService extends SystemService {
private void handleScheduleTrustTimeout(boolean shouldOverride, TimeoutType timeoutType) {
int userId = mCurrentUser;
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "handleScheduleTrustTimeout(shouldOverride=%s, timeoutType=%s)",
+ shouldOverride,
+ timeoutType);
+ }
if (timeoutType == TimeoutType.TRUSTABLE) {
// don't override the hard timeout unless biometric or knowledge factor authentication
// occurs which isn't where this is called from. Override the idle timeout what the
@@ -383,6 +397,7 @@ public class TrustManagerService extends SystemService {
/* Override both the idle and hard trustable timeouts */
private void refreshTrustableTimers(int userId) {
+ if (DEBUG) Slogf.d(TAG, "refreshTrustableTimers(userId=%s)", userId);
handleScheduleTrustableTimeouts(userId, true /* overrideIdleTimeout */,
true /* overrideHardTimeout */);
}
@@ -405,13 +420,20 @@ public class TrustManagerService extends SystemService {
}
private void handleScheduleTrustedTimeout(int userId, boolean shouldOverride) {
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "handleScheduleTrustedTimeout(userId=%s, shouldOverride=%s)",
+ userId,
+ shouldOverride);
+ }
long when = SystemClock.elapsedRealtime() + TRUST_TIMEOUT_IN_MILLIS;
TrustedTimeoutAlarmListener alarm = mTrustTimeoutAlarmListenerForUser.get(userId);
// Cancel existing trust timeouts for this user if needed.
if (alarm != null) {
if (!shouldOverride && alarm.isQueued()) {
- if (DEBUG) Slog.d(TAG, "Found existing trust timeout alarm. Skipping.");
+ if (DEBUG) Slogf.d(TAG, "Found existing trust timeout alarm. Skipping.");
return;
}
mAlarmManager.cancel(alarm);
@@ -420,7 +442,9 @@ public class TrustManagerService extends SystemService {
mTrustTimeoutAlarmListenerForUser.put(userId, alarm);
}
- if (DEBUG) Slog.d(TAG, "\tSetting up trust timeout alarm");
+ if (DEBUG) {
+ Slogf.d(TAG, "\tSetting up trust timeout alarm triggering at elapsedRealTime=%s", when);
+ }
alarm.setQueued(true /* isQueued */);
mAlarmManager.setExact(
AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm,
@@ -434,6 +458,13 @@ public class TrustManagerService extends SystemService {
}
private void setUpIdleTimeout(int userId, boolean overrideIdleTimeout) {
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "setUpIdleTimeout(userId=%s, overrideIdleTimeout=%s)",
+ userId,
+ overrideIdleTimeout);
+ }
long when = SystemClock.elapsedRealtime() + TRUSTABLE_IDLE_TIMEOUT_IN_MILLIS;
TrustableTimeoutAlarmListener alarm = mIdleTrustableTimeoutAlarmListenerForUser.get(userId);
mContext.enforceCallingOrSelfPermission(Manifest.permission.SCHEDULE_EXACT_ALARM, null);
@@ -441,7 +472,7 @@ public class TrustManagerService extends SystemService {
// Cancel existing trustable timeouts for this user if needed.
if (alarm != null) {
if (!overrideIdleTimeout && alarm.isQueued()) {
- if (DEBUG) Slog.d(TAG, "Found existing trustable timeout alarm. Skipping.");
+ if (DEBUG) Slogf.d(TAG, "Found existing trustable timeout alarm. Skipping.");
return;
}
mAlarmManager.cancel(alarm);
@@ -450,7 +481,12 @@ public class TrustManagerService extends SystemService {
mIdleTrustableTimeoutAlarmListenerForUser.put(userId, alarm);
}
- if (DEBUG) Slog.d(TAG, "\tSetting up trustable idle timeout alarm");
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "\tSetting up trustable idle timeout alarm triggering at elapsedRealTime=%s",
+ when);
+ }
alarm.setQueued(true /* isQueued */);
mAlarmManager.setExact(
AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm,
@@ -458,6 +494,13 @@ public class TrustManagerService extends SystemService {
}
private void setUpHardTimeout(int userId, boolean overrideHardTimeout) {
+ if (DEBUG) {
+ Slogf.i(
+ TAG,
+ "setUpHardTimeout(userId=%s, overrideHardTimeout=%s)",
+ userId,
+ overrideHardTimeout);
+ }
mContext.enforceCallingOrSelfPermission(Manifest.permission.SCHEDULE_EXACT_ALARM, null);
TrustableTimeoutAlarmListener alarm = mTrustableTimeoutAlarmListenerForUser.get(userId);
@@ -472,7 +515,13 @@ public class TrustManagerService extends SystemService {
} else if (overrideHardTimeout) {
mAlarmManager.cancel(alarm);
}
- if (DEBUG) Slog.d(TAG, "\tSetting up trustable hard timeout alarm");
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "\tSetting up trustable hard timeout alarm triggering at "
+ + "elapsedRealTime=%s",
+ when);
+ }
alarm.setQueued(true /* isQueued */);
mAlarmManager.setExact(
AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm,
@@ -503,6 +552,12 @@ public class TrustManagerService extends SystemService {
public int hashCode() {
return component.hashCode() * 31 + userId;
}
+
+ @Override
+ public String toString() {
+ return String.format(
+ "AgentInfo{label=%s, component=%s, userId=%s}", label, component, userId);
+ }
}
private void updateTrustAll() {
@@ -532,6 +587,15 @@ public class TrustManagerService extends SystemService {
int flags,
boolean isFromUnlock,
@Nullable AndroidFuture<GrantTrustResult> resultCallback) {
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "updateTrust(userId=%s, flags=%s, isFromUnlock=%s, resultCallbackPresent=%s)",
+ userId,
+ flags,
+ isFromUnlock,
+ Objects.isNull(resultCallback));
+ }
boolean managed = aggregateIsTrustManaged(userId);
dispatchOnTrustManagedChanged(managed, userId);
if (mStrongAuthTracker.isTrustAllowedForUser(userId)
@@ -559,27 +623,50 @@ public class TrustManagerService extends SystemService {
(flags & TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) != 0);
boolean canMoveToTrusted =
alreadyUnlocked || isFromUnlock || renewingTrust || isAutomotive();
- boolean upgradingTrustForCurrentUser = (userId == mCurrentUser);
+ boolean updatingTrustForCurrentUser = (userId == mCurrentUser);
+
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "updateTrust: alreadyUnlocked=%s, wasTrusted=%s, wasTrustable=%s, "
+ + "renewingTrust=%s, canMoveToTrusted=%s, "
+ + "updatingTrustForCurrentUser=%s",
+ alreadyUnlocked,
+ wasTrusted,
+ wasTrustable,
+ renewingTrust,
+ canMoveToTrusted,
+ updatingTrustForCurrentUser);
+ }
if (trustedByAtLeastOneAgent && wasTrusted) {
// no change
return;
- } else if (trustedByAtLeastOneAgent && canMoveToTrusted
- && upgradingTrustForCurrentUser) {
+ } else if (trustedByAtLeastOneAgent
+ && canMoveToTrusted
+ && updatingTrustForCurrentUser) {
pendingTrustState = TrustState.TRUSTED;
- } else if (trustableByAtLeastOneAgent && (wasTrusted || wasTrustable)
- && upgradingTrustForCurrentUser) {
+ } else if (trustableByAtLeastOneAgent
+ && (wasTrusted || wasTrustable)
+ && updatingTrustForCurrentUser) {
pendingTrustState = TrustState.TRUSTABLE;
} else {
pendingTrustState = TrustState.UNTRUSTED;
}
+ if (DEBUG) Slogf.d(TAG, "updateTrust: pendingTrustState=%s", pendingTrustState);
mUserTrustState.put(userId, pendingTrustState);
}
- if (DEBUG) Slog.d(TAG, "pendingTrustState: " + pendingTrustState);
boolean isNowTrusted = pendingTrustState == TrustState.TRUSTED;
boolean newlyUnlocked = !alreadyUnlocked && isNowTrusted;
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "updateTrust: isNowTrusted=%s, newlyUnlocked=%s",
+ isNowTrusted,
+ newlyUnlocked);
+ }
maybeActiveUnlockRunningChanged(userId);
dispatchOnTrustChanged(
isNowTrusted, newlyUnlocked, userId, flags, getTrustGrantedMessages(userId));
@@ -598,13 +685,13 @@ public class TrustManagerService extends SystemService {
boolean shouldSendCallback = newlyUnlocked;
if (shouldSendCallback) {
if (resultCallback != null) {
- if (DEBUG) Slog.d(TAG, "calling back with UNLOCKED_BY_GRANT");
+ if (DEBUG) Slogf.d(TAG, "calling back with UNLOCKED_BY_GRANT");
resultCallback.complete(new GrantTrustResult(STATUS_UNLOCKED_BY_GRANT));
}
}
if ((wasTrusted || wasTrustable) && pendingTrustState == TrustState.UNTRUSTED) {
- if (DEBUG) Slog.d(TAG, "Trust was revoked, destroy trustable alarms");
+ if (DEBUG) Slogf.d(TAG, "Trust was revoked, destroy trustable alarms");
cancelBothTrustableAlarms(userId);
}
}
@@ -650,7 +737,7 @@ public class TrustManagerService extends SystemService {
try {
WindowManagerGlobal.getWindowManagerService().lockNow(null);
} catch (RemoteException e) {
- Slog.e(TAG, "Error locking screen when called from trust agent");
+ Slogf.e(TAG, "Error locking screen when called from trust agent");
}
}
@@ -659,8 +746,9 @@ public class TrustManagerService extends SystemService {
}
void refreshAgentList(int userIdOrAll) {
- if (DEBUG) Slog.d(TAG, "refreshAgentList(" + userIdOrAll + ")");
+ if (DEBUG) Slogf.d(TAG, "refreshAgentList(userIdOrAll=%s)", userIdOrAll);
if (!mTrustAgentsCanRun) {
+ if (DEBUG) Slogf.d(TAG, "Did not refresh agent list because agents cannot run.");
return;
}
if (userIdOrAll != UserHandle.USER_ALL && userIdOrAll < UserHandle.USER_SYSTEM) {
@@ -686,18 +774,30 @@ public class TrustManagerService extends SystemService {
if (userInfo == null || userInfo.partial || !userInfo.isEnabled()
|| userInfo.guestToRemove) continue;
if (!userInfo.supportsSwitchToByUser()) {
- if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
- + ": switchToByUser=false");
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "refreshAgentList: skipping user %s: switchToByUser=false",
+ userInfo.id);
+ }
continue;
}
if (!mActivityManager.isUserRunning(userInfo.id)) {
- if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
- + ": user not started");
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "refreshAgentList: skipping user %s: user not started",
+ userInfo.id);
+ }
continue;
}
if (!lockPatternUtils.isSecure(userInfo.id)) {
- if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
- + ": no secure credential");
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "refreshAgentList: skipping user %s: no secure credential",
+ userInfo.id);
+ }
continue;
}
@@ -708,8 +808,12 @@ public class TrustManagerService extends SystemService {
List<ComponentName> enabledAgents = lockPatternUtils.getEnabledTrustAgents(userInfo.id);
if (enabledAgents.isEmpty()) {
- if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
- + ": no agents enabled by user");
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "refreshAgentList: skipping user %s: no agents enabled by user",
+ userInfo.id);
+ }
continue;
}
List<ResolveInfo> resolveInfos = resolveAllowedTrustAgents(pm, userInfo.id);
@@ -717,9 +821,13 @@ public class TrustManagerService extends SystemService {
ComponentName name = getComponentName(resolveInfo);
if (!enabledAgents.contains(name)) {
- if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping "
- + name.flattenToShortString() + " u"+ userInfo.id
- + ": not enabled by user");
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "refreshAgentList: skipping %s u%s: not enabled by user",
+ name.flattenToShortString(),
+ userInfo.id);
+ }
continue;
}
if (disableTrustAgents) {
@@ -727,9 +835,13 @@ public class TrustManagerService extends SystemService {
dpm.getTrustAgentConfiguration(null /* admin */, name, userInfo.id);
// Disable agent if no features are enabled.
if (config == null || config.isEmpty()) {
- if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping "
- + name.flattenToShortString() + " u"+ userInfo.id
- + ": not allowed by DPM");
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "refreshAgentList: skipping %s u%s: not allowed by DPM",
+ name.flattenToShortString(),
+ userInfo.id);
+ }
continue;
}
}
@@ -752,15 +864,26 @@ public class TrustManagerService extends SystemService {
}
if (directUnlock) {
- if (DEBUG) Slog.d(TAG, "refreshAgentList: trustagent " + name
- + "of user " + userInfo.id + "can unlock user profile.");
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "refreshAgentList: trustagent %s of user %s can unlock user "
+ + "profile.",
+ name,
+ userInfo.id);
+ }
}
if (!mUserManager.isUserUnlockingOrUnlocked(userInfo.id)
&& !directUnlock) {
- if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
- + "'s trust agent " + name + ": FBE still locked and "
- + " the agent cannot unlock user profile.");
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "refreshAgentList: skipping user %s's trust agent %s: FBE still "
+ + "locked and the agent cannot unlock user profile.",
+ userInfo.id,
+ name);
+ }
continue;
}
@@ -769,11 +892,16 @@ public class TrustManagerService extends SystemService {
if (flag != StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) {
if (flag != StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
|| !directUnlock) {
- if (DEBUG)
- Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
- + ": prevented by StrongAuthTracker = 0x"
- + Integer.toHexString(mStrongAuthTracker.getStrongAuthForUser(
- userInfo.id)));
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "refreshAgentList: skipping user %s: prevented by "
+ + "StrongAuthTracker = 0x%s",
+ userInfo.id,
+ Integer.toHexString(
+ mStrongAuthTracker.getStrongAuthForUser(
+ userInfo.id)));
+ }
continue;
}
}
@@ -804,6 +932,15 @@ public class TrustManagerService extends SystemService {
}
}
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "refreshAgentList: userInfos=%s, obsoleteAgents=%s, trustMayHaveChanged=%s",
+ userInfos,
+ obsoleteAgents,
+ trustMayHaveChanged);
+ }
+
if (trustMayHaveChanged) {
if (userIdOrAll == UserHandle.USER_ALL) {
updateTrustAll();
@@ -1044,7 +1181,7 @@ public class TrustManagerService extends SystemService {
parser = resolveInfo.serviceInfo.loadXmlMetaData(pm,
TrustAgentService.TRUST_AGENT_META_DATA);
if (parser == null) {
- Slog.w(TAG, "Can't find " + TrustAgentService.TRUST_AGENT_META_DATA + " meta-data");
+ Slogf.w(TAG, "Can't find %s meta-data", TrustAgentService.TRUST_AGENT_META_DATA);
return null;
}
Resources res = pm.getResourcesForApplication(resolveInfo.serviceInfo.applicationInfo);
@@ -1056,7 +1193,7 @@ public class TrustManagerService extends SystemService {
}
String nodeName = parser.getName();
if (!"trust-agent".equals(nodeName)) {
- Slog.w(TAG, "Meta-data does not start with trust-agent tag");
+ Slogf.w(TAG, "Meta-data does not start with trust-agent tag");
return null;
}
TypedArray sa = res
@@ -1075,7 +1212,11 @@ public class TrustManagerService extends SystemService {
if (parser != null) parser.close();
}
if (caughtException != null) {
- Slog.w(TAG, "Error parsing : " + resolveInfo.serviceInfo.packageName, caughtException);
+ Slogf.w(
+ TAG,
+ caughtException,
+ "Error parsing : %s",
+ resolveInfo.serviceInfo.packageName);
return null;
}
if (cn == null) {
@@ -1242,13 +1383,18 @@ public class TrustManagerService extends SystemService {
// Agent dispatch and aggregation
private boolean aggregateIsTrusted(int userId) {
+ if (DEBUG) Slogf.d(TAG, "aggregateIsTrusted(userId=%s)", userId);
if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) {
+ if (DEBUG) {
+ Slogf.d(TAG, "not trusted because trust not allowed for userId=%s", userId);
+ }
return false;
}
for (int i = 0; i < mActiveAgents.size(); i++) {
AgentInfo info = mActiveAgents.valueAt(i);
if (info.userId == userId) {
if (info.agent.isTrusted()) {
+ if (DEBUG) Slogf.d(TAG, "trusted by %s", info);
return true;
}
}
@@ -1257,13 +1403,18 @@ public class TrustManagerService extends SystemService {
}
private boolean aggregateIsTrustable(int userId) {
+ if (DEBUG) Slogf.d(TAG, "aggregateIsTrustable(userId=%s)", userId);
if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) {
+ if (DEBUG) {
+ Slogf.d(TAG, "not trustable because trust not allowed for userId=%s", userId);
+ }
return false;
}
for (int i = 0; i < mActiveAgents.size(); i++) {
AgentInfo info = mActiveAgents.valueAt(i);
if (info.userId == userId) {
if (info.agent.isTrustable()) {
+ if (DEBUG) Slogf.d(TAG, "trustable by %s", info);
return true;
}
}
@@ -1328,20 +1479,31 @@ public class TrustManagerService extends SystemService {
private boolean aggregateIsTrustManaged(int userId) {
if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) {
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "trust not managed due to trust not being allowed for userId=%s",
+ userId);
+ }
return false;
}
for (int i = 0; i < mActiveAgents.size(); i++) {
AgentInfo info = mActiveAgents.valueAt(i);
if (info.userId == userId) {
if (info.agent.isManagingTrust()) {
+ if (DEBUG) Slogf.d(TAG, "trust managed for userId=%s", userId);
return true;
}
}
}
+ if (DEBUG) Slogf.d(TAG, "trust not managed for userId=%s", userId);
return false;
}
private void dispatchUnlockAttempt(boolean successful, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "dispatchUnlockAttempt(successful=%s, userId=%s)", successful, userId);
+ }
if (successful) {
mStrongAuthTracker.allowTrustFromUnlock(userId);
// Allow the presence of trust on a successful unlock attempt to extend unlock
@@ -1359,8 +1521,11 @@ public class TrustManagerService extends SystemService {
private void dispatchUserRequestedUnlock(int userId, boolean dismissKeyguard) {
if (DEBUG) {
- Slog.d(TAG, "dispatchUserRequestedUnlock(user=" + userId + ", dismissKeyguard="
- + dismissKeyguard + ")");
+ Slogf.d(
+ TAG,
+ "dispatchUserRequestedUnlock(user=%s, dismissKeyguard=%s)",
+ userId,
+ dismissKeyguard);
}
for (int i = 0; i < mActiveAgents.size(); i++) {
AgentInfo info = mActiveAgents.valueAt(i);
@@ -1372,7 +1537,7 @@ public class TrustManagerService extends SystemService {
private void dispatchUserMayRequestUnlock(int userId) {
if (DEBUG) {
- Slog.d(TAG, "dispatchUserMayRequestUnlock(user=" + userId + ")");
+ Slogf.d(TAG, "dispatchUserMayRequestUnlock(user=%s)", userId);
}
for (int i = 0; i < mActiveAgents.size(); i++) {
AgentInfo info = mActiveAgents.valueAt(i);
@@ -1405,9 +1570,9 @@ public class TrustManagerService extends SystemService {
try {
listener.onIsActiveUnlockRunningChanged(isRunning, userId);
} catch (DeadObjectException e) {
- Slog.d(TAG, "TrustListener dead while trying to notify Active Unlock running state");
+ Slogf.d(TAG, "TrustListener dead while trying to notify Active Unlock running state");
} catch (RemoteException e) {
- Slog.e(TAG, "Exception while notifying TrustListener.", e);
+ Slogf.e(TAG, "Exception while notifying TrustListener.", e);
}
}
@@ -1445,11 +1610,11 @@ public class TrustManagerService extends SystemService {
mTrustListeners.get(i).onTrustChanged(
enabled, newlyUnlocked, userId, flags, trustGrantedMessages);
} catch (DeadObjectException e) {
- Slog.d(TAG, "Removing dead TrustListener.");
+ Slogf.d(TAG, "Removing dead TrustListener.");
mTrustListeners.remove(i);
i--;
} catch (RemoteException e) {
- Slog.e(TAG, "Exception while notifying TrustListener.", e);
+ Slogf.e(TAG, "Exception while notifying TrustListener.", e);
}
}
}
@@ -1462,11 +1627,11 @@ public class TrustManagerService extends SystemService {
try {
mTrustListeners.get(i).onEnabledTrustAgentsChanged(userId);
} catch (DeadObjectException e) {
- Slog.d(TAG, "Removing dead TrustListener.");
+ Slogf.d(TAG, "Removing dead TrustListener.");
mTrustListeners.remove(i);
i--;
} catch (RemoteException e) {
- Slog.e(TAG, "Exception while notifying TrustListener.", e);
+ Slogf.e(TAG, "Exception while notifying TrustListener.", e);
}
}
}
@@ -1479,11 +1644,11 @@ public class TrustManagerService extends SystemService {
try {
mTrustListeners.get(i).onTrustManagedChanged(managed, userId);
} catch (DeadObjectException e) {
- Slog.d(TAG, "Removing dead TrustListener.");
+ Slogf.d(TAG, "Removing dead TrustListener.");
mTrustListeners.remove(i);
i--;
} catch (RemoteException e) {
- Slog.e(TAG, "Exception while notifying TrustListener.", e);
+ Slogf.e(TAG, "Exception while notifying TrustListener.", e);
}
}
}
@@ -1496,11 +1661,11 @@ public class TrustManagerService extends SystemService {
try {
mTrustListeners.get(i).onTrustError(message);
} catch (DeadObjectException e) {
- Slog.d(TAG, "Removing dead TrustListener.");
+ Slogf.d(TAG, "Removing dead TrustListener.");
mTrustListeners.remove(i);
i--;
} catch (RemoteException e) {
- Slog.e(TAG, "Exception while notifying TrustListener.", e);
+ Slogf.e(TAG, "Exception while notifying TrustListener.", e);
}
}
}
@@ -1535,7 +1700,7 @@ public class TrustManagerService extends SystemService {
&& mFingerprintManager.hasEnrolledTemplates(userId)
&& isWeakOrConvenienceSensor(
mFingerprintManager.getSensorProperties().get(0))) {
- Slog.i(TAG, "User is unlockable by non-strong fingerprint auth");
+ Slogf.i(TAG, "User is unlockable by non-strong fingerprint auth");
return true;
}
@@ -1543,7 +1708,7 @@ public class TrustManagerService extends SystemService {
&& (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_FACE) == 0
&& mFaceManager.hasEnrolledTemplates(userId)
&& isWeakOrConvenienceSensor(mFaceManager.getSensorProperties().get(0))) {
- Slog.i(TAG, "User is unlockable by non-strong face auth");
+ Slogf.i(TAG, "User is unlockable by non-strong face auth");
return true;
}
}
@@ -1551,7 +1716,7 @@ public class TrustManagerService extends SystemService {
// Check whether it's possible for the device to be actively unlocked by a trust agent.
if (getUserTrustStateInner(userId) == TrustState.TRUSTABLE
|| (isAutomotive() && isTrustUsuallyManagedInternal(userId))) {
- Slog.i(TAG, "User is unlockable by trust agent");
+ Slogf.i(TAG, "User is unlockable by trust agent");
return true;
}
@@ -1595,6 +1760,13 @@ public class TrustManagerService extends SystemService {
private final IBinder mService = new ITrustManager.Stub() {
@Override
public void reportUnlockAttempt(boolean authenticated, int userId) throws RemoteException {
+ if (DEBUG) {
+ Slogf.d(
+ TAG,
+ "reportUnlockAttempt(authenticated=%s, userId=%s)",
+ authenticated,
+ userId);
+ }
enforceReportPermission();
mHandler.obtainMessage(MSG_DISPATCH_UNLOCK_ATTEMPT, authenticated ? 1 : 0, userId)
.sendToTarget();
@@ -1611,7 +1783,8 @@ public class TrustManagerService extends SystemService {
@Override
public void reportUserMayRequestUnlock(int userId) throws RemoteException {
enforceReportPermission();
- mHandler.obtainMessage(MSG_USER_MAY_REQUEST_UNLOCK, userId, /*arg2=*/ 0).sendToTarget();
+ mHandler.obtainMessage(MSG_USER_MAY_REQUEST_UNLOCK, userId, /* arg2= */ 0)
+ .sendToTarget();
}
@Override
@@ -1932,6 +2105,7 @@ public class TrustManagerService extends SystemService {
return new Handler(looper) {
@Override
public void handleMessage(Message msg) {
+ if (DEBUG) Slogf.d(TAG, "handler: %s", msg.what);
switch (msg.what) {
case MSG_REGISTER_LISTENER:
addListener((ITrustListener) msg.obj);
@@ -2002,8 +2176,24 @@ public class TrustManagerService extends SystemService {
handleScheduleTrustTimeout(shouldOverride, timeoutType);
break;
case MSG_REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH:
+ if (DEBUG) {
+ Slogf.d(TAG, "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH userId=%s", msg.arg1);
+ }
TrustableTimeoutAlarmListener trustableAlarm =
mTrustableTimeoutAlarmListenerForUser.get(msg.arg1);
+ if (DEBUG) {
+ if (trustableAlarm != null) {
+ Slogf.d(
+ TAG,
+ "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH trustable alarm "
+ + "isQueued=%s",
+ trustableAlarm.mIsQueued);
+ } else {
+ Slogf.d(
+ TAG,
+ "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH no trustable alarm");
+ }
+ }
if (trustableAlarm != null && trustableAlarm.isQueued()) {
refreshTrustableTimers(msg.arg1);
}
@@ -2194,7 +2384,7 @@ public class TrustManagerService extends SystemService {
handleAlarm();
// Only fire if trust can unlock.
if (mStrongAuthTracker.isTrustAllowedForUser(mUserId)) {
- if (DEBUG) Slog.d(TAG, "Revoking all trust because of trust timeout");
+ if (DEBUG) Slogf.d(TAG, "Revoking all trust because of trust timeout");
mLockPatternUtils.requireStrongAuth(
mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, mUserId);
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 86115990f746..2d2a88a866ba 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3964,7 +3964,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
}
if (isCurrentVisible) {
- if (isNextNotYetVisible || delayRemoval || isInTransition()) {
+ if (isNextNotYetVisible || delayRemoval || (next != null && isInTransition())) {
// Add this activity to the list of stopping activities. It will be processed and
// destroyed when the next activity reports idle.
addToStopping(false /* scheduleIdle */, false /* idleDelayed */,
@@ -7644,6 +7644,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
// This could only happen when the window is removed from hierarchy. So do not keep its
// reference anymore.
mStartingWindow = null;
+ mStartingData = null;
+ mStartingSurface = null;
}
if (mChildren.size() == 0 && mVisibleSetFromTransferredStartingWindow) {
// We set the visible state to true for the token from a transferred starting
@@ -9259,6 +9261,19 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
@NonNull CompatDisplayInsets compatDisplayInsets) {
final Configuration resolvedConfig = getResolvedOverrideConfiguration();
final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
+ final Insets insets;
+ if (mResolveConfigHint.mUseOverrideInsetsForConfig) {
+ // TODO(b/343197837): Add test to verify SCM behaviour with new bound configuration
+ // Insets are decoupled from configuration by default from V+, use legacy
+ // compatibility behaviour for apps targeting SDK earlier than 35
+ // (see applySizeOverrideIfNeeded).
+ insets = Insets.of(mDisplayContent.getDisplayPolicy()
+ .getDecorInsetsInfo(mDisplayContent.mDisplayFrames.mRotation,
+ mDisplayContent.mDisplayFrames.mWidth,
+ mDisplayContent.mDisplayFrames.mHeight).mOverrideNonDecorInsets);
+ } else {
+ insets = Insets.NONE;
+ }
// When an activity needs to be letterboxed because of fixed orientation, use fixed
// orientation bounds (stored in resolved bounds) instead of parent bounds since the
@@ -9269,9 +9284,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
final Rect containerBounds = useResolvedBounds
? new Rect(resolvedBounds)
: newParentConfiguration.windowConfiguration.getBounds();
+ final Rect parentAppBounds =
+ newParentConfiguration.windowConfiguration.getAppBounds();
+ parentAppBounds.inset(insets);
final Rect containerAppBounds = useResolvedBounds
? new Rect(resolvedConfig.windowConfiguration.getAppBounds())
- : newParentConfiguration.windowConfiguration.getAppBounds();
+ : parentAppBounds;
final int requestedOrientation = getRequestedConfigurationOrientation();
final boolean orientationRequested = requestedOrientation != ORIENTATION_UNDEFINED;
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index ac2c886d1b66..207707efb51d 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -28,7 +28,6 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
import static android.os.Process.INVALID_PID;
import static android.os.Process.INVALID_UID;
-import static android.os.Process.ROOT_UID;
import static android.os.Process.SYSTEM_UID;
import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER;
@@ -386,10 +385,6 @@ public class BackgroundActivityStartController {
return BackgroundStartPrivileges.NONE;
case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED:
// no explicit choice by the app - let us decide what to do
- if (callingUid == ROOT_UID || callingUid == SYSTEM_UID) {
- // root and system must always opt in explicitly
- return BackgroundStartPrivileges.NONE;
- }
if (callingPackage != null) {
// determine based on the calling/creating package
boolean changeEnabled = CompatChanges.isChangeEnabled(
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 94a22394cf41..a00b6fc47de7 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1927,7 +1927,7 @@ public class WindowManagerService extends IWindowManager.Stub
displayContent.getInsetsStateController().updateAboveInsetsState(
false /* notifyInsetsChanged */);
- outInsetsState.set(win.getCompatInsetsState(), true /* copySources */);
+ win.fillInsetsState(outInsetsState, true /* copySources */);
getInsetsSourceControls(win, outActiveControls);
if (win.mLayoutAttached) {
@@ -2680,7 +2680,7 @@ public class WindowManagerService extends IWindowManager.Stub
}
if (outInsetsState != null) {
- outInsetsState.set(win.getCompatInsetsState(), true /* copySources */);
+ win.fillInsetsState(outInsetsState, true /* copySources */);
}
ProtoLog.v(WM_DEBUG_FOCUS, "Relayout of %s: focusMayChange=%b",
@@ -2743,12 +2743,10 @@ public class WindowManagerService extends IWindowManager.Stub
}
private void getInsetsSourceControls(WindowState win, InsetsSourceControl.Array outArray) {
- final InsetsSourceControl[] controls =
- win.getDisplayContent().getInsetsStateController().getControlsForDispatch(win);
// We will leave the critical section before returning the leash to the client,
// so we need to copy the leash to prevent others release the one that we are
// about to return.
- outArray.set(controls, true /* copyControls */);
+ win.fillInsetsSourceControls(outArray, true /* copyControls */);
// This source control is an extra copy if the client is not local. By setting
// PARCELABLE_WRITE_RETURN_VALUE, the leash will be released at the end of
// SurfaceControl.writeToParcel.
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index d7c49ac81a6c..d1efc27d9661 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -434,7 +434,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
/** @see #isLastConfigReportedToClient() */
private boolean mLastConfigReportedToClient;
- // TODO(b/339380439): Ensure to use the same object for IWindowSession#relayout
+ private final ClientWindowFrames mLastReportedFrames = new ClientWindowFrames();
+
+ private final InsetsState mLastReportedInsetsState = new InsetsState();
+
private final InsetsSourceControl.Array mLastReportedActiveControls =
new InsetsSourceControl.Array();
@@ -495,8 +498,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
private final WindowFrames mWindowFrames = new WindowFrames();
- private final ClientWindowFrames mClientWindowFrames = new ClientWindowFrames();
-
/**
* List of rects where system gestures should be ignored.
*
@@ -3650,8 +3651,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
outFrames.attachedFrame.scale(mInvGlobalScale);
}
}
-
outFrames.compatScale = getCompatScaleForClient();
+ if (mLastReportedFrames != outFrames) {
+ mLastReportedFrames.setTo(outFrames);
+ }
// Note: in the cases where the window is tied to an activity, we should not send a
// configuration update when the window has requested to be hidden. Doing so can lead to
@@ -3678,6 +3681,25 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mLastConfigReportedToClient = true;
}
+ void fillInsetsState(@NonNull InsetsState outInsetsState, boolean copySources) {
+ outInsetsState.set(getCompatInsetsState(), copySources);
+ if (outInsetsState != mLastReportedInsetsState) {
+ // No need to copy for the recorded.
+ mLastReportedInsetsState.set(outInsetsState, false /* copySources */);
+ }
+ }
+
+ void fillInsetsSourceControls(@NonNull InsetsSourceControl.Array outArray,
+ boolean copyControls) {
+ final InsetsSourceControl[] controls =
+ getDisplayContent().getInsetsStateController().getControlsForDispatch(this);
+ outArray.set(controls, copyControls);
+ if (outArray != mLastReportedActiveControls) {
+ // No need to copy for the recorded.
+ mLastReportedActiveControls.setTo(outArray, false /* copyControls */);
+ }
+ }
+
void reportResized() {
// If the activity is scheduled to relaunch, skip sending the resized to ViewRootImpl now
// since it will be destroyed anyway. This also prevents the client from receiving
@@ -3712,9 +3734,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
final int prevRotation = mLastReportedConfiguration
.getMergedConfiguration().windowConfiguration.getRotation();
- fillClientWindowFramesAndConfiguration(mClientWindowFrames, mLastReportedConfiguration,
+ fillClientWindowFramesAndConfiguration(mLastReportedFrames, mLastReportedConfiguration,
mLastReportedActivityWindowInfo, true /* useLatestConfig */,
false /* relayoutVisible */);
+ fillInsetsState(mLastReportedInsetsState, false /* copySources */);
final boolean syncRedraw = shouldSendRedrawForSync();
final boolean syncWithBuffers = syncRedraw && shouldSyncWithBuffers();
final boolean reportDraw = syncRedraw || drawPending;
@@ -3734,8 +3757,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
if (Flags.bundleClientTransactionFlag()) {
getProcess().scheduleClientTransactionItem(
- WindowStateResizeItem.obtain(mClient, mClientWindowFrames, reportDraw,
- mLastReportedConfiguration, getCompatInsetsState(), forceRelayout,
+ WindowStateResizeItem.obtain(mClient, mLastReportedFrames, reportDraw,
+ mLastReportedConfiguration, mLastReportedInsetsState, forceRelayout,
alwaysConsumeSystemBars, displayId,
syncWithBuffers ? mSyncSeqId : -1, isDragResizing,
mLastReportedActivityWindowInfo));
@@ -3743,8 +3766,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
} else {
// TODO(b/301870955): cleanup after launch
try {
- mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration,
- getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId,
+ mClient.resized(mLastReportedFrames, reportDraw, mLastReportedConfiguration,
+ mLastReportedInsetsState, forceRelayout, alwaysConsumeSystemBars, displayId,
syncWithBuffers ? mSyncSeqId : -1, isDragResizing,
mLastReportedActivityWindowInfo);
onResizePostDispatched(drawPending, prevRotation, displayId);
@@ -3817,17 +3840,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
if (mRemoved) {
return;
}
- final InsetsStateController stateController =
- getDisplayContent().getInsetsStateController();
- final InsetsState insetsState = getCompatInsetsState();
- mLastReportedActiveControls.set(stateController.getControlsForDispatch(this),
- false /* copyControls */);
+ fillInsetsState(mLastReportedInsetsState, false /* copySources */);
+ fillInsetsSourceControls(mLastReportedActiveControls, false /* copyControls */);
if (Flags.insetsControlChangedItem()) {
getProcess().scheduleClientTransactionItem(WindowStateInsetsControlChangeItem.obtain(
- mClient, insetsState, mLastReportedActiveControls));
+ mClient, mLastReportedInsetsState, mLastReportedActiveControls));
} else {
try {
- mClient.insetsControlChanged(insetsState, mLastReportedActiveControls);
+ mClient.insetsControlChanged(mLastReportedInsetsState, mLastReportedActiveControls);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to deliver inset control state change to w=" + this, e);
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
index 52b33db556e6..d4f0d5aa7ef6 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
@@ -593,6 +593,12 @@ public class ProxyManagerTest {
}
@Override
+ public void onMagnificationSystemUIConnectionChanged(boolean connected)
+ throws RemoteException {
+
+ }
+
+ @Override
public void onMagnificationChanged(int displayId, Region region, MagnificationConfig config)
throws RemoteException {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
index 87fe6cf8f283..0de5807067e6 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
@@ -59,6 +59,7 @@ import android.view.accessibility.MagnificationAnimationCallback;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.FlakyTest;
+import com.android.compatibility.common.util.TestUtils;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.server.LocalServices;
import com.android.server.accessibility.AccessibilityTraceManager;
@@ -76,6 +77,7 @@ import org.mockito.invocation.InvocationOnMock;
*/
public class MagnificationConnectionManagerTest {
+ private static final int WAIT_CONNECTION_TIMEOUT_SECOND = 1;
private static final int CURRENT_USER_ID = UserHandle.USER_SYSTEM;
private static final int SERVICE_ID = 1;
@@ -115,17 +117,19 @@ public class MagnificationConnectionManagerTest {
private void stubSetConnection(boolean needDelay) {
doAnswer((InvocationOnMock invocation) -> {
final boolean connect = (Boolean) invocation.getArguments()[0];
- // Simulates setConnection() called by another process.
+ // Use post to simulate setConnection() called by another process.
+ final Context context = ApplicationProvider.getApplicationContext();
if (needDelay) {
- final Context context = ApplicationProvider.getApplicationContext();
context.getMainThreadHandler().postDelayed(
() -> {
mMagnificationConnectionManager.setConnection(
connect ? mMockConnection.getConnection() : null);
}, 10);
} else {
- mMagnificationConnectionManager.setConnection(
- connect ? mMockConnection.getConnection() : null);
+ context.getMainThreadHandler().post(() -> {
+ mMagnificationConnectionManager.setConnection(
+ connect ? mMockConnection.getConnection() : null);
+ });
}
return true;
}).when(mMockStatusBarManagerInternal).requestMagnificationConnection(anyBoolean());
@@ -629,9 +633,10 @@ public class MagnificationConnectionManagerTest {
}
@Test
- public void isConnected_requestConnection_expectedValue() throws RemoteException {
+ public void isConnected_requestConnection_expectedValue() throws Exception {
mMagnificationConnectionManager.requestConnection(true);
- assertTrue(mMagnificationConnectionManager.isConnected());
+ TestUtils.waitUntil("connection is not ready", WAIT_CONNECTION_TIMEOUT_SECOND,
+ () -> mMagnificationConnectionManager.isConnected());
mMagnificationConnectionManager.requestConnection(false);
assertFalse(mMagnificationConnectionManager.isConnected());
diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
index e756082bc912..758c84a26dcd 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
@@ -28,6 +28,7 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.AppOpsManager;
import android.content.Context;
import android.content.res.Resources;
import android.media.AudioDeviceAttributes;
@@ -37,6 +38,7 @@ import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.IAudioDeviceVolumeDispatcher;
import android.media.VolumeInfo;
+import android.os.PermissionEnforcer;
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.platform.test.annotations.Presubmit;
@@ -98,7 +100,8 @@ public class AbsoluteVolumeBehaviorTest {
mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy,
- mTestLooper.getLooper()) {
+ mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
+ mock(AudioServerPermissionProvider.class)) {
@Override
public int getDeviceForStream(int stream) {
return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
index 3623012b348f..2cb02bdd2806 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -23,12 +23,14 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import android.app.AppOpsManager;
import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.VolumeInfo;
+import android.os.PermissionEnforcer;
import android.os.test.TestLooper;
import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
@@ -75,7 +77,8 @@ public class AudioDeviceVolumeManagerTest {
mAudioVolumeGroupHelper = new AudioVolumeGroupHelperBase();
mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock,
- mTestLooper.getLooper()) {
+ mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
+ mock(AudioServerPermissionProvider.class)) {
@Override
public int getDeviceForStream(int stream) {
return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java
new file mode 100644
index 000000000000..8d772ad5c124
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.audio;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.media.permission.INativePermissionController;
+import com.android.media.permission.UidPackageState;
+import com.android.server.pm.pkg.PackageState;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public final class AudioServerPermissionProviderTest {
+
+ // Class under test
+ private AudioServerPermissionProvider mPermissionProvider;
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock public INativePermissionController mMockPc;
+
+ @Mock public PackageState mMockPackageStateOne_10000_one;
+ @Mock public PackageState mMockPackageStateTwo_10001_two;
+ @Mock public PackageState mMockPackageStateThree_10000_one;
+ @Mock public PackageState mMockPackageStateFour_10000_three;
+ @Mock public PackageState mMockPackageStateFive_10001_four;
+ @Mock public PackageState mMockPackageStateSix_10000_two;
+
+ public List<UidPackageState> mInitPackageListExpected;
+
+ // Argument matcher which matches that the state is equal even if the package names are out of
+ // order (since they are logically a set).
+ public static final class UidPackageStateMatcher implements ArgumentMatcher<UidPackageState> {
+ private final int mUid;
+ private final List<String> mSortedPackages;
+
+ public UidPackageStateMatcher(int uid, List<String> packageNames) {
+ mUid = uid;
+ if (packageNames != null) {
+ mSortedPackages = new ArrayList(packageNames);
+ Collections.sort(mSortedPackages);
+ } else {
+ mSortedPackages = null;
+ }
+ }
+
+ public UidPackageStateMatcher(UidPackageState toMatch) {
+ this(toMatch.uid, toMatch.packageNames);
+ }
+
+ @Override
+ public boolean matches(UidPackageState state) {
+ if (state == null) return false;
+ if (state.uid != mUid) return false;
+ if ((state.packageNames == null) != (mSortedPackages == null)) return false;
+ var copy = new ArrayList(state.packageNames);
+ Collections.sort(copy);
+ return mSortedPackages.equals(copy);
+ }
+
+ @Override
+ public String toString() {
+ return "Matcher for UidState with uid: " + mUid + ": " + mSortedPackages;
+ }
+ }
+
+ public static final class PackageStateListMatcher
+ implements ArgumentMatcher<List<UidPackageState>> {
+
+ private final List<UidPackageState> mToMatch;
+
+ public PackageStateListMatcher(List<UidPackageState> toMatch) {
+ mToMatch = Objects.requireNonNull(toMatch);
+ }
+
+ @Override
+ public boolean matches(List<UidPackageState> other) {
+ if (other == null) return false;
+ if (other.size() != mToMatch.size()) return false;
+ for (int i = 0; i < mToMatch.size(); i++) {
+ var matcher = new UidPackageStateMatcher(mToMatch.get(i));
+ if (!matcher.matches(other.get(i))) return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "Matcher for List<UidState> with uid: " + mToMatch;
+ }
+ }
+
+ @Before
+ public void setup() {
+ when(mMockPackageStateOne_10000_one.getAppId()).thenReturn(10000);
+ when(mMockPackageStateOne_10000_one.getPackageName()).thenReturn("com.package.one");
+
+ when(mMockPackageStateTwo_10001_two.getAppId()).thenReturn(10001);
+ when(mMockPackageStateTwo_10001_two.getPackageName()).thenReturn("com.package.two");
+
+ // Same state as the first is intentional, emulating multi-user
+ when(mMockPackageStateThree_10000_one.getAppId()).thenReturn(10000);
+ when(mMockPackageStateThree_10000_one.getPackageName()).thenReturn("com.package.one");
+
+ when(mMockPackageStateFour_10000_three.getAppId()).thenReturn(10000);
+ when(mMockPackageStateFour_10000_three.getPackageName()).thenReturn("com.package.three");
+
+ when(mMockPackageStateFive_10001_four.getAppId()).thenReturn(10001);
+ when(mMockPackageStateFive_10001_four.getPackageName()).thenReturn("com.package.four");
+
+ when(mMockPackageStateSix_10000_two.getAppId()).thenReturn(10000);
+ when(mMockPackageStateSix_10000_two.getPackageName()).thenReturn("com.package.two");
+ }
+
+ @Test
+ public void testInitialPackagePopulation() throws Exception {
+ var initPackageListData =
+ List.of(
+ mMockPackageStateOne_10000_one,
+ mMockPackageStateTwo_10001_two,
+ mMockPackageStateThree_10000_one,
+ mMockPackageStateFour_10000_three,
+ mMockPackageStateFive_10001_four,
+ mMockPackageStateSix_10000_two);
+ var expectedPackageList =
+ List.of(
+ createUidPackageState(
+ 10000,
+ List.of("com.package.one", "com.package.two", "com.package.three")),
+ createUidPackageState(
+ 10001, List.of("com.package.two", "com.package.four")));
+
+ mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+ mPermissionProvider.onServiceStart(mMockPc);
+ verify(mMockPc)
+ .populatePackagesForUids(argThat(new PackageStateListMatcher(expectedPackageList)));
+ }
+
+ @Test
+ public void testOnModifyPackageState_whenNewUid() throws Exception {
+ // 10000: one | 10001: two
+ var initPackageListData =
+ List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two);
+ mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+ mPermissionProvider.onServiceStart(mMockPc);
+
+ // new uid, including user component
+ mPermissionProvider.onModifyPackageState(1_10002, "com.package.new", false /* isRemove */);
+
+ verify(mMockPc)
+ .updatePackagesForUid(
+ argThat(new UidPackageStateMatcher(10002, List.of("com.package.new"))));
+ verify(mMockPc).updatePackagesForUid(any()); // exactly once
+ }
+
+ @Test
+ public void testOnModifyPackageState_whenRemoveUid() throws Exception {
+ // 10000: one | 10001: two
+ var initPackageListData =
+ List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two);
+ mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+ mPermissionProvider.onServiceStart(mMockPc);
+
+ // Includes user-id
+ mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */);
+
+ verify(mMockPc).updatePackagesForUid(argThat(new UidPackageStateMatcher(10000, List.of())));
+ verify(mMockPc).updatePackagesForUid(any()); // exactly once
+ }
+
+ @Test
+ public void testOnModifyPackageState_whenUpdatedUidAddition() throws Exception {
+ // 10000: one | 10001: two
+ var initPackageListData =
+ List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two);
+ mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+ mPermissionProvider.onServiceStart(mMockPc);
+
+ // Includes user-id
+ mPermissionProvider.onModifyPackageState(1_10000, "com.package.new", false /* isRemove */);
+
+ verify(mMockPc)
+ .updatePackagesForUid(
+ argThat(
+ new UidPackageStateMatcher(
+ 10000, List.of("com.package.one", "com.package.new"))));
+ verify(mMockPc).updatePackagesForUid(any()); // exactly once
+ }
+
+ @Test
+ public void testOnModifyPackageState_whenUpdateUidRemoval() throws Exception {
+ // 10000: one, two | 10001: two
+ var initPackageListData =
+ List.of(
+ mMockPackageStateOne_10000_one,
+ mMockPackageStateTwo_10001_two,
+ mMockPackageStateSix_10000_two);
+ mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+ mPermissionProvider.onServiceStart(mMockPc);
+
+ // Includes user-id
+ mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */);
+
+ verify(mMockPc)
+ .updatePackagesForUid(
+ argThat(
+ new UidPackageStateMatcher(
+ createUidPackageState(10000, List.of("com.package.two")))));
+ verify(mMockPc).updatePackagesForUid(any()); // exactly once
+ }
+
+ @Test
+ public void testOnServiceStart() throws Exception {
+ // 10000: one, two | 10001: two
+ var initPackageListData =
+ List.of(
+ mMockPackageStateOne_10000_one,
+ mMockPackageStateTwo_10001_two,
+ mMockPackageStateSix_10000_two);
+ mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+ mPermissionProvider.onServiceStart(mMockPc);
+ mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */);
+ verify(mMockPc)
+ .updatePackagesForUid(
+ argThat(new UidPackageStateMatcher(10000, List.of("com.package.two"))));
+
+ verify(mMockPc).updatePackagesForUid(any()); // exactly once
+ mPermissionProvider.onModifyPackageState(
+ 1_10000, "com.package.three", false /* isRemove */);
+ verify(mMockPc)
+ .updatePackagesForUid(
+ argThat(
+ new UidPackageStateMatcher(
+ 10000, List.of("com.package.two", "com.package.three"))));
+ verify(mMockPc, times(2)).updatePackagesForUid(any()); // exactly twice
+ // state is now 10000: two, three | 10001: two
+
+ // simulate restart of the service
+ mPermissionProvider.onServiceStart(null); // should handle null
+ var newMockPc = mock(INativePermissionController.class);
+ mPermissionProvider.onServiceStart(newMockPc);
+
+ var expectedPackageList =
+ List.of(
+ createUidPackageState(
+ 10000, List.of("com.package.two", "com.package.three")),
+ createUidPackageState(10001, List.of("com.package.two")));
+
+ verify(newMockPc)
+ .populatePackagesForUids(argThat(new PackageStateListMatcher(expectedPackageList)));
+
+ verify(newMockPc, never()).updatePackagesForUid(any());
+ // updates should still work after restart
+ mPermissionProvider.onModifyPackageState(10001, "com.package.four", false /* isRemove */);
+ verify(newMockPc)
+ .updatePackagesForUid(
+ argThat(
+ new UidPackageStateMatcher(
+ 10001, List.of("com.package.two", "com.package.four"))));
+ // exactly once
+ verify(newMockPc).updatePackagesForUid(any());
+ }
+
+ private static UidPackageState createUidPackageState(int uid, List<String> packages) {
+ var res = new UidPackageState();
+ res.uid = uid;
+ res.packageNames = packages;
+ return res;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
index 634877eb2539..037c3c00443c 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
@@ -66,6 +66,7 @@ public class AudioServiceTest {
@Mock private AppOpsManager mMockAppOpsManager;
@Mock private AudioPolicyFacade mMockAudioPolicy;
@Mock private PermissionEnforcer mMockPermissionEnforcer;
+ @Mock private AudioServerPermissionProvider mMockPermissionProvider;
// the class being unit-tested here
private AudioService mAudioService;
@@ -86,7 +87,7 @@ public class AudioServiceTest {
.thenReturn(AppOpsManager.MODE_ALLOWED);
mAudioService = new AudioService(mContext, mSpyAudioSystem, mSpySystemServer,
mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, null,
- mMockAppOpsManager, mMockPermissionEnforcer);
+ mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider);
}
/**
diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
index 8dfcc1843fed..27b552fa7cdd 100644
--- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
@@ -22,11 +22,13 @@ import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.Mockito.mock;
import android.annotation.NonNull;
+import android.app.AppOpsManager;
import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.IDeviceVolumeBehaviorDispatcher;
+import android.os.PermissionEnforcer;
import android.os.test.TestLooper;
import android.platform.test.annotations.Presubmit;
@@ -75,7 +77,8 @@ public class DeviceVolumeBehaviorTest {
mAudioVolumeGroupHelper = new AudioVolumeGroupHelperBase();
mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer,
mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock,
- mTestLooper.getLooper());
+ mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
+ mock(AudioServerPermissionProvider.class));
mTestLooper.dispatchAll();
}
diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
index 23728db34c34..8e34ee1b6a42 100644
--- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
@@ -40,6 +40,7 @@ import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
+import static com.android.media.audio.Flags.FLAG_ABS_VOLUME_INDEX_FIX;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -132,9 +133,12 @@ public class VolumeHelperTest {
@Mock
private PermissionEnforcer mMockPermissionEnforcer;
@Mock
+ private AudioServerPermissionProvider mMockPermissionProvider;
+ @Mock
private AudioVolumeGroupHelperBase mAudioVolumeGroupHelper;
- private final AudioPolicyFacade mFakeAudioPolicy = lookbackAudio -> false;
+ @Mock
+ private AudioPolicyFacade mMockAudioPolicy;
private AudioVolumeGroup mAudioMusicVolumeGroup;
@@ -153,9 +157,10 @@ public class VolumeHelperTest {
SystemServerAdapter systemServer, SettingsAdapter settings,
AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy,
@Nullable Looper looper, AppOpsManager appOps,
- @NonNull PermissionEnforcer enforcer) {
+ @NonNull PermissionEnforcer enforcer,
+ AudioServerPermissionProvider permissionProvider) {
super(context, audioSystem, systemServer, settings, audioVolumeGroupHelper,
- audioPolicy, looper, appOps, enforcer);
+ audioPolicy, looper, appOps, enforcer, permissionProvider);
}
public void setDeviceForStream(int stream, int device) {
@@ -209,8 +214,9 @@ public class VolumeHelperTest {
mAm = mContext.getSystemService(AudioManager.class);
mAudioService = new MyAudioService(mContext, mSpyAudioSystem, mSpySystemServer,
- mSettingsAdapter, mAudioVolumeGroupHelper, mFakeAudioPolicy,
- mTestLooper.getLooper(), mMockAppOpsManager, mMockPermissionEnforcer);
+ mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy,
+ mTestLooper.getLooper(), mMockAppOpsManager, mMockPermissionEnforcer,
+ mMockPermissionProvider);
mTestLooper.dispatchAll();
prepareAudioServiceState();
@@ -552,7 +558,7 @@ public class VolumeHelperTest {
}
@Test
- @RequiresFlagsDisabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
+ @RequiresFlagsDisabled({FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME, FLAG_ABS_VOLUME_INDEX_FIX})
public void configurablePreScaleAbsoluteVolume_checkIndex() throws Exception {
final int minIndex = mAm.getStreamMinVolume(STREAM_MUSIC);
final int maxIndex = mAm.getStreamMaxVolume(STREAM_MUSIC);
@@ -607,6 +613,7 @@ public class VolumeHelperTest {
@Test
@RequiresFlagsEnabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
+ @RequiresFlagsDisabled(FLAG_ABS_VOLUME_INDEX_FIX)
public void disablePreScaleAbsoluteVolume_checkIndex() throws Exception {
final int minIndex = mAm.getStreamMinVolume(STREAM_MUSIC);
final int maxIndex = mAm.getStreamMaxVolume(STREAM_MUSIC);
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 4a9760bc3317..e91fd3794a48 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2726,6 +2726,19 @@ public class ActivityRecordTests extends WindowTestsBase {
assertNoStartingWindow(activity);
}
+ @Test
+ public void testPostCleanupStartingWindow() {
+ registerTestStartingWindowOrganizer();
+ final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ activity.addStartingWindow(mPackageName, android.R.style.Theme, null, true, true, false,
+ true, false, false, false);
+ waitUntilHandlersIdle();
+ assertHasStartingWindow(activity);
+ // Simulate Shell remove starting window actively.
+ activity.mStartingWindow.removeImmediately();
+ assertNoStartingWindow(activity);
+ }
+
private void testLegacySplashScreen(int targetSdk, int verifyType) {
final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
activity.mTargetSdk = targetSdk;
diff --git a/telephony/java/android/telephony/CarrierRestrictionRules.java b/telephony/java/android/telephony/CarrierRestrictionRules.java
index d5db61233230..65e8e13036a6 100644
--- a/telephony/java/android/telephony/CarrierRestrictionRules.java
+++ b/telephony/java/android/telephony/CarrierRestrictionRules.java
@@ -555,10 +555,11 @@ public final class CarrierRestrictionRules implements Parcelable {
* Set the device's carrier restriction status
*
* @param carrierRestrictionStatus device restriction status
- * @hide
*/
public @NonNull
- Builder setCarrierRestrictionStatus(int carrierRestrictionStatus) {
+ @FlaggedApi(Flags.FLAG_SET_CARRIER_RESTRICTION_STATUS)
+ Builder setCarrierRestrictionStatus(
+ @CarrierRestrictionStatus int carrierRestrictionStatus) {
mRules.mCarrierRestrictionStatus = carrierRestrictionStatus;
return this;
}
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
index dc5013519dbf..ed6e8df3e293 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
@@ -23,6 +23,7 @@ import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.legacy.LegacyFlickerTestFactory
import android.tools.traces.component.ComponentNameMatcher
+import androidx.test.filters.FlakyTest
import com.android.server.wm.flicker.BaseTest
import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
import org.junit.FixMethodOrder
@@ -77,6 +78,7 @@ class CloseImeShownOnAppStartToAppOnPressBackTest(flicker: LegacyFlickerTest) :
@Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible()
+ @FlakyTest(bugId = 330486656)
@Presubmit
@Test
fun imeAppLayerIsAlwaysVisible() {
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index a8b383cd4274..093923f3ed53 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -101,8 +101,8 @@ public class PackageWatchdogTest {
private static final String OBSERVER_NAME_2 = "observer2";
private static final String OBSERVER_NAME_3 = "observer3";
private static final String OBSERVER_NAME_4 = "observer4";
- private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(10);
- private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(50);
+ private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1);
+ private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5);
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -1453,8 +1453,7 @@ public class PackageWatchdogTest {
raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A,
VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN);
- moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS
- - TimeUnit.MINUTES.toMillis(1));
+ moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS);
// The first failure will be outside the threshold.
raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A,
@@ -1713,9 +1712,6 @@ public class PackageWatchdogTest {
watchdog.onPackageFailure(packages, failureReason);
}
mTestLooper.dispatchAll();
- if (Flags.recoverabilityDetection()) {
- moveTimeForwardAndDispatch(watchdog.DEFAULT_MITIGATION_WINDOW_MS);
- }
}
private PackageWatchdog createWatchdog() {
diff --git a/tests/TrustTests/AndroidManifest.xml b/tests/TrustTests/AndroidManifest.xml
index 30cf345db34d..2f6c0dd14adc 100644
--- a/tests/TrustTests/AndroidManifest.xml
+++ b/tests/TrustTests/AndroidManifest.xml
@@ -78,6 +78,7 @@
<action android:name="android.service.trust.TrustAgentService" />
</intent-filter>
</service>
+
<service
android:name=".IsActiveUnlockRunningTrustAgent"
android:exported="true"
@@ -88,6 +89,16 @@
</intent-filter>
</service>
+ <service
+ android:name=".UnlockAttemptTrustAgent"
+ android:exported="true"
+ android:label="Test Agent"
+ android:permission="android.permission.BIND_TRUST_AGENT">
+ <intent-filter>
+ <action android:name="android.service.trust.TrustAgentService" />
+ </intent-filter>
+ </service>
+
</application>
<!-- self-instrumenting test package. -->
diff --git a/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt b/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt
new file mode 100644
index 000000000000..2c9361df63fd
--- /dev/null
+++ b/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt
@@ -0,0 +1,227 @@
+/*
+ * 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.trust.test
+
+import android.app.trust.TrustManager
+import android.content.Context
+import android.trust.BaseTrustAgentService
+import android.trust.TrustTestActivity
+import android.trust.test.lib.LockStateTrackingRule
+import android.trust.test.lib.ScreenLockRule
+import android.trust.test.lib.TestTrustListener
+import android.trust.test.lib.TrustAgentRule
+import android.util.Log
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
+
+/**
+ * Test for the impacts of reporting unlock attempts.
+ *
+ * atest TrustTests:UnlockAttemptTest
+ */
+@RunWith(AndroidJUnit4::class)
+class UnlockAttemptTest {
+ private val context = getApplicationContext<Context>()
+ private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager
+ private val userId = context.userId
+ private val activityScenarioRule = ActivityScenarioRule(TrustTestActivity::class.java)
+ private val screenLockRule = ScreenLockRule(requireStrongAuth = true)
+ private val lockStateTrackingRule = LockStateTrackingRule()
+ private val trustAgentRule =
+ TrustAgentRule<UnlockAttemptTrustAgent>(startUnlocked = false, startEnabled = false)
+
+ private val trustListener = UnlockAttemptTrustListener()
+ private val agent get() = trustAgentRule.agent
+
+ @get:Rule
+ val rule: RuleChain =
+ RuleChain.outerRule(activityScenarioRule)
+ .around(screenLockRule)
+ .around(lockStateTrackingRule)
+ .around(trustAgentRule)
+
+ @Before
+ fun setUp() {
+ trustManager.registerTrustListener(trustListener)
+ }
+
+ @Test
+ fun successfulUnlockAttempt_allowsTrustAgentToStart() =
+ runUnlockAttemptTest(enableAndVerifyTrustAgent = false, managingTrust = false) {
+ trustAgentRule.enableTrustAgent()
+
+ triggerSuccessfulUnlock()
+
+ trustAgentRule.verifyAgentIsRunning(MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START)
+ }
+
+ @Test
+ fun successfulUnlockAttempt_notifiesTrustAgent() =
+ runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) {
+ val oldSuccessfulCount = agent.successfulUnlockCallCount
+ val oldFailedCount = agent.failedUnlockCallCount
+
+ triggerSuccessfulUnlock()
+
+ assertThat(agent.successfulUnlockCallCount).isEqualTo(oldSuccessfulCount + 1)
+ assertThat(agent.failedUnlockCallCount).isEqualTo(oldFailedCount)
+ }
+
+ @Test
+ fun successfulUnlockAttempt_notifiesTrustListenerOfManagedTrust() =
+ runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) {
+ val oldTrustManagedChangedCount = trustListener.onTrustManagedChangedCount[userId] ?: 0
+
+ triggerSuccessfulUnlock()
+
+ assertThat(trustListener.onTrustManagedChangedCount[userId] ?: 0).isEqualTo(
+ oldTrustManagedChangedCount + 1
+ )
+ }
+
+ @Test
+ fun failedUnlockAttempt_doesNotAllowTrustAgentToStart() =
+ runUnlockAttemptTest(enableAndVerifyTrustAgent = false, managingTrust = false) {
+ trustAgentRule.enableTrustAgent()
+
+ triggerFailedUnlock()
+
+ trustAgentRule.ensureAgentIsNotRunning(MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START)
+ }
+
+ @Test
+ fun failedUnlockAttempt_notifiesTrustAgent() =
+ runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) {
+ val oldSuccessfulCount = agent.successfulUnlockCallCount
+ val oldFailedCount = agent.failedUnlockCallCount
+
+ triggerFailedUnlock()
+
+ assertThat(agent.successfulUnlockCallCount).isEqualTo(oldSuccessfulCount)
+ assertThat(agent.failedUnlockCallCount).isEqualTo(oldFailedCount + 1)
+ }
+
+ @Test
+ fun failedUnlockAttempt_doesNotNotifyTrustListenerOfManagedTrust() =
+ runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) {
+ val oldTrustManagedChangedCount = trustListener.onTrustManagedChangedCount[userId] ?: 0
+
+ triggerFailedUnlock()
+
+ assertThat(trustListener.onTrustManagedChangedCount[userId] ?: 0).isEqualTo(
+ oldTrustManagedChangedCount
+ )
+ }
+
+ private fun runUnlockAttemptTest(
+ enableAndVerifyTrustAgent: Boolean,
+ managingTrust: Boolean,
+ testBlock: () -> Unit,
+ ) {
+ if (enableAndVerifyTrustAgent) {
+ Log.i(TAG, "Triggering successful unlock")
+ triggerSuccessfulUnlock()
+ Log.i(TAG, "Enabling and waiting for trust agent")
+ trustAgentRule.enableAndVerifyTrustAgentIsRunning(
+ MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START
+ )
+ Log.i(TAG, "Managing trust: $managingTrust")
+ agent.setManagingTrust(managingTrust)
+ await()
+ }
+ testBlock()
+ }
+
+ private fun triggerSuccessfulUnlock() {
+ screenLockRule.successfulScreenLockAttempt()
+ trustAgentRule.reportSuccessfulUnlock()
+ await()
+ }
+
+ private fun triggerFailedUnlock() {
+ screenLockRule.failedScreenLockAttempt()
+ trustAgentRule.reportFailedUnlock()
+ await()
+ }
+
+ companion object {
+ private const val TAG = "UnlockAttemptTest"
+ private fun await(millis: Long = 500) = Thread.sleep(millis)
+ private const val MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START = 10000L
+ }
+}
+
+class UnlockAttemptTrustAgent : BaseTrustAgentService() {
+ var successfulUnlockCallCount: Long = 0
+ private set
+ var failedUnlockCallCount: Long = 0
+ private set
+
+ override fun onUnlockAttempt(successful: Boolean) {
+ super.onUnlockAttempt(successful)
+ if (successful) {
+ successfulUnlockCallCount++
+ } else {
+ failedUnlockCallCount++
+ }
+ }
+}
+
+private class UnlockAttemptTrustListener : TestTrustListener() {
+ var enabledTrustAgentsChangedCount = mutableMapOf<Int, Int>()
+ var onTrustManagedChangedCount = mutableMapOf<Int, Int>()
+
+ override fun onEnabledTrustAgentsChanged(userId: Int) {
+ enabledTrustAgentsChangedCount.compute(userId) { _: Int, curr: Int? ->
+ if (curr == null) 0 else curr + 1
+ }
+ }
+
+ data class TrustChangedParams(
+ val enabled: Boolean,
+ val newlyUnlocked: Boolean,
+ val userId: Int,
+ val flags: Int,
+ val trustGrantedMessages: MutableList<String>?
+ )
+
+ val onTrustChangedCalls = mutableListOf<TrustChangedParams>()
+
+ override fun onTrustChanged(
+ enabled: Boolean,
+ newlyUnlocked: Boolean,
+ userId: Int,
+ flags: Int,
+ trustGrantedMessages: MutableList<String>
+ ) {
+ onTrustChangedCalls += TrustChangedParams(
+ enabled, newlyUnlocked, userId, flags, trustGrantedMessages
+ )
+ }
+
+ override fun onTrustManagedChanged(enabled: Boolean, userId: Int) {
+ onTrustManagedChangedCount.compute(userId) { _: Int, curr: Int? ->
+ if (curr == null) 0 else curr + 1
+ }
+ }
+}
diff --git a/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt
index f1edca3ff86e..1ccdcc623c5b 100644
--- a/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt
+++ b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt
@@ -24,6 +24,8 @@ import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.UiDevice
import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
import com.android.internal.widget.LockscreenCredential
import com.google.common.truth.Truth.assertWithMessage
import org.junit.rules.TestRule
@@ -32,13 +34,18 @@ import org.junit.runners.model.Statement
/**
* Sets a screen lock on the device for the duration of the test.
+ *
+ * @param requireStrongAuth Whether a strong auth is required at the beginning.
+ * If true, trust agents will not be available until the user verifies their credentials.
*/
-class ScreenLockRule : TestRule {
+class ScreenLockRule(val requireStrongAuth: Boolean = false) : TestRule {
private val context: Context = getApplicationContext()
+ private val userId = context.userId
private val uiDevice = UiDevice.getInstance(getInstrumentation())
private val windowManager = checkNotNull(WindowManagerGlobal.getWindowManagerService())
private val lockPatternUtils = LockPatternUtils(context)
private var instantLockSavedValue = false
+ private var strongAuthSavedValue: Int = 0
override fun apply(base: Statement, description: Description) = object : Statement() {
override fun evaluate() {
@@ -46,10 +53,12 @@ class ScreenLockRule : TestRule {
dismissKeyguard()
setScreenLock()
setLockOnPowerButton()
+ configureStrongAuthState()
try {
base.evaluate()
} finally {
+ restoreStrongAuthState()
removeScreenLock()
revertLockOnPowerButton()
dismissKeyguard()
@@ -57,6 +66,22 @@ class ScreenLockRule : TestRule {
}
}
+ private fun configureStrongAuthState() {
+ strongAuthSavedValue = lockPatternUtils.getStrongAuthForUser(userId)
+ if (requireStrongAuth) {
+ Log.d(TAG, "Triggering strong auth due to simulated lockdown")
+ lockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, userId)
+ wait("strong auth required after lockdown") {
+ lockPatternUtils.getStrongAuthForUser(userId) ==
+ STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
+ }
+ }
+ }
+
+ private fun restoreStrongAuthState() {
+ lockPatternUtils.requireStrongAuth(strongAuthSavedValue, userId)
+ }
+
private fun verifyNoScreenLockAlreadySet() {
assertWithMessage("Screen Lock must not already be set on device")
.that(lockPatternUtils.isSecure(context.userId))
@@ -82,6 +107,22 @@ class ScreenLockRule : TestRule {
}
}
+ fun successfulScreenLockAttempt() {
+ lockPatternUtils.verifyCredential(LockscreenCredential.createPin(PIN), context.userId, 0)
+ lockPatternUtils.userPresent(context.userId)
+ wait("strong auth not required") {
+ lockPatternUtils.getStrongAuthForUser(context.userId) == STRONG_AUTH_NOT_REQUIRED
+ }
+ }
+
+ fun failedScreenLockAttempt() {
+ lockPatternUtils.verifyCredential(
+ LockscreenCredential.createPin(WRONG_PIN),
+ context.userId,
+ 0
+ )
+ }
+
private fun setScreenLock() {
lockPatternUtils.setLockCredential(
LockscreenCredential.createPin(PIN),
@@ -121,5 +162,6 @@ class ScreenLockRule : TestRule {
companion object {
private const val TAG = "ScreenLockRule"
private const val PIN = "0000"
+ private const val WRONG_PIN = "0001"
}
}
diff --git a/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt
index 18bc029b6845..404c6d968b3a 100644
--- a/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt
+++ b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt
@@ -20,14 +20,15 @@ import android.app.trust.TrustManager
import android.content.ComponentName
import android.content.Context
import android.trust.BaseTrustAgentService
+import android.trust.test.lib.TrustAgentRule.Companion.invoke
import android.util.Log
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.android.internal.widget.LockPatternUtils
import com.google.common.truth.Truth.assertWithMessage
+import kotlin.reflect.KClass
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
-import kotlin.reflect.KClass
/**
* Enables a trust agent and causes the system service to bind to it.
@@ -37,7 +38,9 @@ import kotlin.reflect.KClass
* @constructor Creates the rule. Do not use; instead, use [invoke].
*/
class TrustAgentRule<T : BaseTrustAgentService>(
- private val serviceClass: KClass<T>
+ private val serviceClass: KClass<T>,
+ private val startUnlocked: Boolean,
+ private val startEnabled: Boolean,
) : TestRule {
private val context: Context = getApplicationContext()
private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager
@@ -48,11 +51,18 @@ class TrustAgentRule<T : BaseTrustAgentService>(
override fun apply(base: Statement, description: Description) = object : Statement() {
override fun evaluate() {
verifyTrustServiceRunning()
- unlockDeviceWithCredential()
- enableTrustAgent()
+ if (startUnlocked) {
+ reportSuccessfulUnlock()
+ } else {
+ Log.i(TAG, "Trust manager not starting in unlocked state")
+ }
try {
- verifyAgentIsRunning()
+ if (startEnabled) {
+ enableAndVerifyTrustAgentIsRunning()
+ } else {
+ Log.i(TAG, "Trust agent ${serviceClass.simpleName} not enabled")
+ }
base.evaluate()
} finally {
disableTrustAgent()
@@ -64,12 +74,22 @@ class TrustAgentRule<T : BaseTrustAgentService>(
assertWithMessage("Trust service is not running").that(trustManager).isNotNull()
}
- private fun unlockDeviceWithCredential() {
- Log.d(TAG, "Unlocking device with credential")
+ fun reportSuccessfulUnlock() {
+ Log.i(TAG, "Reporting successful unlock")
trustManager.reportUnlockAttempt(true, context.userId)
}
- private fun enableTrustAgent() {
+ fun reportFailedUnlock() {
+ Log.i(TAG, "Reporting failed unlock")
+ trustManager.reportUnlockAttempt(false, context.userId)
+ }
+
+ fun enableAndVerifyTrustAgentIsRunning(maxWait: Long = 30000L) {
+ enableTrustAgent()
+ verifyAgentIsRunning(maxWait)
+ }
+
+ fun enableTrustAgent() {
val componentName = ComponentName(context, serviceClass.java)
val userId = context.userId
Log.i(TAG, "Enabling trust agent ${componentName.flattenToString()} for user $userId")
@@ -79,12 +99,18 @@ class TrustAgentRule<T : BaseTrustAgentService>(
lockPatternUtils.setEnabledTrustAgents(agents, userId)
}
- private fun verifyAgentIsRunning() {
- wait("${serviceClass.simpleName} to be running") {
+ fun verifyAgentIsRunning(maxWait: Long = 30000L) {
+ wait("${serviceClass.simpleName} to be running", maxWait) {
BaseTrustAgentService.instance(serviceClass) != null
}
}
+ fun ensureAgentIsNotRunning(window: Long = 30000L) {
+ ensure("${serviceClass.simpleName} is not running", window) {
+ BaseTrustAgentService.instance(serviceClass) == null
+ }
+ }
+
private fun disableTrustAgent() {
val componentName = ComponentName(context, serviceClass.java)
val userId = context.userId
@@ -97,13 +123,23 @@ class TrustAgentRule<T : BaseTrustAgentService>(
companion object {
/**
- * Creates a new rule for the specified agent class. Example usage:
+ * Creates a new rule for the specified agent class. Starts with the device unlocked and
+ * the trust agent enabled. Example usage:
* ```
* @get:Rule val rule = TrustAgentRule<MyTestAgent>()
* ```
+ *
+ * Also supports setting different device lock and trust agent enablement states:
+ * ```
+ * @get:Rule val rule = TrustAgentRule<MyTestAgent>(startUnlocked = false, startEnabled = false)
+ * ```
*/
- inline operator fun <reified T : BaseTrustAgentService> invoke() =
- TrustAgentRule(T::class)
+ inline operator fun <reified T : BaseTrustAgentService> invoke(
+ startUnlocked: Boolean = true,
+ startEnabled: Boolean = true,
+ ) =
+ TrustAgentRule(T::class, startUnlocked, startEnabled)
+
private const val TAG = "TrustAgentRule"
}
diff --git a/tests/TrustTests/src/android/trust/test/lib/utils.kt b/tests/TrustTests/src/android/trust/test/lib/Utils.kt
index e047202f6740..3b32b47a6160 100644
--- a/tests/TrustTests/src/android/trust/test/lib/utils.kt
+++ b/tests/TrustTests/src/android/trust/test/lib/Utils.kt
@@ -39,7 +39,7 @@ internal fun wait(
) {
var waited = 0L
var count = 0
- while (!conditionFunction.invoke(count)) {
+ while (!conditionFunction(count)) {
assertWithMessage("Condition exceeded maximum wait time of $maxWait ms: $description")
.that(waited <= maxWait)
.isTrue()
@@ -49,3 +49,34 @@ internal fun wait(
Thread.sleep(rate)
}
}
+
+/**
+ * Ensures that [conditionFunction] is true with a failed assertion if it is not within [window]
+ * ms.
+ *
+ * The condition function can perform additional logic (for example, logging or attempting to make
+ * the condition become true).
+ *
+ * @param conditionFunction function which takes the attempt count & returns whether the condition
+ * is met
+ */
+internal fun ensure(
+ description: String? = null,
+ window: Long = 30000L,
+ rate: Long = 50L,
+ conditionFunction: (count: Int) -> Boolean
+) {
+ var waited = 0L
+ var count = 0
+ while (waited <= window) {
+ assertWithMessage("Condition failed within $window ms: $description").that(
+ conditionFunction(
+ count
+ )
+ ).isTrue()
+ waited += rate
+ count++
+ Log.i(TAG, "Ensuring $description ($waited/$window) #$count")
+ Thread.sleep(rate)
+ }
+}