Merge "Don't read AnimatedState.value during composition" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index c231b30..c477a75 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -249,6 +249,14 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+java_aconfig_library {
+ name: "android.app.usage.flags-aconfig-java-host",
+ aconfig_declarations: "android.app.usage.flags-aconfig",
+ host_supported: true,
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
+
// OS
aconfig_declarations {
name: "android.os.flags-aconfig",
diff --git a/Android.bp b/Android.bp
index 13b1703..f6a9328 100644
--- a/Android.bp
+++ b/Android.bp
@@ -220,6 +220,7 @@
"updatable-driver-protos",
"ota_metadata_proto_java",
"android.hidl.base-V1.0-java",
+ "android.hidl.manager-V1.2-java",
"android.hardware.cas-V1-java", // AIDL
"android.hardware.cas-V1.0-java",
"android.hardware.cas-V1.1-java",
diff --git a/core/api/current.txt b/core/api/current.txt
index 95af71c..8109e04 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -12368,6 +12368,7 @@
public final class ModuleInfo implements android.os.Parcelable {
method public int describeContents();
+ method @FlaggedApi("android.content.pm.provide_info_of_apk_in_apex") @NonNull public java.util.Collection<java.lang.String> getApkInApexPackageNames();
method @Nullable public CharSequence getName();
method @Nullable public String getPackageName();
method public boolean isHidden();
@@ -12378,6 +12379,7 @@
public class PackageInfo implements android.os.Parcelable {
ctor public PackageInfo();
method public int describeContents();
+ method @FlaggedApi("android.content.pm.provide_info_of_apk_in_apex") @Nullable public String getApexPackageName();
method @FlaggedApi("android.content.pm.archiving") public long getArchiveTimeMillis();
method public long getLongVersionCode();
method public void setLongVersionCode(long);
diff --git a/core/java/android/content/pm/ModuleInfo.java b/core/java/android/content/pm/ModuleInfo.java
index a7306a3..a1c8747 100644
--- a/core/java/android/content/pm/ModuleInfo.java
+++ b/core/java/android/content/pm/ModuleInfo.java
@@ -16,10 +16,15 @@
package android.content.pm;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
import java.util.Objects;
/**
@@ -46,6 +51,13 @@
/** Whether or not this module is hidden from the user. */
private boolean mHidden;
+ /**
+ * The list of the package names of all APK-in-APEX apps in the module, or
+ * null if there are none.
+ */
+ @Nullable
+ private List<String> mApkInApexPackageNames;
+
// TODO: Decide whether we need an additional metadata bundle to support out of band
// updates to ModuleInfo.
//
@@ -61,6 +73,9 @@
mPackageName = orig.mPackageName;
mHidden = orig.mHidden;
mApexModuleName = orig.mApexModuleName;
+ if (orig.mApkInApexPackageNames != null) {
+ mApkInApexPackageNames = List.copyOf(orig.mApkInApexPackageNames);
+ }
}
/** @hide Sets the public name of this module. */
@@ -107,6 +122,25 @@
return mApexModuleName;
}
+ /** @hide Sets the list of the package name of APK-in-APEX apps in this module. */
+ public ModuleInfo setApkInApexPackageNames(@NonNull Collection<String> apkInApexPackageNames) {
+ Objects.requireNonNull(apkInApexPackageNames);
+ mApkInApexPackageNames = List.copyOf(apkInApexPackageNames);
+ return this;
+ }
+
+ /**
+ * Gets the list of the package name of all APK-in-APEX apps in the module.
+ */
+ @NonNull
+ @FlaggedApi(android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
+ public Collection<String> getApkInApexPackageNames() {
+ if (mApkInApexPackageNames == null) {
+ return Collections.emptyList();
+ }
+ return mApkInApexPackageNames;
+ }
+
/** Returns a string representation of this object. */
public String toString() {
return "ModuleInfo{"
@@ -125,6 +159,7 @@
hashCode = 31 * hashCode + Objects.hashCode(mName);
hashCode = 31 * hashCode + Objects.hashCode(mPackageName);
hashCode = 31 * hashCode + Objects.hashCode(mApexModuleName);
+ hashCode = 31 * hashCode + Objects.hashCode(mApkInApexPackageNames);
hashCode = 31 * hashCode + Boolean.hashCode(mHidden);
return hashCode;
}
@@ -138,6 +173,7 @@
return Objects.equals(mName, other.mName)
&& Objects.equals(mPackageName, other.mPackageName)
&& Objects.equals(mApexModuleName, other.mApexModuleName)
+ && Objects.equals(mApkInApexPackageNames, other.mApkInApexPackageNames)
&& mHidden == other.mHidden;
}
@@ -147,6 +183,8 @@
dest.writeString(mPackageName);
dest.writeBoolean(mHidden);
dest.writeString(mApexModuleName);
+ // Parcel#writeStringList handles null case, we can use it directly
+ dest.writeStringList(mApkInApexPackageNames);
}
private ModuleInfo(Parcel source) {
@@ -154,6 +192,7 @@
mPackageName = source.readString();
mHidden = source.readBoolean();
mApexModuleName = source.readString();
+ mApkInApexPackageNames = source.createStringArrayList();
}
public static final @android.annotation.NonNull Parcelable.Creator<ModuleInfo> CREATOR =
diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java
index 4f61613..c1c9928 100644
--- a/core/java/android/content/pm/PackageInfo.java
+++ b/core/java/android/content/pm/PackageInfo.java
@@ -499,6 +499,16 @@
*/
public boolean isActiveApex;
+ /**
+ * If the package is an APEX package (i.e. the value of {@link #isApex}
+ * is true), this field is the package name of the APEX. If the package
+ * is one APK-in-APEX app, this field is the package name of the parent
+ * APEX that contains the app. If the package is not one of the above
+ * two cases, this field is {@code null}.
+ */
+ @Nullable
+ private String mApexPackageName;
+
public PackageInfo() {
}
@@ -535,6 +545,26 @@
mArchiveTimeMillis = value;
}
+ /**
+ * If the package is an APEX package (i.e. the value of {@link #isApex}
+ * is true), returns the package name of the APEX. If the package
+ * is one APK-in-APEX app, returns the package name of the parent
+ * APEX that contains the app. If the package is not one of the above
+ * two cases, returns {@code null}.
+ */
+ @Nullable
+ @FlaggedApi(android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
+ public String getApexPackageName() {
+ return mApexPackageName;
+ }
+
+ /**
+ * @hide
+ */
+ public void setApexPackageName(@Nullable String apexPackageName) {
+ mApexPackageName = apexPackageName;
+ }
+
@Override
public String toString() {
return "PackageInfo{"
@@ -603,6 +633,12 @@
dest.writeBoolean(isApex);
dest.writeBoolean(isActiveApex);
dest.writeLong(mArchiveTimeMillis);
+ if (mApexPackageName != null) {
+ dest.writeInt(1);
+ dest.writeString8(mApexPackageName);
+ } else {
+ dest.writeInt(0);
+ }
dest.restoreAllowSquashing(prevAllowSquashing);
}
@@ -669,5 +705,9 @@
isApex = source.readBoolean();
isActiveApex = source.readBoolean();
mArchiveTimeMillis = source.readLong();
+ int hasApexPackageName = source.readInt();
+ if (hasApexPackageName != 0) {
+ mApexPackageName = source.readString8();
+ }
}
}
diff --git a/core/java/android/credentials/ui/Constants.java b/core/java/android/credentials/ui/Constants.java
index 7092f29..37f850b 100644
--- a/core/java/android/credentials/ui/Constants.java
+++ b/core/java/android/credentials/ui/Constants.java
@@ -29,6 +29,13 @@
public static final String EXTRA_RESULT_RECEIVER =
"android.credentials.ui.extra.RESULT_RECEIVER";
+ /**
+ * The intent extra key for indicating whether the bottom sheet should be started directly
+ * on the 'All Options' screen.
+ */
+ public static final String EXTRA_REQ_FOR_ALL_OPTIONS =
+ "android.credentials.ui.extra.REQ_FOR_ALL_OPTIONS";
+
/** The intent action for when the enabled Credential Manager providers has been updated. */
public static final String CREDMAN_ENABLED_PROVIDERS_UPDATED =
"android.credentials.ui.action.CREDMAN_ENABLED_PROVIDERS_UPDATED";
diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java
index 5e8372d..49321d5 100644
--- a/core/java/android/credentials/ui/IntentFactory.java
+++ b/core/java/android/credentials/ui/IntentFactory.java
@@ -35,6 +35,31 @@
*/
@TestApi
public class IntentFactory {
+
+ /**
+ * Generate a new launch intent to the Credential Selector UI.
+ *
+ * @hide
+ */
+ @NonNull
+ public static Intent createCredentialSelectorIntent(
+ @NonNull RequestInfo requestInfo,
+ @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
+ @NonNull
+ ArrayList<ProviderData> enabledProviderDataList,
+ @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
+ @NonNull
+ ArrayList<DisabledProviderData> disabledProviderDataList,
+ @NonNull ResultReceiver resultReceiver,
+ boolean isRequestForAllOptions) {
+
+ Intent intent = createCredentialSelectorIntent(requestInfo, enabledProviderDataList,
+ disabledProviderDataList, resultReceiver);
+ intent.putExtra(Constants.EXTRA_REQ_FOR_ALL_OPTIONS, isRequestForAllOptions);
+
+ return intent;
+ }
+
/** Generate a new launch intent to the Credential Selector UI. */
@NonNull
public static Intent createCredentialSelectorIntent(
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 6626baf..7bea9ae 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -25,6 +25,7 @@
import android.hardware.input.IInputDeviceBatteryState;
import android.hardware.input.IKeyboardBacklightListener;
import android.hardware.input.IKeyboardBacklightState;
+import android.hardware.input.IStickyModifierStateListener;
import android.hardware.input.ITabletModeChangedListener;
import android.hardware.input.TouchCalibration;
import android.os.CombinedVibration;
@@ -241,4 +242,14 @@
void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener);
HostUsiVersion getHostUsiVersionFromDisplayConfig(int displayId);
+
+ @EnforcePermission("MONITOR_STICKY_MODIFIER_STATE")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)")
+ void registerStickyModifierStateListener(IStickyModifierStateListener listener);
+
+ @EnforcePermission("MONITOR_STICKY_MODIFIER_STATE")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)")
+ void unregisterStickyModifierStateListener(IStickyModifierStateListener listener);
}
diff --git a/core/java/android/hardware/input/IStickyModifierStateListener.aidl b/core/java/android/hardware/input/IStickyModifierStateListener.aidl
new file mode 100644
index 0000000..bd139ab
--- /dev/null
+++ b/core/java/android/hardware/input/IStickyModifierStateListener.aidl
@@ -0,0 +1,26 @@
+/*
+ * 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 android.hardware.input;
+
+/** @hide */
+oneway interface IStickyModifierStateListener {
+
+ /**
+ * Called when the sticky modifier state is changed when A11y Sticky keys feature is enabled
+ */
+ void onStickyModifierStateChanged(int modifierState, int lockedModifierState);
+}
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index f941ad8..4ebbde7 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1297,6 +1297,42 @@
}
/**
+ * Registers a Sticky modifier state change listener to be notified about {@link
+ * StickyModifierState} changes.
+ *
+ * @param executor an executor on which the callback will be called
+ * @param listener the {@link StickyModifierStateListener}
+ * @throws IllegalArgumentException if {@code listener} has already been registered previously.
+ * @throws NullPointerException if {@code listener} or {@code executor} is null.
+ * @hide
+ * @see #unregisterStickyModifierStateListener(StickyModifierStateListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ public void registerStickyModifierStateListener(@NonNull Executor executor,
+ @NonNull StickyModifierStateListener listener) throws IllegalArgumentException {
+ if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
+ return;
+ }
+ mGlobal.registerStickyModifierStateListener(executor, listener);
+ }
+
+ /**
+ * Unregisters a previously added Sticky modifier state change listener.
+ *
+ * @param listener the {@link StickyModifierStateListener}
+ * @hide
+ * @see #registerStickyModifierStateListener(Executor, StickyModifierStateListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ public void unregisterStickyModifierStateListener(
+ @NonNull StickyModifierStateListener listener) {
+ if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
+ return;
+ }
+ mGlobal.unregisterStickyModifierStateListener(listener);
+ }
+
+ /**
* A callback used to be notified about battery state changes for an input device. The
* {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the
* listener is successfully registered to provide the initial battery state of the device.
@@ -1378,4 +1414,23 @@
void onKeyboardBacklightChanged(
int deviceId, @NonNull KeyboardBacklightState state, boolean isTriggeredByKeyPress);
}
+
+ /**
+ * A callback used to be notified about sticky modifier state changes when A11y Sticky keys
+ * feature is enabled.
+ *
+ * @see #registerStickyModifierStateListener(Executor, StickyModifierStateListener)
+ * @see #unregisterStickyModifierStateListener(StickyModifierStateListener)
+ * @hide
+ */
+ public interface StickyModifierStateListener {
+ /**
+ * Called when the sticky modifier state changes.
+ * This method will be called once after the listener is successfully registered to provide
+ * the initial modifier state.
+ *
+ * @param state the new sticky modifier state, never null.
+ */
+ void onStickyModifierStateChanged(@NonNull StickyModifierState state);
+ }
}
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index 24a6911..7c104a0 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -27,6 +27,7 @@
import android.hardware.input.InputManager.InputDeviceListener;
import android.hardware.input.InputManager.KeyboardBacklightListener;
import android.hardware.input.InputManager.OnTabletModeChangedListener;
+import android.hardware.input.InputManager.StickyModifierStateListener;
import android.hardware.lights.Light;
import android.hardware.lights.LightState;
import android.hardware.lights.LightsManager;
@@ -52,6 +53,7 @@
import android.view.InputEvent;
import android.view.InputMonitor;
import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
import android.view.PointerIcon;
import com.android.internal.annotations.GuardedBy;
@@ -100,6 +102,14 @@
@GuardedBy("mKeyboardBacklightListenerLock")
@Nullable private IKeyboardBacklightListener mKeyboardBacklightListener;
+ private final Object mStickyModifierStateListenerLock = new Object();
+ @GuardedBy("mStickyModifierStateListenerLock")
+ @Nullable
+ private ArrayList<StickyModifierStateListenerDelegate> mStickyModifierStateListeners;
+ @GuardedBy("mStickyModifierStateListenerLock")
+ @Nullable
+ private IStickyModifierStateListener mStickyModifierStateListener;
+
// InputDeviceSensorManager gets notified synchronously from the binder thread when input
// devices change, so it must be synchronized with the input device listeners.
@GuardedBy("mInputDeviceListeners")
@@ -905,6 +915,158 @@
}
}
+ private static final class StickyModifierStateListenerDelegate {
+ final InputManager.StickyModifierStateListener mListener;
+ final Executor mExecutor;
+
+ StickyModifierStateListenerDelegate(StickyModifierStateListener listener,
+ Executor executor) {
+ mListener = listener;
+ mExecutor = executor;
+ }
+
+ void notifyStickyModifierStateChange(int modifierState, int lockedModifierState) {
+ mExecutor.execute(() ->
+ mListener.onStickyModifierStateChanged(
+ new LocalStickyModifierState(modifierState, lockedModifierState)));
+ }
+ }
+
+ private class LocalStickyModifierStateListener extends IStickyModifierStateListener.Stub {
+
+ @Override
+ public void onStickyModifierStateChanged(int modifierState, int lockedModifierState) {
+ synchronized (mStickyModifierStateListenerLock) {
+ if (mStickyModifierStateListeners == null) return;
+ final int numListeners = mStickyModifierStateListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ mStickyModifierStateListeners.get(i)
+ .notifyStickyModifierStateChange(modifierState, lockedModifierState);
+ }
+ }
+ }
+ }
+
+ // Implementation of the android.hardware.input.StickyModifierState interface used to report
+ // the sticky modifier state via the StickyModifierStateListener interfaces.
+ private static final class LocalStickyModifierState extends StickyModifierState {
+
+ private final int mModifierState;
+ private final int mLockedModifierState;
+
+ LocalStickyModifierState(int modifierState, int lockedModifierState) {
+ mModifierState = modifierState;
+ mLockedModifierState = lockedModifierState;
+ }
+
+ @Override
+ public boolean isShiftModifierOn() {
+ return (mModifierState & KeyEvent.META_SHIFT_ON) != 0;
+ }
+
+ @Override
+ public boolean isShiftModifierLocked() {
+ return (mLockedModifierState & KeyEvent.META_SHIFT_ON) != 0;
+ }
+
+ @Override
+ public boolean isCtrlModifierOn() {
+ return (mModifierState & KeyEvent.META_CTRL_ON) != 0;
+ }
+
+ @Override
+ public boolean isCtrlModifierLocked() {
+ return (mLockedModifierState & KeyEvent.META_CTRL_ON) != 0;
+ }
+
+ @Override
+ public boolean isMetaModifierOn() {
+ return (mModifierState & KeyEvent.META_META_ON) != 0;
+ }
+
+ @Override
+ public boolean isMetaModifierLocked() {
+ return (mLockedModifierState & KeyEvent.META_META_ON) != 0;
+ }
+
+ @Override
+ public boolean isAltModifierOn() {
+ return (mModifierState & KeyEvent.META_ALT_LEFT_ON) != 0;
+ }
+
+ @Override
+ public boolean isAltModifierLocked() {
+ return (mLockedModifierState & KeyEvent.META_ALT_LEFT_ON) != 0;
+ }
+
+ @Override
+ public boolean isAltGrModifierOn() {
+ return (mModifierState & KeyEvent.META_ALT_RIGHT_ON) != 0;
+ }
+
+ @Override
+ public boolean isAltGrModifierLocked() {
+ return (mLockedModifierState & KeyEvent.META_ALT_RIGHT_ON) != 0;
+ }
+ }
+
+ /**
+ * @see InputManager#registerStickyModifierStateListener(Executor, StickyModifierStateListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ void registerStickyModifierStateListener(@NonNull Executor executor,
+ @NonNull StickyModifierStateListener listener) throws IllegalArgumentException {
+ Objects.requireNonNull(executor, "executor should not be null");
+ Objects.requireNonNull(listener, "listener should not be null");
+
+ synchronized (mStickyModifierStateListenerLock) {
+ if (mStickyModifierStateListener == null) {
+ mStickyModifierStateListeners = new ArrayList<>();
+ mStickyModifierStateListener = new LocalStickyModifierStateListener();
+
+ try {
+ mIm.registerStickyModifierStateListener(mStickyModifierStateListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ final int numListeners = mStickyModifierStateListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ if (mStickyModifierStateListeners.get(i).mListener == listener) {
+ throw new IllegalArgumentException("Listener has already been registered!");
+ }
+ }
+ StickyModifierStateListenerDelegate delegate =
+ new StickyModifierStateListenerDelegate(listener, executor);
+ mStickyModifierStateListeners.add(delegate);
+ }
+ }
+
+ /**
+ * @see InputManager#unregisterStickyModifierStateListener(StickyModifierStateListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ void unregisterStickyModifierStateListener(
+ @NonNull StickyModifierStateListener listener) {
+ Objects.requireNonNull(listener, "listener should not be null");
+
+ synchronized (mStickyModifierStateListenerLock) {
+ if (mStickyModifierStateListeners == null) {
+ return;
+ }
+ mStickyModifierStateListeners.removeIf((delegate) -> delegate.mListener == listener);
+ if (mStickyModifierStateListeners.isEmpty()) {
+ try {
+ mIm.unregisterStickyModifierStateListener(mStickyModifierStateListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mStickyModifierStateListeners = null;
+ mStickyModifierStateListener = null;
+ }
+ }
+ }
+
/**
* @see InputManager#getKeyboardLayoutsForInputDevice(InputDeviceIdentifier)
*/
diff --git a/core/java/android/hardware/input/StickyModifierState.java b/core/java/android/hardware/input/StickyModifierState.java
new file mode 100644
index 0000000..a3f7a0a
--- /dev/null
+++ b/core/java/android/hardware/input/StickyModifierState.java
@@ -0,0 +1,127 @@
+/*
+ * 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 android.hardware.input;
+
+/**
+ * The StickyModifierState class is a representation of a modifier state when A11y Sticky keys
+ * feature is enabled
+ *
+ * @hide
+ */
+public abstract class StickyModifierState {
+
+ /**
+ * Represents whether current sticky modifier state includes 'Shift' modifier.
+ * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Shift' modifier in
+ * its metaState.
+ *
+ * @return whether Shift modifier key is on.
+ */
+ public abstract boolean isShiftModifierOn();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Shift' modifier, and it is
+ * locked.
+ * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Shift'
+ * modifier in its metaState and this state will remain sticky (will not be cleared), until
+ * user presses 'Shift' key again to clear the locked state.
+ *
+ * @return whether Shift modifier key is locked.
+ */
+ public abstract boolean isShiftModifierLocked();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Ctrl' modifier.
+ * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Ctrl' modifier in
+ * its metaState.
+ *
+ * @return whether Ctrl modifier key is on.
+ */
+ public abstract boolean isCtrlModifierOn();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Ctrl' modifier, and it is
+ * locked.
+ * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Ctrl'
+ * modifier in its metaState and this state will remain sticky (will not be cleared), until
+ * user presses 'Ctrl' key again to clear the locked state.
+ *
+ * @return whether Ctrl modifier key is locked.
+ */
+ public abstract boolean isCtrlModifierLocked();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Meta' modifier.
+ * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Meta' modifier in
+ * its metaState.
+ *
+ * @return whether Meta modifier key is on.
+ */
+ public abstract boolean isMetaModifierOn();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Meta' modifier, and it is
+ * locked.
+ * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Meta'
+ * modifier in its metaState and this state will remain sticky (will not be cleared), until
+ * user presses 'Meta' key again to clear the locked state.
+ *
+ * @return whether Meta modifier key is locked.
+ */
+ public abstract boolean isMetaModifierLocked();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Alt' modifier.
+ * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Alt' modifier in
+ * its metaState.
+ *
+ * @return whether Alt modifier key is on.
+ */
+ public abstract boolean isAltModifierOn();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Alt' modifier, and it is
+ * locked.
+ * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Alt'
+ * modifier in its metaState and this state will remain sticky (will not be cleared), until
+ * user presses 'Alt' key again to clear the locked state.
+ *
+ * @return whether Alt modifier key is locked.
+ */
+ public abstract boolean isAltModifierLocked();
+
+ /**
+ * Represents whether current sticky modifier state includes 'AltGr' modifier.
+ * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'AltGr' modifier in
+ * its metaState.
+ *
+ * @return whether AltGr modifier key is on.
+ */
+ public abstract boolean isAltGrModifierOn();
+
+ /**
+ * Represents whether current sticky modifier state includes 'AltGr' modifier, and it is
+ * locked.
+ * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'AltGr'
+ * modifier in its metaState and this state will remain sticky (will not be cleared), until
+ * user presses 'AltGr' key again to clear the locked state.
+ *
+ * @return whether AltGr modifier key is locked.
+ */
+ public abstract boolean isAltGrModifierLocked();
+}
+
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 18d3e5e..71698e4 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -127,6 +127,7 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InlineSuggestionsResponse;
@@ -388,6 +389,9 @@
private long mStylusHwSessionsTimeout = STYLUS_HANDWRITING_IDLE_TIMEOUT_MS;
private Runnable mStylusWindowIdleTimeoutRunnable;
private long mStylusWindowIdleTimeoutForTest;
+ /** Tracks last {@link MotionEvent#getToolType(int)} used for {@link MotionEvent#ACTION_DOWN}.
+ **/
+ private int mLastUsedToolType;
/**
* Returns whether {@link InputMethodService} is responsible for rendering the back button and
@@ -1005,7 +1009,7 @@
*/
@Override
public void updateEditorToolType(@ToolType int toolType) {
- onUpdateEditorToolType(toolType);
+ updateEditorToolTypeInternal(toolType);
}
/**
@@ -1249,6 +1253,14 @@
rootView.setSystemGestureExclusionRects(exclusionRects);
}
+ private void updateEditorToolTypeInternal(int toolType) {
+ if (Flags.useHandwritingListenerForTooltype()) {
+ mLastUsedToolType = toolType;
+ mInputEditorInfo.setInitialToolType(toolType);
+ }
+ onUpdateEditorToolType(toolType);
+ }
+
/**
* Concrete implementation of
* {@link AbstractInputMethodService.AbstractInputMethodSessionImpl} that provides
@@ -3110,6 +3122,9 @@
null /* icProto */);
mInputStarted = true;
mStartedInputConnection = ic;
+ if (Flags.useHandwritingListenerForTooltype()) {
+ editorInfo.setInitialToolType(mLastUsedToolType);
+ }
mInputEditorInfo = editorInfo;
initialize();
mInlineSuggestionSessionController.notifyOnStartInput(
@@ -3354,6 +3369,10 @@
* had not seen the event at all.
*/
public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (Flags.useHandwritingListenerForTooltype()) {
+ // any KeyEvent keyDown should reset last toolType.
+ updateEditorToolTypeInternal(MotionEvent.TOOL_TYPE_UNKNOWN);
+ }
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
final ExtractEditText eet = getExtractEditTextIfVisible();
if (eet != null && eet.handleBackInTextActionModeIfNeeded(event)) {
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index 8fcff78..3149de4 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -673,6 +673,7 @@
if (anglePkg.isEmpty()) {
return;
}
+ intent.setPackage(anglePkg);
context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
@Override
diff --git a/core/java/android/os/HwNoService.java b/core/java/android/os/HwNoService.java
index 117c3ad..0840314 100644
--- a/core/java/android/os/HwNoService.java
+++ b/core/java/android/os/HwNoService.java
@@ -16,37 +16,127 @@
package android.os;
+import android.hidl.manager.V1_2.IServiceManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+
/**
* A fake hwservicemanager that is used locally when HIDL isn't supported on the device.
*
* @hide
*/
-final class HwNoService implements IHwBinder, IHwInterface {
- /** @hide */
- @Override
- public void transact(int code, HwParcel request, HwParcel reply, int flags) {}
+final class HwNoService extends IServiceManager.Stub implements IHwBinder, IHwInterface {
+ private static final String TAG = "HwNoService";
/** @hide */
@Override
- public IHwInterface queryLocalInterface(String descriptor) {
- return new HwNoService();
+ public String toString() {
+ return "[HwNoService]";
}
- /** @hide */
@Override
- public boolean linkToDeath(DeathRecipient recipient, long cookie) {
+ public android.hidl.base.V1_0.IBase get(String fqName, String name)
+ throws android.os.RemoteException {
+ Log.i(TAG, "get " + fqName + "/" + name + " with no hwservicemanager");
+ return null;
+ }
+
+ @Override
+ public boolean add(String name, android.hidl.base.V1_0.IBase service)
+ throws android.os.RemoteException {
+ Log.i(TAG, "get " + name + " with no hwservicemanager");
+ return false;
+ }
+
+ @Override
+ public byte getTransport(String fqName, String name) throws android.os.RemoteException {
+ Log.i(TAG, "getTransoport " + fqName + "/" + name + " with no hwservicemanager");
+ return 0x0;
+ }
+
+ @Override
+ public java.util.ArrayList<String> list() throws android.os.RemoteException {
+ Log.i(TAG, "list with no hwservicemanager");
+ return new ArrayList<String>();
+ }
+
+ @Override
+ public java.util.ArrayList<String> listByInterface(String fqName)
+ throws android.os.RemoteException {
+ Log.i(TAG, "listByInterface with no hwservicemanager");
+ return new ArrayList<String>();
+ }
+
+ @Override
+ public boolean registerForNotifications(
+ String fqName, String name, android.hidl.manager.V1_0.IServiceNotification callback)
+ throws android.os.RemoteException {
+ Log.i(TAG, "registerForNotifications with no hwservicemanager");
return true;
}
- /** @hide */
@Override
- public boolean unlinkToDeath(DeathRecipient recipient) {
+ public ArrayList<android.hidl.manager.V1_0.IServiceManager.InstanceDebugInfo> debugDump()
+ throws android.os.RemoteException {
+ Log.i(TAG, "debugDump with no hwservicemanager");
+ return new ArrayList<android.hidl.manager.V1_0.IServiceManager.InstanceDebugInfo>();
+ }
+
+ @Override
+ public void registerPassthroughClient(String fqName, String name)
+ throws android.os.RemoteException {
+ Log.i(TAG, "registerPassthroughClient with no hwservicemanager");
+ }
+
+ @Override
+ public boolean unregisterForNotifications(
+ String fqName, String name, android.hidl.manager.V1_0.IServiceNotification callback)
+ throws android.os.RemoteException {
+ Log.i(TAG, "unregisterForNotifications with no hwservicemanager");
return true;
}
- /** @hide */
@Override
- public IHwBinder asBinder() {
- return this;
+ public boolean registerClientCallback(
+ String fqName,
+ String name,
+ android.hidl.base.V1_0.IBase server,
+ android.hidl.manager.V1_2.IClientCallback cb)
+ throws android.os.RemoteException {
+ Log.i(
+ TAG,
+ "registerClientCallback for " + fqName + "/" + name + " with no hwservicemanager");
+ return true;
+ }
+
+ @Override
+ public boolean unregisterClientCallback(
+ android.hidl.base.V1_0.IBase server, android.hidl.manager.V1_2.IClientCallback cb)
+ throws android.os.RemoteException {
+ Log.i(TAG, "unregisterClientCallback with no hwservicemanager");
+ return true;
+ }
+
+ @Override
+ public boolean addWithChain(
+ String name, android.hidl.base.V1_0.IBase service, java.util.ArrayList<String> chain)
+ throws android.os.RemoteException {
+ Log.i(TAG, "addWithChain with no hwservicemanager");
+ return true;
+ }
+
+ @Override
+ public java.util.ArrayList<String> listManifestByInterface(String fqName)
+ throws android.os.RemoteException {
+ Log.i(TAG, "listManifestByInterface for " + fqName + " with no hwservicemanager");
+ return new ArrayList<String>();
+ }
+
+ @Override
+ public boolean tryUnregister(String fqName, String name, android.hidl.base.V1_0.IBase service)
+ throws android.os.RemoteException {
+ Log.i(TAG, "tryUnregister for " + fqName + "/" + name + " with no hwservicemanager");
+ return true;
}
}
diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java
index e32a8f3..8c8af0e 100644
--- a/core/java/android/os/StrictMode.java
+++ b/core/java/android/os/StrictMode.java
@@ -2373,13 +2373,29 @@
/** Assume locked until we hear otherwise */
private static volatile boolean sCeStorageUnlocked = false;
+ /**
+ * Avoid (potentially) costly and repeated lookups to the same mount service.
+ * Note that we don't use the Singleton wrapper as lookup may fail early during boot.
+ */
+ private static volatile IStorageManager sStorageManager;
+
private static boolean isCeStorageUnlocked(int userId) {
- final IStorageManager storage = IStorageManager.Stub
+ IStorageManager storage = sStorageManager;
+ if (storage == null) {
+ storage = IStorageManager.Stub
.asInterface(ServiceManager.getService("mount"));
+ // As the queried handle may be null early during boot, only stash valid handles,
+ // avoiding races with concurrent service queries.
+ if (storage != null) {
+ sStorageManager = storage;
+ }
+ }
if (storage != null) {
try {
return storage.isCeStorageUnlocked(userId);
} catch (RemoteException ignored) {
+ // Conservatively clear the ref, allowing refresh if the remote process restarts.
+ sStorageManager = null;
}
}
return false;
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 54116a2..692dad4 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -26,6 +26,8 @@
import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static com.android.window.flags.Flags.noConsecutiveVisibilityEvents;
+
import android.animation.AnimationHandler;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -1431,27 +1433,36 @@
}
if (didSurface && !mReportedVisible) {
- // This wallpaper is currently invisible, but its
- // surface has changed. At this point let's tell it
- // again that it is invisible in case the report about
- // the surface caused it to start running. We really
- // don't want wallpapers running when not visible.
if (mIsCreating) {
- // Some wallpapers will ignore this call if they
- // had previously been told they were invisble,
- // so if we are creating a new surface then toggle
- // the state to get them to notice.
- if (DEBUG) Log.v(TAG, "onVisibilityChanged(true) at surface: "
- + this);
- Trace.beginSection("WPMS.Engine.onVisibilityChanged-true");
- onVisibilityChanged(true);
+ // The surface has been created, but the wallpaper isn't visible.
+ // Trigger onVisibilityChanged(true) then onVisibilityChanged(false)
+ // to make sure the wallpaper is stopped even after the events
+ // onSurfaceCreated() and onSurfaceChanged().
+ if (noConsecutiveVisibilityEvents()) {
+ if (DEBUG) Log.v(TAG, "toggling doVisibilityChanged");
+ Trace.beginSection("WPMS.Engine.doVisibilityChanged-true");
+ doVisibilityChanged(true);
+ Trace.endSection();
+ Trace.beginSection("WPMS.Engine.doVisibilityChanged-false");
+ doVisibilityChanged(false);
+ Trace.endSection();
+ } else {
+ if (DEBUG) {
+ Log.v(TAG, "onVisibilityChanged(true) at surface: " + this);
+ }
+ Trace.beginSection("WPMS.Engine.onVisibilityChanged-true");
+ onVisibilityChanged(true);
+ Trace.endSection();
+ }
+ }
+ if (!noConsecutiveVisibilityEvents()) {
+ if (DEBUG) {
+ Log.v(TAG, "onVisibilityChanged(false) at surface: " + this);
+ }
+ Trace.beginSection("WPMS.Engine.onVisibilityChanged-false");
+ onVisibilityChanged(false);
Trace.endSection();
}
- if (DEBUG) Log.v(TAG, "onVisibilityChanged(false) at surface: "
- + this);
- Trace.beginSection("WPMS.Engine.onVisibilityChanged-false");
- onVisibilityChanged(false);
- Trace.endSection();
}
} finally {
mIsCreating = false;
diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java
index ad0bf7c..7850554 100644
--- a/core/java/android/view/Surface.java
+++ b/core/java/android/view/Surface.java
@@ -274,7 +274,8 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"FRAME_RATE_CATEGORY_"},
value = {FRAME_RATE_CATEGORY_DEFAULT, FRAME_RATE_CATEGORY_NO_PREFERENCE,
- FRAME_RATE_CATEGORY_LOW, FRAME_RATE_CATEGORY_NORMAL, FRAME_RATE_CATEGORY_HIGH})
+ FRAME_RATE_CATEGORY_LOW, FRAME_RATE_CATEGORY_NORMAL,
+ FRAME_RATE_CATEGORY_HIGH_HINT, FRAME_RATE_CATEGORY_HIGH})
public @interface FrameRateCategory {}
// From native_window.h or window.h. Keep these in sync.
@@ -308,11 +309,21 @@
public static final int FRAME_RATE_CATEGORY_NORMAL = 3;
/**
+ * Hints that, as a result of a user interaction, an animation is likely to start.
+ * This category is a signal that a user interaction heuristic determined the need of a
+ * high refresh rate, and is not an explicit request from the app.
+ * As opposed to {@link #FRAME_RATE_CATEGORY_HIGH}, this vote may be ignored in favor of
+ * more explicit votes.
+ * @hide
+ */
+ public static final int FRAME_RATE_CATEGORY_HIGH_HINT = 4;
+
+ /**
* Indicates a frame rate suitable for animations that require a high frame rate, which may
* increase smoothness but may also increase power usage.
* @hide
*/
- public static final int FRAME_RATE_CATEGORY_HIGH = 4;
+ public static final int FRAME_RATE_CATEGORY_HIGH = 5;
/**
* Create an empty surface, which will later be filled in by readFromParcel().
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 442ea66..75be729 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -32,6 +32,7 @@
import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE;
import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
+import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout;
import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision;
import static android.view.flags.Flags.toolkitSetFrameRateReadOnly;
import static android.view.flags.Flags.viewVelocityApi;
@@ -955,6 +956,12 @@
private static boolean sAlwaysRemeasureExactly = false;
/**
+ * When true makes it possible to use onMeasure caches also when the force layout flag is
+ * enabled. This helps avoiding multiple measures in the same frame with the same dimensions.
+ */
+ private static boolean sUseMeasureCacheDuringForceLayoutFlagValue;
+
+ /**
* Allow setForeground/setBackground to be called (and ignored) on a textureview,
* without throwing
*/
@@ -2396,6 +2403,7 @@
sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly();
sToolkitMetricsForFrameRateDecisionFlagValue = toolkitMetricsForFrameRateDecision();
+ sUseMeasureCacheDuringForceLayoutFlagValue = enableUseMeasureCacheDuringForceLayout();
}
/**
@@ -27417,7 +27425,13 @@
resolveRtlPropertiesIfNeeded();
- int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
+ int cacheIndex;
+ if (sUseMeasureCacheDuringForceLayoutFlagValue) {
+ cacheIndex = mMeasureCache.indexOfKey(key);
+ } else {
+ cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
+ }
+
if (cacheIndex < 0 || sIgnoreMeasureCache) {
if (isTraversalTracingEnabled()) {
Trace.beginSection(mTracingStrings.onMeasure);
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 287c7b2..fbefbf3 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -49,6 +49,7 @@
import android.os.Bundle;
import android.os.Parcelable;
import android.os.SystemClock;
+import android.service.autofill.Flags;
import android.util.AttributeSet;
import android.util.IntArray;
import android.util.Log;
@@ -3752,7 +3753,16 @@
&& !child.isActivityDeniedForAutofillForUnimportantView())
|| (shouldIncludeAllChildrenViewWithAutofillTypeNotNone(afm)
&& child.getAutofillType() != AUTOFILL_TYPE_NONE)
- || shouldIncludeAllChildrenViews(afm)){
+ || shouldIncludeAllChildrenViews(afm)
+ || (Flags.includeInvisibleViewGroupInAssistStructure()
+ && child instanceof ViewGroup && child.getVisibility() != View.VISIBLE)) {
+ // If the child is a ViewGroup object and its visibility is not visible, include
+ // it as part of the assist structure. The children of these invisible ViewGroup
+ // objects are parsed and included in the assist structure. When the Autofill
+ // Provider determines the visibility of these children, it looks at their
+ // visibility as well as their parent's visibility. Omitting invisible parents
+ // will lead to the Autofill Provider incorrectly assuming that these children
+ // of invisible parents are actually visible.
list.add(child);
} else if (child instanceof ViewGroup) {
((ViewGroup) child).populateChildrenForAutofill(list, flags);
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
new file mode 100644
index 0000000..a74b06a
--- /dev/null
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -0,0 +1,10 @@
+package: "android.view.flags"
+
+flag {
+ name: "enable_use_measure_cache_during_force_layout"
+ namespace: "toolkit"
+ description: "Enables using the measure cache during a view force layout from the second "
+ "onMeasure call onwards during the same traversal."
+ bug: "316170253"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index dc6aa6c..bb7677d6 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -38,3 +38,12 @@
description: "Feature flag for supporting stylus handwriting delegation from RemoteViews on the home screen"
bug: "279959705"
}
+
+flag {
+ name: "use_handwriting_listener_for_tooltype"
+ namespace: "input_method"
+ description: "Feature flag for using handwriting spy for determining pointer toolType."
+ bug: "309554999"
+ is_fixed_read_only: true
+}
+
diff --git a/core/java/android/window/flags/wallpaper_manager.aconfig b/core/java/android/window/flags/wallpaper_manager.aconfig
index f03c993..ea9da96 100644
--- a/core/java/android/window/flags/wallpaper_manager.aconfig
+++ b/core/java/android/window/flags/wallpaper_manager.aconfig
@@ -13,3 +13,10 @@
description: "Support storing different wallpaper crops for different display dimensions. Only effective after rebooting."
bug: "281648899"
}
+
+flag {
+ name: "no_consecutive_visibility_events"
+ namespace: "systemui"
+ description: "Prevent the system from sending consecutive onVisibilityChanged(false) events."
+ bug: "285631818"
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/util/function/pooled/PooledLambda.java b/core/java/com/android/internal/util/function/pooled/PooledLambda.java
index bdc8a66..1d6d69c 100755
--- a/core/java/com/android/internal/util/function/pooled/PooledLambda.java
+++ b/core/java/com/android/internal/util/function/pooled/PooledLambda.java
@@ -1009,7 +1009,7 @@
K arg11, L arg12) {
synchronized (Message.sPoolSync) {
PooledRunnable callback = acquire(PooledLambdaImpl.sMessageCallbacksPool,
- function, 11, 0, ReturnType.VOID, arg1, arg2, arg3, arg4, arg5, arg6, arg7,
+ function, 12, 0, ReturnType.VOID, arg1, arg2, arg3, arg4, arg5, arg6, arg7,
arg8, arg9, arg10, arg11, arg12);
return Message.obtain().setCallback(callback.recycleOnUse());
}
diff --git a/core/jni/android_os_HwBinder.cpp b/core/jni/android_os_HwBinder.cpp
index 781895e..477bd09 100644
--- a/core/jni/android_os_HwBinder.cpp
+++ b/core/jni/android_os_HwBinder.cpp
@@ -258,14 +258,59 @@
JHwBinder::SetNativeContext(env, thiz, context);
}
-static void JHwBinder_native_transact(
- JNIEnv * /* env */,
- jobject /* thiz */,
- jint /* code */,
- jobject /* requestObj */,
- jobject /* replyObj */,
- jint /* flags */) {
- CHECK(!"Should not be here");
+static void JHwBinder_native_transact(JNIEnv *env, jobject thiz, jint code, jobject requestObj,
+ jobject replyObj, jint flags) {
+ if (requestObj == NULL) {
+ jniThrowException(env, "java/lang/NullPointerException", NULL);
+ return;
+ }
+ sp<hardware::IBinder> binder = JHwBinder::GetNativeBinder(env, thiz);
+ sp<android::hidl::base::V1_0::IBase> base = new android::hidl::base::V1_0::BpHwBase(binder);
+ hidl_string desc;
+ auto ret = base->interfaceDescriptor(
+ [&desc](const hidl_string &descriptor) { desc = descriptor; });
+ ret.assertOk();
+ // Only the fake hwservicemanager is allowed to be used locally like this.
+ if (desc != "android.hidl.manager@1.2::IServiceManager" &&
+ desc != "android.hidl.manager@1.1::IServiceManager" &&
+ desc != "android.hidl.manager@1.0::IServiceManager") {
+ LOG(FATAL) << "Local binders are not supported!";
+ }
+ if (replyObj == nullptr) {
+ LOG(FATAL) << "Unexpected null replyObj. code: " << code;
+ return;
+ }
+ const hardware::Parcel *request = JHwParcel::GetNativeContext(env, requestObj)->getParcel();
+ sp<JHwParcel> replyContext = JHwParcel::GetNativeContext(env, replyObj);
+ hardware::Parcel *reply = replyContext->getParcel();
+
+ request->setDataPosition(0);
+
+ bool isOneway = (flags & IBinder::FLAG_ONEWAY) != 0;
+ if (!isOneway) {
+ replyContext->setTransactCallback([](auto &replyParcel) {});
+ }
+
+ env->CallVoidMethod(thiz, gFields.onTransactID, code, requestObj, replyObj, flags);
+
+ if (env->ExceptionCheck()) {
+ jthrowable excep = env->ExceptionOccurred();
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+
+ binder_report_exception(env, excep, "Uncaught error or exception in hwbinder!");
+
+ env->DeleteLocalRef(excep);
+ }
+
+ if (!isOneway) {
+ if (!replyContext->wasSent()) {
+ // The implementation never finished the transaction.
+ LOG(ERROR) << "The reply failed to send!";
+ }
+ }
+
+ reply->setDataPosition(0);
}
static void JHwBinder_native_registerService(
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 232a36f..e65bfab 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7692,6 +7692,13 @@
<permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT"
android:protectionLevel="signature" />
+ <!-- Allows low-level access to monitor sticky modifier state changes when A11Y Sticky keys
+ feature is enabled.
+ <p>Not for use by third-party applications.
+ @hide -->
+ <permission android:name="android.permission.MONITOR_STICKY_MODIFIER_STATE"
+ android:protectionLevel="signature" />
+
<uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" />
<!-- Allows financed device kiosk apps to perform actions on the Device Lock service
diff --git a/core/tests/coretests/src/android/content/pm/ModuleInfoTest.java b/core/tests/coretests/src/android/content/pm/ModuleInfoTest.java
new file mode 100644
index 0000000..4366e02c
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/ModuleInfoTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import android.platform.test.annotations.AppModeFull;
+import android.text.TextUtils;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull
+public class ModuleInfoTest {
+
+ private static final String APEX_MODULE_NAME = "apexModuleName";
+ private static final String APK_IN_APEX_PACKAGE_NAME = "apkInApexPackageName";
+ private static final String MODULE_PACKAGE_NAME = "modulePackageName";
+ private static final String MODULE_NAME = "moduleName";
+
+ @Test
+ public void testSimple() {
+ ModuleInfo info = new ModuleInfo();
+ assertThat(info.toString()).isNotNull();
+ }
+
+ @Test
+ public void testDefaultCopy() {
+ ModuleInfo oldInfo = new ModuleInfo();
+ ModuleInfo newInfo = new ModuleInfo(oldInfo);
+ assertThat(newInfo).isEqualTo(oldInfo);
+ }
+
+ @Test
+ public void testCopy() {
+ boolean isHidden = false;
+ ModuleInfo info = new ModuleInfo();
+ info.setHidden(isHidden);
+ info.setApexModuleName(APEX_MODULE_NAME);
+ info.setPackageName(MODULE_PACKAGE_NAME);
+ info.setName(MODULE_NAME);
+ info.setApkInApexPackageNames(List.of(APK_IN_APEX_PACKAGE_NAME));
+
+ ModuleInfo newInfo = new ModuleInfo(info);
+ assertThat(newInfo).isEqualTo(info);
+ }
+
+ @Test
+ public void testGetApkInApexPackageNamesReturnEmptyListInDefault() {
+ ModuleInfo info = new ModuleInfo();
+ assertThat(info.getApkInApexPackageNames()).isNotNull();
+ assertThat(info.getApkInApexPackageNames()).isEmpty();
+ }
+
+ @Test
+ public void testModuleInfoParcelizeDeparcelize() {
+ boolean isHidden = false;
+ ModuleInfo info = new ModuleInfo();
+ info.setHidden(isHidden);
+ info.setApexModuleName(APEX_MODULE_NAME);
+ info.setPackageName(MODULE_PACKAGE_NAME);
+ info.setName(MODULE_NAME);
+ info.setApkInApexPackageNames(List.of(APK_IN_APEX_PACKAGE_NAME));
+
+ final Parcel p = Parcel.obtain();
+ info.writeToParcel(p, 0);
+ p.setDataPosition(0);
+
+ final ModuleInfo targetInfo = ModuleInfo.CREATOR.createFromParcel(p);
+ p.recycle();
+
+ assertThat(info.isHidden()).isEqualTo(targetInfo.isHidden());
+ assertThat(info.getApexModuleName()).isEqualTo(targetInfo.getApexModuleName());
+ assertThat(info.getPackageName()).isEqualTo(targetInfo.getPackageName());
+ assertThat(TextUtils.equals(info.getName(), targetInfo.getName())).isTrue();
+ assertThat(info.getApkInApexPackageNames().toArray()).isEqualTo(
+ targetInfo.getApkInApexPackageNames().toArray());
+ }
+}
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
index cec7ee2..ef7478c 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
@@ -18,13 +18,13 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/desktop_mode_caption"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
<ImageButton
android:id="@+id/caption_handle"
- android:layout_width="128dp"
+ android:layout_width="@dimen/desktop_mode_fullscreen_decor_caption_width"
android:layout_height="@dimen/desktop_mode_fullscreen_decor_caption_height"
android:paddingVertical="16dp"
android:contentDescription="@string/handle_text"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 0a40cea..28e7098 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -413,6 +413,9 @@
<!-- Height of desktop mode caption for fullscreen tasks. -->
<dimen name="desktop_mode_fullscreen_decor_caption_height">36dp</dimen>
+ <!-- Width of desktop mode caption for fullscreen tasks. -->
+ <dimen name="desktop_mode_fullscreen_decor_caption_width">128dp</dimen>
+
<!-- Required empty space to be visible for partially offscreen tasks. -->
<dimen name="freeform_required_visible_empty_space_in_header">48dp</dimen>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 48a0a46..3b0e7c1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -19,6 +19,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_PIP;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
@@ -54,6 +55,8 @@
@Nullable
private WindowContainerToken mPipTaskToken;
@Nullable
+ private IBinder mEnterTransition;
+ @Nullable
private IBinder mAutoEnterButtonNavTransition;
@Nullable
private IBinder mExitViaExpandTransition;
@@ -98,11 +101,8 @@
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
- if (isAutoEnterInButtonNavigation(request)) {
- mAutoEnterButtonNavTransition = transition;
- return getEnterPipTransaction(transition, request);
- } else if (isLegacyEnter(request)) {
- mLegacyEnterTransition = transition;
+ if (isAutoEnterInButtonNavigation(request) || isEnterPictureInPictureModeRequest(request)) {
+ mEnterTransition = transition;
return getEnterPipTransaction(transition, request);
}
return null;
@@ -111,12 +111,9 @@
@Override
public void augmentRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request,
@NonNull WindowContainerTransaction outWct) {
- if (isAutoEnterInButtonNavigation(request)) {
+ if (isAutoEnterInButtonNavigation(request) || isEnterPictureInPictureModeRequest(request)) {
outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */);
- mAutoEnterButtonNavTransition = transition;
- } else if (isLegacyEnter(request)) {
- outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */);
- mLegacyEnterTransition = transition;
+ mEnterTransition = transition;
}
}
@@ -162,7 +159,7 @@
&& pipTask.pictureInPictureParams.isAutoEnterEnabled();
}
- private boolean isLegacyEnter(@NonNull TransitionRequestInfo requestInfo) {
+ private boolean isEnterPictureInPictureModeRequest(@NonNull TransitionRequestInfo requestInfo) {
return requestInfo.getType() == TRANSIT_PIP;
}
@@ -172,13 +169,15 @@
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- if (transition == mAutoEnterButtonNavTransition) {
- mAutoEnterButtonNavTransition = null;
- return startAutoEnterButtonNavAnimation(info, startTransaction, finishTransaction,
- finishCallback);
- } else if (transition == mLegacyEnterTransition) {
- mLegacyEnterTransition = null;
- return startLegacyEnterAnimation(info, startTransaction, finishTransaction,
+ if (transition == mEnterTransition) {
+ mEnterTransition = null;
+ if (isLegacyEnter(info)) {
+ // If this is a legacy-enter-pip (auto-enter is off and PiP activity went to pause),
+ // then we should run an ALPHA type (cross-fade) animation.
+ return startAlphaTypeEnterAnimation(info, startTransaction, finishTransaction,
+ finishCallback);
+ }
+ return startBoundsTypeEnterAnimation(info, startTransaction, finishTransaction,
finishCallback);
} else if (transition == mExitViaExpandTransition) {
mExitViaExpandTransition = null;
@@ -187,7 +186,15 @@
return false;
}
- private boolean startAutoEnterButtonNavAnimation(@NonNull TransitionInfo info,
+ private boolean isLegacyEnter(@NonNull TransitionInfo info) {
+ TransitionInfo.Change pipChange = getPipChange(info);
+ // If the only change in the changes list is a TO_FRONT mode PiP task,
+ // then this is legacy-enter PiP.
+ return pipChange != null && pipChange.getMode() == TRANSIT_TO_FRONT
+ && info.getChanges().size() == 1;
+ }
+
+ private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
@@ -205,7 +212,7 @@
return true;
}
- private boolean startLegacyEnterAnimation(@NonNull TransitionInfo info,
+ private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
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 4fd3625..61a8e9b 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
@@ -726,7 +726,7 @@
private void handleEventOutsideFocusedCaption(MotionEvent ev,
DesktopModeWindowDecoration relevantDecor) {
// Returns if event occurs within caption
- if (relevantDecor == null || relevantDecor.checkTouchEventInCaption(ev)) {
+ if (relevantDecor == null || relevantDecor.checkTouchEventInCaptionHandle(ev)) {
return;
}
@@ -761,7 +761,8 @@
|| windowingMode == WINDOWING_MODE_MULTI_WINDOW;
}
- if (dragFromStatusBarAllowed && relevantDecor.checkTouchEventInHandle(ev)) {
+ if (dragFromStatusBarAllowed
+ && relevantDecor.checkTouchEventInCaptionHandle(ev)) {
mTransitionDragActive = true;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 0c8e93b..d08b655 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -317,6 +317,7 @@
relayoutParams.mLayoutResId =
getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
+ relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId);
if (DesktopModeStatus.useWindowShadow(/* isFocusedWindow= */ taskInfo.isFocused)) {
relayoutParams.mShadowRadiusId = taskInfo.isFocused
? R.dimen.freeform_decor_shadow_focused_thickness
@@ -345,6 +346,17 @@
}
}
+ /**
+ * If task has focused window decor, return the caption id of the fullscreen caption size
+ * resource. Otherwise, return ID_NULL and caption width be set to task width.
+ */
+ private static int getCaptionWidthId(int layoutResId) {
+ if (layoutResId == R.layout.desktop_mode_focused_window_decor) {
+ return R.dimen.desktop_mode_fullscreen_decor_caption_width;
+ }
+ return Resources.ID_NULL;
+ }
+
private PointF calculateMaximizeMenuPosition() {
final PointF position = new PointF();
@@ -558,7 +570,6 @@
.setOnClickListener(mOnCaptionButtonClickListener)
.setOnTouchListener(mOnCaptionTouchListener)
.setLayoutId(mRelayoutParams.mLayoutResId)
- .setCaptionPosition(mRelayoutParams.mCaptionX, mRelayoutParams.mCaptionY)
.setWindowingButtonsVisible(DesktopModeStatus.isEnabled())
.setCaptionHeight(mResult.mCaptionHeight)
.build();
@@ -635,35 +646,25 @@
mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId);
if (taskInfo == null) return result;
final Point positionInParent = taskInfo.positionInParent;
- result.offset(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY);
result.offset(-positionInParent.x, -positionInParent.y);
return result;
}
/**
- * Determine if a passed MotionEvent is in a view in caption
+ * Checks if motion event occurs in the caption handle area. This should be used in cases where
+ * onTouchListener will not work (i.e. when caption is in status bar area).
*
* @param ev the {@link MotionEvent} to check
- * @param layoutId the id of the view
* @return {@code true} if event is inside the specified view, {@code false} if not
*/
- private boolean checkEventInCaptionView(MotionEvent ev, int layoutId) {
- if (mResult.mRootView == null) return false;
+ boolean checkTouchEventInCaptionHandle(MotionEvent ev) {
+ if (isHandleMenuActive() || !(mWindowDecorViewHolder
+ instanceof DesktopModeFocusedWindowDecorationViewHolder)) {
+ return false;
+ }
final PointF inputPoint = offsetCaptionLocation(ev);
- final View view = mResult.mRootView.findViewById(layoutId);
- return view != null && pointInView(view, inputPoint.x, inputPoint.y);
- }
-
- boolean checkTouchEventInHandle(MotionEvent ev) {
- if (isHandleMenuActive()) return false;
- return checkEventInCaptionView(ev, R.id.caption_handle);
- }
-
- /**
- * Returns true if motion event is within the caption's root view's bounds.
- */
- boolean checkTouchEventInCaption(MotionEvent ev) {
- return checkEventInCaptionView(ev, getCaptionViewId());
+ return ((DesktopModeFocusedWindowDecorationViewHolder) mWindowDecorViewHolder)
+ .pointInCaption(inputPoint, mResult.mCaptionX);
}
/**
@@ -676,24 +677,19 @@
void checkClickEvent(MotionEvent ev) {
if (mResult.mRootView == null) return;
if (!isHandleMenuActive()) {
+ // Click if point in caption handle view
final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
final View handle = caption.findViewById(R.id.caption_handle);
- clickIfPointInView(new PointF(ev.getX(), ev.getY()), handle);
+ if (checkTouchEventInCaptionHandle(ev)) {
+ mOnCaptionButtonClickListener.onClick(handle);
+ }
} else {
mHandleMenu.checkClickEvent(ev);
closeHandleMenuIfNeeded(ev);
}
}
- private boolean clickIfPointInView(PointF inputPoint, View v) {
- if (pointInView(v, inputPoint.x, inputPoint.y)) {
- mOnCaptionButtonClickListener.onClick(v);
- return true;
- }
- return false;
- }
-
- boolean pointInView(View v, float x, float y) {
+ private boolean pointInView(View v, float x, float y) {
return v != null && v.getLeft() <= x && v.getRight() >= x
&& v.getTop() <= y && v.getBottom() >= y;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
index 652a2ed..b37dd0d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
@@ -64,8 +64,6 @@
private final View.OnTouchListener mOnTouchListener;
private final RunningTaskInfo mTaskInfo;
private final int mLayoutResId;
- private final int mCaptionX;
- private final int mCaptionY;
private int mMarginMenuTop;
private int mMarginMenuStart;
private int mMenuHeight;
@@ -74,16 +72,13 @@
private HandleMenuAnimator mHandleMenuAnimator;
- HandleMenu(WindowDecoration parentDecor, int layoutResId, int captionX, int captionY,
- View.OnClickListener onClickListener, View.OnTouchListener onTouchListener,
- Bitmap appIcon, CharSequence appName, boolean shouldShowWindowingPill,
- int captionHeight) {
+ HandleMenu(WindowDecoration parentDecor, int layoutResId, View.OnClickListener onClickListener,
+ View.OnTouchListener onTouchListener, Bitmap appIcon, CharSequence appName,
+ boolean shouldShowWindowingPill, int captionHeight) {
mParentDecor = parentDecor;
mContext = mParentDecor.mDecorWindowContext;
mTaskInfo = mParentDecor.mTaskInfo;
mLayoutResId = layoutResId;
- mCaptionX = captionX;
- mCaptionY = captionY;
mOnClickListener = onClickListener;
mOnTouchListener = onTouchListener;
mAppIconBitmap = appIcon;
@@ -225,12 +220,12 @@
if (mLayoutResId
== R.layout.desktop_mode_app_controls_window_decor) {
// Align the handle menu to the left of the caption.
- menuX = mCaptionX + mMarginMenuStart;
- menuY = mCaptionY + mMarginMenuTop;
+ menuX = mMarginMenuStart;
+ menuY = mMarginMenuTop;
} else {
// Position the handle menu at the center of the caption.
- menuX = mCaptionX + (captionWidth / 2) - (mMenuWidth / 2);
- menuY = mCaptionY + mMarginMenuStart;
+ menuX = (captionWidth / 2) - (mMenuWidth / 2);
+ menuY = mMarginMenuStart;
}
// Handle Menu position setup.
@@ -346,8 +341,6 @@
private View.OnClickListener mOnClickListener;
private View.OnTouchListener mOnTouchListener;
private int mLayoutId;
- private int mCaptionX;
- private int mCaptionY;
private boolean mShowWindowingPill;
private int mCaptionHeight;
@@ -381,12 +374,6 @@
return this;
}
- Builder setCaptionPosition(int captionX, int captionY) {
- mCaptionX = captionX;
- mCaptionY = captionY;
- return this;
- }
-
Builder setWindowingButtonsVisible(boolean windowingButtonsVisible) {
mShowWindowingPill = windowingButtonsVisible;
return this;
@@ -398,8 +385,8 @@
}
HandleMenu build() {
- return new HandleMenu(mParent, mLayoutId, mCaptionX, mCaptionY, mOnClickListener,
- mOnTouchListener, mAppIcon, mName, mShowWindowingPill, mCaptionHeight);
+ return new HandleMenu(mParent, mLayoutId, mOnClickListener, mOnTouchListener,
+ mAppIcon, mName, mShowWindowingPill, mCaptionHeight);
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index b5373c6..6a9258c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -279,9 +279,12 @@
}
outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
- final int captionWidth = taskBounds.width();
+ final int captionWidth = params.mCaptionWidthId != Resources.ID_NULL
+ ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width();
+ outResult.mCaptionX = (outResult.mWidth - captionWidth) / 2;
startT.setWindowCrop(mCaptionContainerSurface, captionWidth, outResult.mCaptionHeight)
+ .setPosition(mCaptionContainerSurface, outResult.mCaptionX, 0 /* y */)
.setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER)
.show(mCaptionContainerSurface);
@@ -292,7 +295,7 @@
mCaptionInsetsRect.set(taskBounds);
if (mIsCaptionVisible) {
mCaptionInsetsRect.bottom =
- mCaptionInsetsRect.top + outResult.mCaptionHeight + params.mCaptionY;
+ mCaptionInsetsRect.top + outResult.mCaptionHeight;
wct.addInsetsSource(mTaskInfo.token,
mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect);
wct.addInsetsSource(mTaskInfo.token,
@@ -554,9 +557,6 @@
int mCornerRadius;
- int mCaptionX;
- int mCaptionY;
-
Configuration mWindowDecorConfig;
boolean mApplyStartTransactionOnDraw;
@@ -570,9 +570,6 @@
mCornerRadius = 0;
- mCaptionX = 0;
- mCaptionY = 0;
-
mApplyStartTransactionOnDraw = false;
mSetTaskPositionAndCrop = false;
mWindowDecorConfig = null;
@@ -581,6 +578,7 @@
static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
int mCaptionHeight;
+ int mCaptionX;
int mWidth;
int mHeight;
T mRootView;
@@ -589,6 +587,7 @@
mWidth = 0;
mHeight = 0;
mCaptionHeight = 0;
+ mCaptionX = 0;
mRootView = null;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
index 4930cb7..5f77022 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
@@ -5,6 +5,7 @@
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.res.ColorStateList
import android.graphics.Color
+import android.graphics.PointF
import android.view.View
import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
import android.widget.ImageButton
@@ -35,9 +36,6 @@
}
override fun bindData(taskInfo: RunningTaskInfo) {
- taskInfo.taskDescription?.statusBarColor?.let { captionColor ->
- captionView.setBackgroundColor(captionColor)
- }
captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
}
@@ -49,6 +47,17 @@
animateCaptionHandleAlpha(startValue = 0f, endValue = 1f)
}
+ /**
+ * Returns true if input point is in the caption's view.
+ * @param inputPoint the input point relative to the task in full "focus" (i.e. fullscreen).
+ */
+ fun pointInCaption(inputPoint: PointF, captionX: Int): Boolean {
+ return inputPoint.x >= captionX &&
+ inputPoint.x <= captionX + captionView.width &&
+ inputPoint.y >= 0 &&
+ inputPoint.y <= captionView.height
+ }
+
private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int {
return if (shouldUseLightCaptionColors(taskInfo)) {
context.getColor(R.color.desktop_mode_caption_handle_bar_light)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
index 690b4e4..81bc34c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
@@ -17,9 +17,9 @@
*/
abstract fun bindData(taskInfo: RunningTaskInfo)
- /** Callback when the handle menu is opened. */
- abstract fun onHandleMenuOpened()
+ /** Callback when the handle menu is opened. */
+ abstract fun onHandleMenuOpened()
- /** Callback when the handle menu is closed. */
- abstract fun onHandleMenuClosed()
+ /** Callback when the handle menu is closed. */
+ abstract fun onHandleMenuClosed()
}
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt
index 32f1259..143f7a7 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt
@@ -69,7 +69,7 @@
setup {
standardAppHelper.launchViaIntent(
wmHelper,
- NetflixAppHelper.getNetflixWatchVideoIntent("70184207"),
+ NetflixAppHelper.getNetflixWatchVideoIntent("81605060"),
ComponentNameMatcher(NetflixAppHelper.PACKAGE_NAME, NetflixAppHelper.WATCH_ACTIVITY)
)
standardAppHelper.waitForVideoPlaying()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 32a91461..7b53f70 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -772,15 +772,13 @@
private WindowDecoration.AdditionalWindow addTestWindow() {
final Resources resources = mDecorWindowContext.getResources();
- int x = mRelayoutParams.mCaptionX;
- int y = mRelayoutParams.mCaptionY;
int width = loadDimensionPixelSize(resources, mCaptionMenuWidthId);
int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
String name = "Test Window";
WindowDecoration.AdditionalWindow additionalWindow =
addWindow(R.layout.desktop_mode_window_decor_handle_menu, name,
- mMockSurfaceControlAddWindowT, mMockSurfaceSyncGroup, x, y,
- width, height);
+ mMockSurfaceControlAddWindowT, mMockSurfaceSyncGroup, 0 /* x */,
+ 0 /* y */, width, height);
return additionalWindow;
}
}
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 4741170..eebf8aa 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -38,6 +38,7 @@
cc_aconfig_library {
name: "hwui_flags_cc_lib",
+ host_supported: true,
aconfig_declarations: "hwui_flags",
}
@@ -109,12 +110,15 @@
"libbase",
"libharfbuzz_ng",
"libminikin",
+ "server_configurable_flags",
],
static_libs: [
"libui-types",
],
+ whole_static_libs: ["hwui_flags_cc_lib"],
+
target: {
android: {
shared_libs: [
@@ -146,7 +150,6 @@
"libstatspull_lazy",
"libstatssocket_lazy",
"libtonemap",
- "hwui_flags_cc_lib",
],
},
host: {
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index ca11975..c156c46 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -15,6 +15,13 @@
}
flag {
+ name: "high_contrast_text_luminance"
+ namespace: "accessibility"
+ description: "Use luminance to determine how to make text more high contrast, instead of RGB heuristic"
+ bug: "186567103"
+}
+
+flag {
name: "hdr_10bit_plus"
namespace: "core_graphics"
description: "Use 10101010 and FP16 formats for HDR-UI when available"
diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h
index 2e6e976..8f99990 100644
--- a/libs/hwui/hwui/DrawTextFunctor.h
+++ b/libs/hwui/hwui/DrawTextFunctor.h
@@ -16,7 +16,9 @@
#include <SkFontMetrics.h>
#include <SkRRect.h>
+#include <com_android_graphics_hwui_flags.h>
+#include "../utils/Color.h"
#include "Canvas.h"
#include "FeatureFlags.h"
#include "MinikinUtils.h"
@@ -27,6 +29,8 @@
#include "hwui/PaintFilter.h"
#include "pipeline/skia/SkiaRecordingCanvas.h"
+namespace flags = com::android::graphics::hwui::flags;
+
namespace android {
static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkScalar thickness,
@@ -73,8 +77,14 @@
if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) {
// high contrast draw path
int color = paint.getColor();
- int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color);
- bool darken = channelSum < (128 * 3);
+ bool darken;
+ if (flags::high_contrast_text_luminance()) {
+ uirenderer::Lab lab = uirenderer::sRGBToLab(color);
+ darken = lab.L <= 50;
+ } else {
+ int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color);
+ darken = channelSum < (128 * 3);
+ }
// outline
gDrawTextBlobMode = DrawTextBlobMode::HctOutline;
diff --git a/libs/hwui/utils/ForceDark.h b/libs/hwui/utils/ForceDark.h
index 28538c4b..ecfe41f 100644
--- a/libs/hwui/utils/ForceDark.h
+++ b/libs/hwui/utils/ForceDark.h
@@ -17,6 +17,8 @@
#ifndef FORCEDARKUTILS_H
#define FORCEDARKUTILS_H
+#include <stdint.h>
+
namespace android {
namespace uirenderer {
@@ -26,9 +28,9 @@
* This should stay in sync with the java @IntDef in
* frameworks/base/graphics/java/android/graphics/ForceDarkType.java
*/
-enum class ForceDarkType : __uint8_t { NONE = 0, FORCE_DARK = 1, FORCE_INVERT_COLOR_DARK = 2 };
+enum class ForceDarkType : uint8_t { NONE = 0, FORCE_DARK = 1, FORCE_INVERT_COLOR_DARK = 2 };
} /* namespace uirenderer */
} /* namespace android */
-#endif // FORCEDARKUTILS_H
\ No newline at end of file
+#endif // FORCEDARKUTILS_H
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index a78509d..c0d7149 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -53,6 +53,7 @@
isNewActivity: Boolean,
) {
val requestInfo: RequestInfo?
+ var isReqForAllOptions: Boolean = false
private val providerEnabledList: List<ProviderData>
private val providerDisabledList: List<DisabledProviderData>?
val resultReceiver: ResultReceiver?
@@ -102,6 +103,11 @@
ResultReceiver::class.java
)
+ isReqForAllOptions = intent.getBooleanExtra(
+ Constants.EXTRA_REQ_FOR_ALL_OPTIONS,
+ /*defaultValue=*/ false
+ )
+
val cancellationRequest = getCancelUiRequest(intent)
val cancelUiRequestState = cancellationRequest?.let {
CancelUiRequestState(getAppLabel(context.getPackageManager(), it.appPackageName))
@@ -141,7 +147,8 @@
)
}
RequestInfo.TYPE_GET -> {
- val getCredentialInitialUiState = getCredentialInitialUiState(originName)!!
+ val getCredentialInitialUiState = getCredentialInitialUiState(originName,
+ isReqForAllOptions)!!
val autoSelectEntry =
findAutoSelectEntry(getCredentialInitialUiState.providerDisplayInfo)
UiState(
@@ -216,14 +223,18 @@
}
// IMPORTANT: new invocation should be mindful that this method can throw.
- private fun getCredentialInitialUiState(originName: String?): GetCredentialUiState? {
+ private fun getCredentialInitialUiState(
+ originName: String?,
+ isReqForAllOptions: Boolean
+ ): GetCredentialUiState? {
val providerEnabledList = GetFlowUtils.toProviderList(
providerEnabledList as List<GetCredentialProviderData>, context
)
val requestDisplayInfo = GetFlowUtils.toRequestDisplayInfo(requestInfo, context, originName)
return GetCredentialUiState(
- providerEnabledList,
- requestDisplayInfo ?: return null,
+ isReqForAllOptions,
+ providerEnabledList,
+ requestDisplayInfo ?: return null
)
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index 46bebc4..a291f59 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -26,10 +26,12 @@
import com.android.internal.util.Preconditions
data class GetCredentialUiState(
+ val isRequestForAllOptions: Boolean,
val providerInfoList: List<ProviderInfo>,
val requestDisplayInfo: RequestDisplayInfo,
val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
- val currentScreenState: GetScreenState = toGetScreenState(providerDisplayInfo),
+ val currentScreenState: GetScreenState = toGetScreenState(
+ providerDisplayInfo, isRequestForAllOptions),
val activeEntry: EntryInfo? = toActiveEntry(providerDisplayInfo),
val isNoAccount: Boolean = false,
)
@@ -184,7 +186,8 @@
}
private fun toGetScreenState(
- providerDisplayInfo: ProviderDisplayInfo
+ providerDisplayInfo: ProviderDisplayInfo,
+ isRequestForAllOptions: Boolean
): GetScreenState {
return if (providerDisplayInfo.sortedUserNameToCredentialEntryList.isEmpty() &&
providerDisplayInfo.remoteEntry == null &&
@@ -194,6 +197,8 @@
providerDisplayInfo.authenticationEntryList.isEmpty() &&
providerDisplayInfo.remoteEntry != null)
GetScreenState.REMOTE_ONLY
+ else if (isRequestForAllOptions)
+ GetScreenState.ALL_SIGN_IN_OPTIONS
else GetScreenState.PRIMARY_SELECTION
}
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 17c4e02..5a4e0a9 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
@@ -16,6 +16,7 @@
package com.android.systemui.communal.ui.compose
+import android.appwidget.AppWidgetHostView
import android.os.Bundle
import android.util.SizeF
import android.widget.FrameLayout
@@ -376,7 +377,7 @@
AndroidView(
modifier = modifier,
factory = { context ->
- FrameLayout(context).apply { addView(model.remoteViews.apply(context, this)) }
+ AppWidgetHostView(context).apply { updateAppWidget(model.remoteViews) }
},
// For reusing composition in lazy lists.
onReset = {},
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 67a6820..ff53ff2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -37,9 +37,6 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
-/** Set this to `true` to use the LockscreenContent replacement of KeyguardRootView. */
-private val UseLockscreenContent = false
-
/** The lock screen scene shows when the device is locked. */
@SysUISingleton
class LockscreenScene
@@ -48,7 +45,6 @@
@Application private val applicationScope: CoroutineScope,
private val viewModel: LockscreenSceneViewModel,
private val lockscreenContent: Lazy<LockscreenContent>,
- private val viewBasedLockscreenContent: Lazy<ViewBasedLockscreenContent>,
) : ComposableScene {
override val key = SceneKey.Lockscreen
@@ -73,7 +69,6 @@
) {
LockscreenScene(
lockscreenContent = lockscreenContent,
- viewBasedLockscreenContent = viewBasedLockscreenContent,
modifier = modifier,
)
}
@@ -93,22 +88,13 @@
}
@Composable
-private fun SceneScope.LockscreenScene(
+private fun LockscreenScene(
lockscreenContent: Lazy<LockscreenContent>,
- viewBasedLockscreenContent: Lazy<ViewBasedLockscreenContent>,
modifier: Modifier = Modifier,
) {
- if (UseLockscreenContent) {
- lockscreenContent
- .get()
- .Content(
- modifier = modifier.fillMaxSize(),
- )
- } else {
- with(viewBasedLockscreenContent.get()) {
- Content(
- modifier = modifier.fillMaxSize(),
- )
- }
- }
+ lockscreenContent
+ .get()
+ .Content(
+ modifier = modifier.fillMaxSize(),
+ )
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
index 9abb50c..3677cab 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
@@ -20,6 +20,7 @@
import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule
import com.android.systemui.keyguard.ui.composable.blueprint.ShortcutsBesideUdfpsBlueprintModule
import com.android.systemui.keyguard.ui.composable.blueprint.SplitShadeBlueprintModule
+import com.android.systemui.keyguard.ui.composable.section.OptionalSectionModule
import dagger.Module
@Module(
@@ -27,6 +28,7 @@
[
CommunalBlueprintModule::class,
DefaultBlueprintModule::class,
+ OptionalSectionModule::class,
ShortcutsBesideUdfpsBlueprintModule::class,
SplitShadeBlueprintModule::class,
],
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt
deleted file mode 100644
index 8119d2a..0000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/ViewBasedLockscreenContent.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.composable
-
-import android.graphics.Rect
-import android.view.View
-import android.view.ViewGroup
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.toComposeRect
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.onPlaced
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.core.view.isVisible
-import com.android.compose.animation.scene.SceneScope
-import com.android.systemui.keyguard.qualifiers.KeyguardRootView
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
-import com.android.systemui.notifications.ui.composable.NotificationStack
-import com.android.systemui.res.R
-import javax.inject.Inject
-
-/**
- * Renders the content of the lockscreen.
- *
- * This is different from [LockscreenContent] (which is pure compose) and uses a view-based
- * implementation of the lockscreen scene content that relies on [KeyguardRootView].
- *
- * TODO(b/316211368): remove this once [LockscreenContent] is feature complete.
- */
-class ViewBasedLockscreenContent
-@Inject
-constructor(
- private val lockscreenSceneViewModel: LockscreenSceneViewModel,
- @KeyguardRootView private val viewProvider: () -> @JvmSuppressWildcards View,
- private val keyguardRootViewModel: KeyguardRootViewModel,
-) {
- @Composable
- fun SceneScope.Content(
- modifier: Modifier = Modifier,
- ) {
- fun findSettingsMenu(): View {
- return viewProvider().requireViewById(R.id.keyguard_settings_button)
- }
-
- LockscreenLongPress(
- viewModel = lockscreenSceneViewModel.longPress,
- modifier = modifier,
- ) { onSettingsMenuPlaced ->
- AndroidView(
- factory = { _ ->
- val keyguardRootView = viewProvider()
- // Remove the KeyguardRootView from any parent it might already have in legacy
- // code just in case (a view can't have two parents).
- (keyguardRootView.parent as? ViewGroup)?.removeView(keyguardRootView)
- keyguardRootView
- },
- modifier = Modifier.fillMaxSize(),
- )
-
- val notificationStackPosition by
- keyguardRootViewModel.notificationBounds.collectAsState()
-
- Layout(
- modifier =
- Modifier.fillMaxSize().onPlaced {
- val settingsMenuView = findSettingsMenu()
- onSettingsMenuPlaced(
- if (settingsMenuView.isVisible) {
- val bounds = Rect()
- settingsMenuView.getHitRect(bounds)
- bounds.toComposeRect()
- } else {
- null
- }
- )
- },
- content = {
- NotificationStack(
- viewModel = lockscreenSceneViewModel.notifications,
- isScrimVisible = false,
- )
- }
- ) { measurables, constraints ->
- check(measurables.size == 1)
- val height = notificationStackPosition.height.toInt()
- val childConstraints = constraints.copy(minHeight = height, maxHeight = height)
- val placeable = measurables[0].measure(childConstraints)
- layout(constraints.maxWidth, constraints.maxHeight) {
- val start = (constraints.maxWidth - placeable.measuredWidth) / 2
- placeable.placeRelative(x = start, y = notificationStackPosition.top.toInt())
- }
- }
- }
- }
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index 7385a25..84d4246 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -38,6 +38,7 @@
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
+import java.util.Optional
import javax.inject.Inject
/**
@@ -53,7 +54,7 @@
private val smartSpaceSection: SmartSpaceSection,
private val notificationSection: NotificationSection,
private val lockSection: LockSection,
- private val ambientIndicationSection: AmbientIndicationSection,
+ private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
private val bottomAreaSection: BottomAreaSection,
private val settingsMenuSection: SettingsMenuSection,
private val clockInteractor: KeyguardClockInteractor,
@@ -94,8 +95,8 @@
with(notificationSection) {
Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
}
- if (!isUdfpsVisible) {
- with(ambientIndicationSection) {
+ if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
AmbientIndication(modifier = Modifier.fillMaxWidth())
}
}
@@ -105,8 +106,8 @@
// Aligned to bottom and constrained to below the lock icon.
Column(modifier = Modifier.fillMaxWidth()) {
- if (isUdfpsVisible) {
- with(ambientIndicationSection) {
+ if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
AmbientIndication(modifier = Modifier.fillMaxWidth())
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
index acd4779..4148462 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -38,6 +38,7 @@
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
+import java.util.Optional
import javax.inject.Inject
/**
@@ -53,7 +54,7 @@
private val smartSpaceSection: SmartSpaceSection,
private val notificationSection: NotificationSection,
private val lockSection: LockSection,
- private val ambientIndicationSection: AmbientIndicationSection,
+ private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
private val bottomAreaSection: BottomAreaSection,
private val settingsMenuSection: SettingsMenuSection,
private val clockInteractor: KeyguardClockInteractor,
@@ -94,8 +95,8 @@
with(notificationSection) {
Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
}
- if (!isUdfpsVisible) {
- with(ambientIndicationSection) {
+ if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
AmbientIndication(modifier = Modifier.fillMaxWidth())
}
}
@@ -111,8 +112,8 @@
// Aligned to bottom and constrained to below the lock icon.
Column(modifier = Modifier.fillMaxWidth()) {
- if (isUdfpsVisible) {
- with(ambientIndicationSection) {
+ if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
AmbientIndication(modifier = Modifier.fillMaxWidth())
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt
index 1e5481e..af9a195 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/AmbientIndicationSection.kt
@@ -16,38 +16,11 @@
package com.android.systemui.keyguard.ui.composable.section
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
-import javax.inject.Inject
-class AmbientIndicationSection @Inject constructor() {
- @Composable
- fun SceneScope.AmbientIndication(modifier: Modifier = Modifier) {
- MovableElement(
- key = AmbientIndicationElementKey,
- modifier = modifier,
- ) {
- content {
- Box(
- modifier = Modifier.fillMaxWidth().background(Color.Green),
- ) {
- Text(
- text = "TODO(b/316211368): Ambient indication",
- color = Color.White,
- modifier = Modifier.align(Alignment.Center),
- )
- }
- }
- }
- }
+/** Defines interface for classes that can render the ambient indication area. */
+interface AmbientIndicationSection {
+ @Composable fun SceneScope.AmbientIndication(modifier: Modifier)
}
-
-private val AmbientIndicationElementKey = ElementKey("AmbientIndication")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
index 0f3fc47..f021bb6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
@@ -16,76 +16,111 @@
package com.android.systemui.keyguard.ui.composable.section
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material3.Text
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
+import com.android.keyguard.KeyguardClockSwitch
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.res.R
import javax.inject.Inject
class ClockSection
@Inject
constructor(
private val viewModel: KeyguardClockViewModel,
+ private val clockInteractor: KeyguardClockInteractor,
) {
+
@Composable
fun SceneScope.SmallClock(
onTopChanged: (top: Float?) -> Unit,
modifier: Modifier = Modifier,
) {
- if (viewModel.useLargeClock) {
+ val clockSize by viewModel.clockSize.collectAsState()
+ val currentClock by viewModel.currentClock.collectAsState()
+ viewModel.clock = currentClock
+
+ if (clockSize != KeyguardClockSwitch.SMALL) {
onTopChanged(null)
return
}
+ if (currentClock?.smallClock?.view == null) {
+ return
+ }
+
+ val view = LocalView.current
+
+ DisposableEffect(view) {
+ clockInteractor.clockEventController.registerListeners(view)
+
+ onDispose { clockInteractor.clockEventController.unregisterListeners() }
+ }
+
MovableElement(
key = ClockElementKey,
modifier = modifier,
) {
content {
- Box(
+ AndroidView(
+ factory = { checkNotNull(currentClock).smallClock.view },
modifier =
- Modifier.fillMaxWidth()
- .background(Color.Magenta)
- .onTopPlacementChanged(onTopChanged)
- ) {
- Text(
- text = "TODO(b/316211368): Small clock",
- color = Color.White,
- modifier = Modifier.align(Alignment.Center),
- )
- }
+ Modifier.padding(
+ horizontal =
+ dimensionResource(R.dimen.keyguard_affordance_horizontal_offset)
+ )
+ .padding(top = { viewModel.getSmallClockTopMargin(view.context) })
+ .onTopPlacementChanged(onTopChanged),
+ )
}
}
}
@Composable
fun SceneScope.LargeClock(modifier: Modifier = Modifier) {
- if (!viewModel.useLargeClock) {
+ val clockSize by viewModel.clockSize.collectAsState()
+ val currentClock by viewModel.currentClock.collectAsState()
+ viewModel.clock = currentClock
+
+ if (clockSize != KeyguardClockSwitch.LARGE) {
return
}
+ if (currentClock?.largeClock?.view == null) {
+ return
+ }
+
+ val view = LocalView.current
+
+ DisposableEffect(view) {
+ clockInteractor.clockEventController.registerListeners(view)
+
+ onDispose { clockInteractor.clockEventController.unregisterListeners() }
+ }
+
MovableElement(
key = ClockElementKey,
modifier = modifier,
) {
content {
- Box(
- modifier = Modifier.fillMaxWidth().background(Color.Blue),
- ) {
- Text(
- text = "TODO(b/316211368): Large clock",
- color = Color.White,
- modifier = Modifier.align(Alignment.Center),
- )
- }
+ AndroidView(
+ factory = { checkNotNull(currentClock).largeClock.view },
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(top = { viewModel.getLargeClockTopMargin(view.context) })
+ )
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/OptionalSectionModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/OptionalSectionModule.kt
new file mode 100644
index 0000000..5b7a8e6
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/OptionalSectionModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable.section
+
+import dagger.BindsOptionalOf
+import dagger.Module
+
+/**
+ * Dagger module for providing placeholders for optional lockscreen scene sections that don't exist
+ * in AOSP but may be provided by OEMs.
+ */
+@Module
+interface OptionalSectionModule {
+ @BindsOptionalOf fun ambientIndicationSection(): AmbientIndicationSection
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index 65a53f5..9778e53 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -40,6 +40,7 @@
import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Expanding
import com.android.systemui.res.R
import com.android.systemui.scene.ui.composable.Gone
+import com.android.systemui.scene.ui.composable.Lockscreen
import com.android.systemui.scene.ui.composable.QuickSettings as QuickSettingsSceneKey
import com.android.systemui.scene.ui.composable.Shade
@@ -77,7 +78,12 @@
toScene == Shade -> QSSceneAdapter.State.QQS
toScene == QuickSettingsSceneKey -> QSSceneAdapter.State.QS
toScene == Gone -> QSSceneAdapter.State.CLOSED
- else -> error("Bad transition for QuickSettings: $transitionState")
+ toScene == Lockscreen -> QSSceneAdapter.State.CLOSED
+ else ->
+ error(
+ "Bad transition for QuickSettings: fromScene=$fromScene," +
+ " toScene=$toScene"
+ )
}
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt
new file mode 100644
index 0000000..be6bb9c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt
@@ -0,0 +1,35 @@
+/*
+ * 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
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SysuiTestCaseSelfTest : SysuiTestCase() {
+ private val contextBeforeSetup = context
+
+ // cf b/311612168
+ @Test
+ fun captureCorrectContextBeforeSetupRuns() {
+ Truth.assertThat(contextBeforeSetup).isEqualTo(context)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
similarity index 96%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
index 477f455..0329794 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -12,20 +12,19 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
- *
*/
package com.android.systemui.keyguard.data.quickaffordance
import android.app.Activity
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.res.R
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -37,17 +36,17 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.junit.runners.Parameterized.Parameter
-import org.junit.runners.Parameterized.Parameters
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.Parameter
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTestCase() {
companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
index 9daf186..e7037a6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
@@ -94,7 +94,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(6)
+ assertThat(values.size).isEqualTo(5)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index 53bca48..e141c2b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -55,6 +55,28 @@
private val underTest = kosmos.dreamingToLockscreenTransitionViewModel
@Test
+ fun shortcutsAlpha_bothShortcutsReceiveLastValue() =
+ testScope.runTest {
+ val valuesLeft by collectValues(underTest.shortcutsAlpha)
+ val valuesRight by collectValues(underTest.shortcutsAlpha)
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ step(0f, TransitionState.STARTED),
+ step(0.3f),
+ step(0.5f),
+ step(0.6f),
+ step(0.8f),
+ step(1f),
+ ),
+ testScope,
+ )
+
+ assertThat(valuesLeft.last()).isEqualTo(1f)
+ assertThat(valuesRight.last()).isEqualTo(1f)
+ }
+
+ @Test
fun dreamOverlayTranslationY() =
testScope.runTest {
val pixels = 100
@@ -73,7 +95,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(7)
+ assertThat(values.size).isEqualTo(6)
values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
}
@@ -95,7 +117,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(4)
+ assertThat(values.size).isEqualTo(3)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
}
@@ -210,7 +232,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
index 3c07034..897ce6d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -61,7 +61,7 @@
// Only three values should be present, since the dream overlay runs for a small
// fraction of the overall animation time
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
}
@@ -84,7 +84,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index a346e8b..4843f8b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -75,7 +75,7 @@
// Only three values should be present, since the dream overlay runs for a small
// fraction of the overall animation time
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
}
@@ -98,10 +98,10 @@
testScope = testScope,
)
- assertThat(values.size).isEqualTo(6)
+ assertThat(values.size).isEqualTo(5)
values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
// Validate finished value
- assertThat(values[5]).isEqualTo(0f)
+ assertThat(values[4]).isEqualTo(0f)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index 274bde1..a1b8aab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -76,7 +76,7 @@
)
// Only 3 values should be present, since the dream overlay runs for a small fraction
// of the overall animation time
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
}
@@ -99,7 +99,7 @@
),
testScope = testScope,
)
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
}
@@ -121,11 +121,11 @@
),
testScope = testScope,
)
- assertThat(values.size).isEqualTo(4)
+ assertThat(values.size).isEqualTo(3)
values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) }
// Cancel will reset the translation
- assertThat(values[3]).isEqualTo(0)
+ assertThat(values[2]).isEqualTo(0)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
index d419d4a..2111ad5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -95,7 +95,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(5)
+ assertThat(values.size).isEqualTo(4)
values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index f027bc8..90b8362 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -95,7 +95,7 @@
testScope,
)
- assertThat(values.size).isEqualTo(3)
+ assertThat(values.size).isEqualTo(1)
values.forEach { assertThat(it).isEqualTo(0f) }
}
@@ -107,7 +107,7 @@
keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
keyguardTransitionRepository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(2)
+ assertThat(values.size).isEqualTo(1)
values.forEach { assertThat(it).isEqualTo(0f) }
}
@@ -121,7 +121,7 @@
keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
keyguardTransitionRepository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(2)
+ assertThat(values.size).isEqualTo(1)
values.forEach { assertThat(it).isEqualTo(1f) }
}
@@ -135,7 +135,7 @@
keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
keyguardTransitionRepository.sendTransitionStep(step(1f))
- assertThat(values.size).isEqualTo(2)
+ assertThat(values.size).isEqualTo(1)
values.forEach { assertThat(it).isEqualTo(1f) }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index 53cb8a7..7a78b36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -25,15 +25,14 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.graphics.Point;
import android.os.PowerManager;
-import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.view.View;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardUpdateMonitor;
@@ -68,7 +67,7 @@
import java.util.HashSet;
@SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
@RunWithLooper(setAsMainLooper = true)
public class DozeServiceHostTest extends SysuiTestCase {
@@ -181,6 +180,7 @@
DozeLog.PULSE_REASON_DOCKING,
DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE,
DozeLog.REASON_SENSOR_QUICK_PICKUP,
+ DozeLog.PULSE_REASON_FINGERPRINT_ACTIVATED,
DozeLog.REASON_SENSOR_TAP));
HashSet<Integer> reasonsThatDontPulse = new HashSet<>(
Arrays.asList(DozeLog.REASON_SENSOR_PICKUP,
@@ -232,7 +232,7 @@
public void onSlpiTap_doesnt_pass_negative_values() {
mDozeServiceHost.onSlpiTap(-1, 200);
mDozeServiceHost.onSlpiTap(100, -2);
- verifyZeroInteractions(mDozeInteractor);
+ verify(mDozeInteractor, never()).setLastTapToWakePosition(any());
}
@Test
public void dozeTimeTickSentToDozeInteractor() {
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index e01a2aa..5c362b2 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -963,10 +963,16 @@
<bool name="config_edgeToEdgeBottomSheetDialog">true</bool>
<!--
+ Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate when
+ the screen is turned off with AOD not enabled.
+ TODO(b/302332976) Get this value from the HAL if they can provide an API for it.
+ -->
+ <integer name="config_restToUnlockDurationScreenOff">500</integer>
+ <!--
Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate
TODO(b/302332976) Get this value from the HAL if they can provide an API for it.
-->
- <integer name="config_restToUnlockDuration">300</integer>
+ <integer name="config_restToUnlockDurationDefault">300</integer>
<!--
Width in pixels of the Side FPS sensor.
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index bcc2044..82410fd 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -33,6 +33,7 @@
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.customization.R
import com.android.systemui.dagger.qualifiers.Background
@@ -325,6 +326,10 @@
}
}
+ if (visible) {
+ refreshTime()
+ }
+
smallTimeListener?.update(shouldTimeListenerRun)
largeTimeListener?.update(shouldTimeListenerRun)
}
@@ -346,6 +351,19 @@
weatherData = data
clock?.run { events.onWeatherDataChanged(data) }
}
+
+ override fun onTimeChanged() {
+ refreshTime()
+ }
+
+ private fun refreshTime() {
+ if (!migrateClocksToBlueprint()) {
+ return
+ }
+
+ clock?.smallClock?.events?.onTimeTick()
+ clock?.largeClock?.events?.onTimeTick()
+ }
}
private val zenModeCallback = object : ZenModeController.Callback {
@@ -558,7 +576,8 @@
isRunning = true
when (clockFace.config.tickRate) {
ClockTickRate.PER_MINUTE -> {
- /* Handled by KeyguardClockSwitchController */
+ // Handled by KeyguardClockSwitchController and
+ // by KeyguardUpdateMonitorCallback#onTimeChanged.
}
ClockTickRate.PER_SECOND -> executor.execute(secondsRunnable)
ClockTickRate.PER_FRAME -> {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
index f4231ac..c320350 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
@@ -26,6 +26,8 @@
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.isDefaultOrientation
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.log.SideFpsLogger
import com.android.systemui.res.R
import java.util.Optional
@@ -47,6 +49,7 @@
windowManager: WindowManager,
displayStateInteractor: DisplayStateInteractor,
fingerprintInteractiveToAuthProvider: Optional<FingerprintInteractiveToAuthProvider>,
+ keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val logger: SideFpsLogger,
) {
@@ -62,8 +65,21 @@
val isAvailable: Flow<Boolean> =
fingerprintPropertyRepository.sensorType.map { it == FingerprintSensorType.POWER_BUTTON }
- val authenticationDuration: Long =
- context.resources?.getInteger(R.integer.config_restToUnlockDuration)?.toLong() ?: 0L
+ val authenticationDuration: Flow<Long> =
+ keyguardTransitionInteractor
+ .isFinishedInStateWhere { it == KeyguardState.OFF || it == KeyguardState.DOZING }
+ .map {
+ if (it)
+ context.resources
+ ?.getInteger(R.integer.config_restToUnlockDurationScreenOff)
+ ?.toLong()
+ else
+ context.resources
+ ?.getInteger(R.integer.config_restToUnlockDurationDefault)
+ ?.toLong()
+ }
+ .map { it ?: 0L }
+ .onEach { logger.authDurationChanged(it) }
val isProlongedTouchRequiredForAuthentication: Flow<Boolean> =
if (fingerprintInteractiveToAuthProvider.isEmpty) {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
index 5df26b3..a6b4320 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt
@@ -28,27 +28,26 @@
/**
* Factory to create dialogs for consenting to show app panels for specific apps.
*
- * [internalDialogFactory] is for facilitating testing.
+ * [dialogFactory] is for facilitating testing.
*/
-class PanelConfirmationDialogFactory(
- private val internalDialogFactory: (Context) -> SystemUIDialog
+class PanelConfirmationDialogFactory @Inject constructor(
+ private val dialogFactory: SystemUIDialog.Factory
) {
- @Inject constructor() : this({ SystemUIDialog(it) })
/**
* Creates a dialog to show to the user. [response] will be true if an only if the user responds
* affirmatively.
*/
fun createConfirmationDialog(
- context: Context,
- appName: CharSequence,
- response: Consumer<Boolean>
+ context: Context,
+ appName: CharSequence,
+ response: Consumer<Boolean>
): Dialog {
val listener =
DialogInterface.OnClickListener { _, which ->
response.accept(which == DialogInterface.BUTTON_POSITIVE)
}
- return internalDialogFactory(context).apply {
+ return dialogFactory.create(context).apply {
setTitle(this.context.getString(R.string.controls_panel_authorization_title, appName))
setMessage(this.context.getString(R.string.controls_panel_authorization, appName))
setCanceledOnTouchOutside(true)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt
index 2ad6014..e42a4a6 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt
@@ -25,20 +25,21 @@
import java.util.function.Consumer
import javax.inject.Inject
-class ControlsDialogsFactory(private val internalDialogFactory: (Context) -> SystemUIDialog) {
+class ControlsDialogsFactory @Inject constructor(
+ private val dialogFactory: SystemUIDialog.Factory
+) {
- @Inject constructor() : this({ SystemUIDialog(it) })
fun createRemoveAppDialog(
- context: Context,
- appName: CharSequence,
- response: Consumer<Boolean>
+ context: Context,
+ appName: CharSequence,
+ response: Consumer<Boolean>
): Dialog {
val listener =
DialogInterface.OnClickListener { _, which ->
response.accept(which == DialogInterface.BUTTON_POSITIVE)
}
- return internalDialogFactory(context).apply {
+ return dialogFactory.create(context).apply {
setTitle(context.getString(R.string.controls_panel_remove_app_authorization, appName))
setCanceledOnTouchOutside(true)
setOnCancelListener { response.accept(false) }
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
index efa1c0a..684627b 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
@@ -19,6 +19,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterIsInstance
@@ -31,4 +32,11 @@
) {
val fingerprintFailure: Flow<FailFingerprintAuthenticationStatus> =
repository.authenticationStatus.filterIsInstance<FailFingerprintAuthenticationStatus>()
+
+ /** Whether fingerprint authentication is currently running or not */
+ val isRunning: Flow<Boolean> = repository.isRunning
+
+ /** Provide the current status of fingerprint authentication. */
+ val authenticationStatus: Flow<FingerprintAuthenticationStatus> =
+ repository.authenticationStatus
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
index 4c4aa5c..8776ec5 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
@@ -118,6 +118,11 @@
* Called when the dozing state may have been updated.
*/
default void onDozingChanged(boolean isDozing) {}
+
+ /**
+ * Called when fingerprint acquisition has started and screen state might need updating.
+ */
+ default void onSideFingerprintAcquisitionStarted() {}
}
interface PulseCallback {
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 5b90ef2..424bd0a 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -514,6 +514,7 @@
case REASON_SENSOR_TAP: return "tap";
case REASON_SENSOR_UDFPS_LONG_PRESS: return "udfps";
case REASON_SENSOR_QUICK_PICKUP: return "quickPickup";
+ case PULSE_REASON_FINGERPRINT_ACTIVATED: return "fingerprint-triggered";
default: throw new IllegalArgumentException("invalid reason: " + pulseReason);
}
}
@@ -542,7 +543,9 @@
PULSE_REASON_SENSOR_SIGMOTION, REASON_SENSOR_PICKUP, REASON_SENSOR_DOUBLE_TAP,
PULSE_REASON_SENSOR_LONG_PRESS, PULSE_REASON_DOCKING, REASON_SENSOR_WAKE_UP_PRESENCE,
PULSE_REASON_SENSOR_WAKE_REACH, REASON_SENSOR_TAP,
- REASON_SENSOR_UDFPS_LONG_PRESS, REASON_SENSOR_QUICK_PICKUP})
+ REASON_SENSOR_UDFPS_LONG_PRESS, REASON_SENSOR_QUICK_PICKUP,
+ PULSE_REASON_FINGERPRINT_ACTIVATED
+ })
public @interface Reason {}
public static final int PULSE_REASON_NONE = -1;
public static final int PULSE_REASON_INTENT = 0;
@@ -557,6 +560,7 @@
public static final int REASON_SENSOR_TAP = 9;
public static final int REASON_SENSOR_UDFPS_LONG_PRESS = 10;
public static final int REASON_SENSOR_QUICK_PICKUP = 11;
+ public static final int PULSE_REASON_FINGERPRINT_ACTIVATED = 12;
- public static final int TOTAL_REASONS = 12;
+ public static final int TOTAL_REASONS = 13;
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 795c3d4..9311187 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -265,6 +265,10 @@
mDozeLog.traceNotificationPulse();
}
+ private void onSideFingerprintAcquisitionStarted() {
+ requestPulse(DozeLog.PULSE_REASON_FINGERPRINT_ACTIVATED, false, null);
+ }
+
private static void runIfNotNull(Runnable runnable) {
if (runnable != null) {
runnable.run();
@@ -690,5 +694,10 @@
public void onNotificationAlerted(Runnable onPulseSuppressedListener) {
onNotification(onPulseSuppressedListener);
}
+
+ @Override
+ public void onSideFingerprintAcquisitionStarted() {
+ DozeTriggers.this.onSideFingerprintAcquisitionStarted();
+ }
};
}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
index 6cb68ba..89bfd96 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
@@ -16,6 +16,7 @@
package com.android.systemui.haptics.slider
+import android.view.MotionEvent
import androidx.annotation.FloatRange
/** Configuration parameters of a [SliderHapticFeedbackProvider] */
@@ -38,6 +39,8 @@
val numberOfLowTicks: Int = 5,
/** Maximum velocity allowed for vibration scaling. This is not expected to change. */
val maxVelocityToScale: Float = 2000f, /* In pixels/sec */
+ /** Axis to use when computing velocity. Must be the same as the slider's axis of movement */
+ val velocityAxis: Int = MotionEvent.AXIS_X,
/** Vibration scale at the upper bookend of the slider */
@FloatRange(from = 0.0, to = 1.0) val upperBookendScale: Float = 1f,
/** Vibration scale at the lower bookend of the slider */
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
index 9e6245a..6f28ab7 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
@@ -162,27 +162,33 @@
override fun onLowerBookend() {
if (!hasVibratedAtLowerBookend) {
- velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
- vibrateOnEdgeCollision(abs(velocityTracker.xVelocity))
+ vibrateOnEdgeCollision(abs(getTrackedVelocity()))
hasVibratedAtLowerBookend = true
}
}
override fun onUpperBookend() {
if (!hasVibratedAtUpperBookend) {
- velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
- vibrateOnEdgeCollision(abs(velocityTracker.xVelocity))
+ vibrateOnEdgeCollision(abs(getTrackedVelocity()))
hasVibratedAtUpperBookend = true
}
}
override fun onProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
- velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
- vibrateDragTexture(abs(velocityTracker.xVelocity), progress)
+ vibrateDragTexture(abs(getTrackedVelocity()), progress)
hasVibratedAtUpperBookend = false
hasVibratedAtLowerBookend = false
}
+ private fun getTrackedVelocity(): Float {
+ velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
+ return if (velocityTracker.isAxisSupported(config.velocityAxis)) {
+ velocityTracker.getAxisVelocity(config.velocityAxis)
+ } else {
+ 0f
+ }
+ }
+
override fun onProgressJump(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
override fun onSelectAndArrow(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 50836fe..afef875 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -46,6 +46,7 @@
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.NotificationShadeWindowView
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.KeyguardIndicationController
@@ -111,7 +112,9 @@
bindKeyguardRootView()
initializeViews()
- KeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel)
+ if (!SceneContainerFlag.isEnabled) {
+ KeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel)
+ }
keyguardBlueprintCommandListener.start()
}
@@ -144,6 +147,10 @@
}
private fun bindKeyguardRootView() {
+ if (SceneContainerFlag.isEnabled) {
+ return
+ }
+
rootViewHandle?.dispose()
rootViewHandle =
KeyguardRootViewBinder.bind(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 1277585..cf1d247 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -32,6 +32,7 @@
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
@@ -89,7 +90,6 @@
val start = (startTime / transitionDuration).toFloat()
val chunks = (transitionDuration / duration).toFloat()
logger.logCreate(name, start)
- var isComplete = true
fun stepToValue(step: TransitionStep): Float? {
val value = (step.value - start) * chunks
@@ -98,17 +98,13 @@
// middle, it is possible this animation is being skipped but we need to inform
// the ViewModels of the last update
STARTED -> {
- isComplete = false
onStart?.invoke()
max(0f, min(1f, value))
}
// Always send a final value of 1. Because of rounding, [value] may never be
// exactly 1.
RUNNING ->
- if (isComplete) {
- null
- } else if (value >= 1f) {
- isComplete = true
+ if (value >= 1f) {
1f
} else if (value >= 0f) {
value
@@ -132,6 +128,7 @@
value
}
.filterNotNull()
+ .distinctUntilChanged()
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 528a2ee..5bb2782 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.content.Context
import androidx.constraintlayout.helper.widget.Layer
import com.android.keyguard.KeyguardClockSwitch.LARGE
import com.android.keyguard.KeyguardClockSwitch.SMALL
@@ -25,6 +26,9 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.shared.model.SettingsClockSize
import com.android.systemui.plugins.clocks.ClockController
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.SplitShadeStateController
+import com.android.systemui.util.Utils
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
@@ -36,9 +40,10 @@
class KeyguardClockViewModel
@Inject
constructor(
- val keyguardInteractor: KeyguardInteractor,
- val keyguardClockInteractor: KeyguardClockInteractor,
+ keyguardInteractor: KeyguardInteractor,
+ private val keyguardClockInteractor: KeyguardClockInteractor,
@Application private val applicationScope: CoroutineScope,
+ private val splitShadeStateController: SplitShadeStateController,
) {
var burnInLayer: Layer? = null
val useLargeClock: Boolean
@@ -85,4 +90,43 @@
started = SharingStarted.WhileSubscribed(),
initialValue = false
)
+
+ // Needs to use a non application context to get display cutout.
+ fun getSmallClockTopMargin(context: Context) =
+ if (splitShadeStateController.shouldUseSplitNotificationShade(context.resources)) {
+ context.resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin)
+ } else {
+ context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
+ Utils.getStatusBarHeaderHeightKeyguard(context)
+ }
+
+ fun getLargeClockTopMargin(context: Context): Int {
+ var largeClockTopMargin =
+ context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
+ context.resources.getDimensionPixelSize(
+ com.android.systemui.customization.R.dimen.small_clock_padding_top
+ ) +
+ context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset)
+ largeClockTopMargin += getDimen(context, DATE_WEATHER_VIEW_HEIGHT)
+ largeClockTopMargin += getDimen(context, ENHANCED_SMARTSPACE_HEIGHT)
+ if (!useLargeClock) {
+ largeClockTopMargin -=
+ context.resources.getDimensionPixelSize(
+ com.android.systemui.customization.R.dimen.small_clock_height
+ )
+ }
+
+ return largeClockTopMargin
+ }
+
+ private fun getDimen(context: Context, name: String): Int {
+ val res = context.packageManager.getResourcesForApplication(context.packageName)
+ val id = res.getIdentifier(name, "dimen", context.packageName)
+ return res.getDimensionPixelSize(id)
+ }
+
+ companion object {
+ private const val DATE_WEATHER_VIEW_HEIGHT = "date_weather_view_height"
+ private const val ENHANCED_SMARTSPACE_HEIGHT = "enhanced_smartspace_height"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
index 1dbf1f1..693e3b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
@@ -28,13 +28,16 @@
import com.android.systemui.biometrics.shared.model.isDefaultOrientation
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.DozeServiceHost
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
@@ -43,6 +46,7 @@
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
@@ -54,9 +58,13 @@
@Inject
constructor(
private val context: Context,
- private val fpAuthRepository: DeviceEntryFingerprintAuthRepository,
+ private val fpAuthRepository: DeviceEntryFingerprintAuthInteractor,
private val sfpsSensorInteractor: SideFpsSensorInteractor,
+ // todo (b/317432075) Injecting DozeServiceHost directly instead of using it through
+ // DozeInteractor as DozeServiceHost already depends on DozeInteractor.
+ private val dozeServiceHost: DozeServiceHost,
displayStateInteractor: DisplayStateInteractor,
+ @Main private val mainDispatcher: CoroutineDispatcher,
@Application private val applicationScope: CoroutineScope,
) {
private val _progress = MutableStateFlow(0.0f)
@@ -168,18 +176,21 @@
return@collectLatest
}
animatorJob =
- fpAuthRepository.authenticationStatus
- .onEach { authStatus ->
+ combine(
+ sfpsSensorInteractor.authenticationDuration,
+ fpAuthRepository.authenticationStatus,
+ ::Pair
+ )
+ .onEach { (authDuration, authStatus) ->
when (authStatus) {
is AcquiredFingerprintAuthenticationStatus -> {
if (authStatus.fingerprintCaptureStarted) {
_visible.value = true
+ dozeServiceHost.fireSideFpsAcquisitionStarted()
_animator?.cancel()
_animator =
ValueAnimator.ofFloat(0.0f, 1.0f)
- .setDuration(
- sfpsSensorInteractor.authenticationDuration
- )
+ .setDuration(authDuration)
.apply {
addUpdateListener {
_progress.value = it.animatedValue as Float
@@ -209,6 +220,7 @@
else -> Unit
}
}
+ .flowOn(mainDispatcher)
.onCompletion { _animator?.cancel() }
.launchIn(applicationScope)
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt
index 919072a..171656a 100644
--- a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt
@@ -108,4 +108,13 @@
}
)
}
+
+ fun authDurationChanged(duration: Long) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { long1 = duration },
+ { "SideFpsSensor auth duration changed: $long1" }
+ )
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
index a6c6233..7e06f5a 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
@@ -87,6 +87,7 @@
import java.util.Objects;
import javax.inject.Inject;
+import javax.inject.Provider;
/**
*/
@@ -149,6 +150,7 @@
public static final String EXTRA_CONFIRM_ONLY = "extra_confirm_only";
private final Context mContext;
+ private final SystemUIDialog.Factory mSystemUIDialogFactory;
private final NotificationManager mNoMan;
private final PowerManager mPowerMan;
private final KeyguardManager mKeyguard;
@@ -186,11 +188,17 @@
/**
*/
@Inject
- public PowerNotificationWarnings(Context context, ActivityStarter activityStarter,
- BroadcastSender broadcastSender, Lazy<BatteryController> batteryControllerLazy,
- DialogLaunchAnimator dialogLaunchAnimator, UiEventLogger uiEventLogger,
- GlobalSettings globalSettings, UserTracker userTracker) {
+ public PowerNotificationWarnings(
+ Context context,
+ ActivityStarter activityStarter,
+ BroadcastSender broadcastSender,
+ Lazy<BatteryController> batteryControllerLazy,
+ DialogLaunchAnimator dialogLaunchAnimator,
+ UiEventLogger uiEventLogger,
+ UserTracker userTracker,
+ SystemUIDialog.Factory systemUIDialogFactory) {
mContext = context;
+ mSystemUIDialogFactory = systemUIDialogFactory;
mNoMan = mContext.getSystemService(NotificationManager.class);
mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mKeyguard = mContext.getSystemService(KeyguardManager.class);
@@ -444,7 +452,7 @@
private void showHighTemperatureDialog() {
if (mHighTempDialog != null) return;
- final SystemUIDialog d = new SystemUIDialog(mContext);
+ final SystemUIDialog d = mSystemUIDialogFactory.create();
d.setIconAttribute(android.R.attr.alertDialogIcon);
d.setTitle(R.string.high_temp_title);
d.setMessage(R.string.high_temp_dialog_message);
@@ -479,7 +487,7 @@
private void showThermalShutdownDialog() {
if (mThermalShutdownDialog != null) return;
- final SystemUIDialog d = new SystemUIDialog(mContext);
+ final SystemUIDialog d = mSystemUIDialogFactory.create();
d.setIconAttribute(android.R.attr.alertDialogIcon);
d.setTitle(R.string.thermal_shutdown_title);
d.setMessage(R.string.thermal_shutdown_dialog_message);
@@ -643,7 +651,7 @@
private void showStartSaverConfirmation(Bundle extras) {
if (mSaverConfirmation != null || mUseExtraSaverConfirmation) return;
- final SystemUIDialog d = new SystemUIDialog(mContext);
+ final SystemUIDialog d = mSystemUIDialogFactory.create();
final boolean confirmOnly = extras.getBoolean(BatterySaverUtils.EXTRA_CONFIRM_TEXT_ONLY);
final int batterySaverTriggerMode =
extras.getInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 6f35cfb..b5def41 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -148,7 +148,8 @@
private val deviceConfigProxy: DeviceConfigProxy,
private val dialogLaunchAnimator: DialogLaunchAnimator,
private val broadcastDispatcher: BroadcastDispatcher,
- private val dumpManager: DumpManager
+ private val dumpManager: DumpManager,
+ private val systemUIDialogFactory: SystemUIDialog.Factory,
) : Dumpable, FgsManagerController {
companion object {
@@ -375,7 +376,7 @@
override fun showDialog(expandable: Expandable?) {
synchronized(lock) {
if (dialog == null) {
- val dialog = SystemUIDialog(context)
+ val dialog = systemUIDialogFactory.create()
dialog.setTitle(R.string.fgs_manager_dialog_title)
dialog.setMessage(R.string.fgs_manager_dialog_message)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
index ccf7afb..c9b0022 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
@@ -55,6 +55,7 @@
private final DataSaverController mDataSaverController;
private final DialogLaunchAnimator mDialogLaunchAnimator;
+ private final SystemUIDialog.Factory mSystemUIDialogFactory;
@Inject
public DataSaverTile(
@@ -68,12 +69,14 @@
ActivityStarter activityStarter,
QSLogger qsLogger,
DataSaverController dataSaverController,
- DialogLaunchAnimator dialogLaunchAnimator
+ DialogLaunchAnimator dialogLaunchAnimator,
+ SystemUIDialog.Factory systemUIDialogFactory
) {
super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
mDataSaverController = dataSaverController;
mDialogLaunchAnimator = dialogLaunchAnimator;
+ mSystemUIDialogFactory = systemUIDialogFactory;
mDataSaverController.observe(getLifecycle(), this);
}
@@ -98,7 +101,7 @@
// Show a dialog to confirm first. Dialogs shown by the DialogLaunchAnimator must be created
// and shown on the main thread, so we post it to the UI handler.
mUiHandler.post(() -> {
- SystemUIDialog dialog = new SystemUIDialog(mContext);
+ SystemUIDialog dialog = mSystemUIDialogFactory.create();
dialog.setTitle(com.android.internal.R.string.data_saver_enable_title);
dialog.setMessage(com.android.internal.R.string.data_saver_description);
dialog.setPositiveButton(com.android.internal.R.string.data_saver_enable_button,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index acd7510..41cd221 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
@@ -23,7 +23,6 @@
import android.content.Intent
import android.provider.Settings
import android.view.LayoutInflater
-import androidx.annotation.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.UiEventLogger
import com.android.systemui.res.R
@@ -44,31 +43,15 @@
* Controller for [UserDialog].
*/
@SysUISingleton
-class UserSwitchDialogController @VisibleForTesting constructor(
- private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
- private val activityStarter: ActivityStarter,
- private val falsingManager: FalsingManager,
- private val dialogLaunchAnimator: DialogLaunchAnimator,
- private val uiEventLogger: UiEventLogger,
- private val dialogFactory: (Context) -> SystemUIDialog
+class UserSwitchDialogController @Inject constructor(
+ private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
+ private val activityStarter: ActivityStarter,
+ private val falsingManager: FalsingManager,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val uiEventLogger: UiEventLogger,
+ private val dialogFactory: SystemUIDialog.Factory
) {
- @Inject
- constructor(
- userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
- activityStarter: ActivityStarter,
- falsingManager: FalsingManager,
- dialogLaunchAnimator: DialogLaunchAnimator,
- uiEventLogger: UiEventLogger
- ) : this(
- userDetailViewAdapterProvider,
- activityStarter,
- falsingManager,
- dialogLaunchAnimator,
- uiEventLogger,
- { SystemUIDialog(it) }
- )
-
companion object {
private const val INTERACTION_JANK_TAG = "switch_user"
private val USER_SETTINGS_INTENT = Intent(Settings.ACTION_USER_SETTINGS)
@@ -81,7 +64,7 @@
* [userDetailViewAdapterProvider] and show it as launched from [expandable].
*/
fun showDialog(context: Context, expandable: Expandable) {
- with(dialogFactory(context)) {
+ with(dialogFactory.create()) {
setShowForAllUsers(true)
setCanceledOnTouchOutside(true)
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
index f071623..9076182 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java
@@ -21,8 +21,10 @@
import android.annotation.TestApi;
import android.content.Context;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManagerGlobal;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.LinearLayout;
@@ -72,20 +74,27 @@
private DeviceStateManager.DeviceStateCallback mDeviceStateManagerCallback =
new DeviceStateManagerCallback();
- private final Context mContext;
private final CommandQueue mCommandQueue;
private final Executor mExecutor;
+ private final Resources mResources;
+ private final LayoutInflater mLayoutInflater;
+ private final SystemUIDialog.Factory mSystemUIDialogFactory;
- @VisibleForTesting
- SystemUIDialog mRearDisplayEducationDialog;
+ private SystemUIDialog mRearDisplayEducationDialog;
@Nullable LinearLayout mDialogViewContainer;
@Inject
- public RearDisplayDialogController(Context context, CommandQueue commandQueue,
- @Main Executor executor) {
- mContext = context;
+ public RearDisplayDialogController(
+ CommandQueue commandQueue,
+ @Main Executor executor,
+ @Main Resources resources,
+ LayoutInflater layoutInflater,
+ SystemUIDialog.Factory systemUIDialogFactory) {
mCommandQueue = commandQueue;
mExecutor = executor;
+ mResources = resources;
+ mLayoutInflater = layoutInflater;
+ mSystemUIDialogFactory = systemUIDialogFactory;
}
@Override
@@ -104,8 +113,7 @@
if (mRearDisplayEducationDialog != null && mRearDisplayEducationDialog.isShowing()
&& mDialogViewContainer != null) {
// Refresh the dialog view when configuration is changed.
- Context dialogContext = mRearDisplayEducationDialog.getContext();
- View dialogView = createDialogView(dialogContext);
+ View dialogView = createDialogView(mRearDisplayEducationDialog.getContext());
mDialogViewContainer.removeAllViews();
mDialogViewContainer.addView(dialogView);
}
@@ -114,9 +122,7 @@
private void createAndShowDialog() {
mServiceNotified = false;
Context dialogContext = mRearDisplayEducationDialog.getContext();
-
View dialogView = createDialogView(dialogContext);
-
mDialogViewContainer = new LinearLayout(dialogContext);
mDialogViewContainer.setLayoutParams(
new LinearLayout.LayoutParams(
@@ -133,11 +139,11 @@
private View createDialogView(Context context) {
View dialogView;
+ LayoutInflater inflater = mLayoutInflater.cloneInContext(context);
if (mStartedFolded) {
- dialogView = View.inflate(context,
- R.layout.activity_rear_display_education, null);
+ dialogView = inflater.inflate(R.layout.activity_rear_display_education, null);
} else {
- dialogView = View.inflate(context,
+ dialogView = inflater.inflate(
R.layout.activity_rear_display_education_opened, null);
}
LottieAnimationView animationView = dialogView.findViewById(
@@ -172,9 +178,9 @@
* Ensures we're not using old values from when the dialog may have been shown previously.
*/
private void initializeValues(int startingBaseState) {
- mRearDisplayEducationDialog = new SystemUIDialog(mContext);
+ mRearDisplayEducationDialog = mSystemUIDialogFactory.create();
if (mFoldedStates == null) {
- mFoldedStates = mContext.getResources().getIntArray(
+ mFoldedStates = mResources.getIntArray(
com.android.internal.R.array.config_foldedDeviceStates);
}
mStartedFolded = isFoldedState(startingBaseState);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index c9df317..9b8dd0b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -275,7 +275,12 @@
updateLockscreenNotificationSetting();
updatePublicMode();
- mPresenter.onUserSwitched(mCurrentUserId);
+ if (mPresenter != null) {
+ mPresenter.onUserSwitched(mCurrentUserId);
+ } else {
+ Log.w(TAG, "user switch before setup with presenter",
+ new Exception());
+ }
for (UserChangedListener listener : mListeners) {
listener.onUserChanged(mCurrentUserId);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 0f640c9..805b44c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -4455,9 +4455,13 @@
mSectionsManager.setHeaderForegroundColors(onSurface, onSurfaceVariant);
- mFooterView.updateColors();
+ if (mFooterView != null) {
+ mFooterView.updateColors();
+ }
- mEmptyShadeView.setTextColors(onSurface, onSurfaceVariant);
+ if (mEmptyShadeView != null) {
+ mEmptyShadeView.setTextColors(onSurface, onSurfaceVariant);
+ }
}
void goToFullShade(long delay) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index 600d4af..45005cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -55,11 +55,12 @@
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.util.Assert;
+import dagger.Lazy;
+
import java.util.ArrayList;
import javax.inject.Inject;
-import dagger.Lazy;
import kotlinx.coroutines.ExperimentalCoroutinesApi;
/**
@@ -175,6 +176,16 @@
}
}
+ /**
+ * Notify the registered callback about SPFS fingerprint acquisition started event.
+ */
+ public void fireSideFpsAcquisitionStarted() {
+ Assert.isMainThread();
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).onSideFingerprintAcquisitionStarted();
+ }
+ }
+
void fireNotificationPulse(NotificationEntry entry) {
Runnable pulseSuppressedListener = () -> {
if (NotificationIconContainerRefactor.isEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index af6da3f..3394eac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -149,6 +149,14 @@
return create(new DialogDelegate<>(){}, mContext);
}
+ /** Creates a new instance of {@link SystemUIDialog} with no customized behavior.
+ *
+ * When you just need a dialog created with a specific {@link Context}, call this.
+ */
+ public SystemUIDialog create(Context context) {
+ return create(new DialogDelegate<>(){}, context);
+ }
+
/**
* Creates a new instance of {@link SystemUIDialog} with {@code delegate} as the {@link
* Delegate}.
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
index 8087a87..550a65c 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -48,6 +48,8 @@
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.leak.LeakDetector;
+import dagger.Lazy;
+
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -87,6 +89,7 @@
// Set of all tunables, used for leak detection.
private final HashSet<Tunable> mTunables = LeakDetector.ENABLED ? new HashSet<>() : null;
private final Context mContext;
+ private final Lazy<SystemUIDialog.Factory> mSystemUIDialogFactoryLazy;
private final LeakDetector mLeakDetector;
private final DemoModeController mDemoModeController;
@@ -104,9 +107,11 @@
@Main Handler mainHandler,
LeakDetector leakDetector,
DemoModeController demoModeController,
- UserTracker userTracker) {
+ UserTracker userTracker,
+ Lazy<SystemUIDialog.Factory> systemUIDialogFactoryLazy) {
super(context);
mContext = context;
+ mSystemUIDialogFactoryLazy = systemUIDialogFactoryLazy;
mContentResolver = mContext.getContentResolver();
mLeakDetector = leakDetector;
mDemoModeController = demoModeController;
@@ -301,7 +306,7 @@
@Override
public void showResetRequest(Runnable onDisabled) {
- SystemUIDialog dialog = new SystemUIDialog(mContext);
+ SystemUIDialog dialog = mSystemUIDialogFactoryLazy.get().create();
dialog.setShowForAllUsers(true);
dialog.setMessage(R.string.remove_from_settings_prompt);
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mContext.getString(R.string.cancel),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
index bfb5485..c525711 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
@@ -120,7 +120,7 @@
fontScalingDialogDelegate
)
- whenever(dialogFactory.create(any())).thenReturn(dialog)
+ whenever(dialogFactory.create(any(), any())).thenReturn(dialog)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt
index 640807b..8adee8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt
@@ -36,15 +36,24 @@
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
import com.android.systemui.log.SideFpsLogger
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.res.R
+import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -62,9 +71,10 @@
@SmallTest
@RunWith(JUnit4::class)
class SideFpsSensorInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
@JvmField @Rule var mockitoRule = MockitoJUnit.rule()
- private val testScope = TestScope(StandardTestDispatcher())
+ private val testScope = kosmos.testScope
private val fingerprintRepository = FakeFingerprintPropertyRepository()
@@ -101,6 +111,7 @@
windowManager,
displayStateInteractor,
Optional.of(fingerprintInteractiveToAuthProvider),
+ kosmos.keyguardTransitionInteractor,
SideFpsLogger(logcatLogBuffer("SfpsLogger"))
)
}
@@ -129,11 +140,62 @@
assertThat(isAvailable).isFalse()
}
+ private suspend fun sendTransition(from: KeyguardState, to: KeyguardState) {
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ TransitionStep(
+ from = from,
+ to = to,
+ transitionState = TransitionState.STARTED,
+ ),
+ TransitionStep(
+ from = from,
+ to = to,
+ transitionState = TransitionState.FINISHED,
+ value = 1.0f
+ )
+ ),
+ testScope
+ )
+ }
+
@Test
- fun authenticationDurationIsAvailableWhenSFPSSensorIsAvailable() =
+ fun authenticationDurationIsLongerIfScreenIsOff() =
testScope.runTest {
- assertThat(underTest.authenticationDuration)
- .isEqualTo(context.resources.getInteger(R.integer.config_restToUnlockDuration))
+ val authenticationDuration by collectLastValue(underTest.authenticationDuration)
+ val longDuration =
+ context.resources.getInteger(R.integer.config_restToUnlockDurationScreenOff)
+ sendTransition(LOCKSCREEN, OFF)
+
+ runCurrent()
+ assertThat(authenticationDuration).isEqualTo(longDuration)
+ }
+
+ @Test
+ fun authenticationDurationIsLongerIfScreenIsDozing() =
+ testScope.runTest {
+ val authenticationDuration by collectLastValue(underTest.authenticationDuration)
+ val longDuration =
+ context.resources.getInteger(R.integer.config_restToUnlockDurationScreenOff)
+ sendTransition(LOCKSCREEN, DOZING)
+ runCurrent()
+ assertThat(authenticationDuration).isEqualTo(longDuration)
+ }
+
+ @Test
+ fun authenticationDurationIsShorterIfScreenIsNotDozingOrOff() =
+ testScope.runTest {
+ val authenticationDuration by collectLastValue(underTest.authenticationDuration)
+ val shortDuration =
+ context.resources.getInteger(R.integer.config_restToUnlockDurationDefault)
+ val allOtherKeyguardStates = KeyguardState.entries.filter { it != OFF && it != DOZING }
+
+ allOtherKeyguardStates.forEach { destinationState ->
+ sendTransition(OFF, destinationState)
+
+ runCurrent()
+ assertThat(authenticationDuration).isEqualTo(shortDuration)
+ }
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index cb26178..755fa02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -75,6 +75,7 @@
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import java.util.Optional
@@ -82,6 +83,7 @@
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -235,15 +237,18 @@
windowManager,
displayStateInteractor,
Optional.of(fingerprintInteractiveToAuthProvider),
+ mock(),
SideFpsLogger(logcatLogBuffer("SfpsLogger"))
)
sideFpsProgressBarViewModel =
SideFpsProgressBarViewModel(
mContext,
- deviceEntryFingerprintAuthRepository,
+ mock(),
sfpsSensorInteractor,
+ mock(),
displayStateInteractor,
+ UnconfinedTestDispatcher(),
testScope.backgroundScope,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
index 823b952..bdca948 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
@@ -72,6 +72,7 @@
import com.android.systemui.unfold.compat.ScreenSizeFoldProvider
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -238,15 +239,18 @@
windowManager,
displayStateInteractor,
Optional.of(fingerprintInteractiveToAuthProvider),
+ mock(),
SideFpsLogger(logcatLogBuffer("SfpsLogger"))
)
sideFpsProgressBarViewModel =
SideFpsProgressBarViewModel(
mContext,
- deviceEntryFingerprintAuthRepository,
+ mock(),
sfpsSensorInteractor,
+ mock(),
displayStateInteractor,
+ StandardTestDispatcher(),
testScope.backgroundScope,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
index 4022d43..3ff43c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java
@@ -28,8 +28,6 @@
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
-import android.view.LayoutInflater;
-import android.view.View;
import android.widget.Button;
import android.widget.TextView;
@@ -95,7 +93,7 @@
mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true);
when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
- when(mSystemUIDialogFactory.create(any())).thenReturn(mDialog);
+ when(mSystemUIDialogFactory.create(any(), any())).thenReturn(mDialog);
mBroadcastDialogDelegate = new BroadcastDialogDelegate(
mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt
index 65f68f9..35ac2ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt
@@ -31,6 +31,7 @@
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.model.SysUiState
import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.phone.DialogDelegate
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
@@ -69,7 +70,8 @@
mDependency.injectTestDependency(SysUiState::class.java, sysuiState)
mDependency.injectMockDependency(DialogLaunchAnimator::class.java)
whenever(sysuiState.setFlag(any(), any())).thenReturn(sysuiState)
- whenever(sysuiDialogFactory.create(any())).thenReturn(sysuiDialog)
+ whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java)))
+ .thenReturn(sysuiDialog)
whenever(sysuiDialog.layoutInflater).thenReturn(LayoutInflater.from(mContext))
whenever(mockUserTracker.userId).thenReturn(context.userId)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
index 4e8f866..7f0ea9a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt
@@ -17,34 +17,48 @@
package com.android.systemui.controls.management
+import android.content.Context
import android.content.DialogInterface
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
class PanelConfirmationDialogFactoryTest : SysuiTestCase() {
+ @Mock private lateinit var mockDialog : SystemUIDialog
+ @Mock private lateinit var mockDialogFactory : SystemUIDialog.Factory
+ private lateinit var factory : PanelConfirmationDialogFactory
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(mockDialogFactory.create(any(Context::class.java))).thenReturn(mockDialog)
+ whenever(mockDialog.context).thenReturn(mContext)
+ factory = PanelConfirmationDialogFactory(mockDialogFactory)
+ }
+
@Test
fun testDialogHasCorrectInfo() {
- val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
- val factory = PanelConfirmationDialogFactory { mockDialog }
val appName = "appName"
- factory.createConfirmationDialog(context, appName) {}
+ factory.createConfirmationDialog(mContext, appName) {}
verify(mockDialog).setCanceledOnTouchOutside(true)
verify(mockDialog)
@@ -55,12 +69,9 @@
@Test
fun testDialogPositiveButton() {
- val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
- val factory = PanelConfirmationDialogFactory { mockDialog }
-
var response: Boolean? = null
- factory.createConfirmationDialog(context, "") { response = it }
+ factory.createConfirmationDialog(mContext,"") { response = it }
val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
verify(mockDialog).setPositiveButton(eq(R.string.controls_dialog_ok), capture(captor))
@@ -72,12 +83,9 @@
@Test
fun testDialogNeutralButton() {
- val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
- val factory = PanelConfirmationDialogFactory { mockDialog }
-
var response: Boolean? = null
- factory.createConfirmationDialog(context, "") { response = it }
+ factory.createConfirmationDialog(mContext, "") { response = it }
val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor()
verify(mockDialog).setNeutralButton(eq(R.string.cancel), capture(captor))
@@ -89,12 +97,9 @@
@Test
fun testDialogCancel() {
- val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) }
- val factory = PanelConfirmationDialogFactory { mockDialog }
-
var response: Boolean? = null
- factory.createConfirmationDialog(context, "") { response = it }
+ factory.createConfirmationDialog(mContext, "") { response = it }
val captor: ArgumentCaptor<DialogInterface.OnCancelListener> = argumentCaptor()
verify(mockDialog).setOnCancelListener(capture(captor))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt
index 8eebcee..38c6a0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt
@@ -17,17 +17,23 @@
package com.android.systemui.controls.ui
+import android.content.Context
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.FakeSystemUIDialogController
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
import org.mockito.Mockito.eq
import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -37,18 +43,24 @@
const val APP_NAME = "Test App"
}
- private val fakeDialogController = FakeSystemUIDialogController()
+ @Mock
+ private lateinit var mockDialogFactory : SystemUIDialog.Factory
+
+ private val fakeDialogController = FakeSystemUIDialogController(mContext)
private lateinit var underTest: ControlsDialogsFactory
@Before
fun setup() {
- underTest = ControlsDialogsFactory { fakeDialogController.dialog }
+ MockitoAnnotations.initMocks(this)
+ whenever(mockDialogFactory.create(any(Context::class.java)))
+ .thenReturn(fakeDialogController.dialog)
+ underTest = ControlsDialogsFactory(mockDialogFactory)
}
@Test
fun testCreatesRemoveAppDialog() {
- val dialog = underTest.createRemoveAppDialog(context, APP_NAME) {}
+ val dialog = underTest.createRemoveAppDialog(mContext, APP_NAME) {}
verify(dialog)
.setTitle(
@@ -60,7 +72,7 @@
@Test
fun testPositiveClickRemoveAppDialogWorks() {
var dialogResult: Boolean? = null
- underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+ underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it }
fakeDialogController.clickPositive()
@@ -70,7 +82,7 @@
@Test
fun testNeutralClickRemoveAppDialogWorks() {
var dialogResult: Boolean? = null
- underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+ underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it }
fakeDialogController.clickNeutral()
@@ -80,7 +92,7 @@
@Test
fun testCancelRemoveAppDialogWorks() {
var dialogResult: Boolean? = null
- underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it }
+ underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it }
fakeDialogController.cancel()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
index 11bd9cb..36ae0c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -51,6 +51,7 @@
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.FakeSystemUIDialogController
import com.android.systemui.util.concurrency.FakeExecutor
@@ -97,9 +98,10 @@
@Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
@Mock lateinit var featureFlags: FeatureFlags
@Mock lateinit var packageManager: PackageManager
+ @Mock lateinit var systemUIDialogFactory: SystemUIDialog.Factory
private val preferredPanelRepository = FakeSelectedComponentRepository()
- private val fakeDialogController = FakeSystemUIDialogController()
+ private lateinit var fakeDialogController: FakeSystemUIDialogController
private val uiExecutor = FakeExecutor(FakeSystemClock())
private val bgExecutor = FakeExecutor(FakeSystemClock())
@@ -114,6 +116,9 @@
fun setup() {
MockitoAnnotations.initMocks(this)
+ fakeDialogController = FakeSystemUIDialogController(mContext)
+ whenever(systemUIDialogFactory.create(any(Context::class.java)))
+ .thenReturn(fakeDialogController.dialog)
controlsSettingsRepository = FakeControlsSettingsRepository()
// This way, it won't be cloned every time `LayoutInflater.fromContext` is called, but we
@@ -146,10 +151,7 @@
authorizedPanelsRepository,
preferredPanelRepository,
featureFlags,
- ControlsDialogsFactory {
- isRemoveAppDialogCreated = true
- fakeDialogController.dialog
- },
+ ControlsDialogsFactory(systemUIDialogFactory),
dumpManager,
)
`when`(userTracker.userId).thenReturn(0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
index ab6bc2c..66fdf53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
@@ -19,7 +19,6 @@
import android.os.VibrationAttributes
import android.os.VibrationEffect
import android.view.VelocityTracker
-import android.view.animation.AccelerateInterpolator
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -51,8 +50,6 @@
private val lowTickDuration = 12 // Mocked duration of a low tick
private val dragTextureThresholdMillis =
lowTickDuration * config.numberOfLowTicks + config.deltaMillisForDragInterval
- private val progressInterpolator = AccelerateInterpolator(config.progressInterpolatorFactor)
- private val velocityInterpolator = AccelerateInterpolator(config.velocityInterpolatorFactor)
private lateinit var sliderHapticFeedbackProvider: SliderHapticFeedbackProvider
@Before
@@ -60,7 +57,9 @@
MockitoAnnotations.initMocks(this)
whenever(vibratorHelper.getPrimitiveDurations(any()))
.thenReturn(intArrayOf(lowTickDuration))
- whenever(velocityTracker.xVelocity).thenReturn(config.maxVelocityToScale)
+ whenever(velocityTracker.isAxisSupported(config.velocityAxis)).thenReturn(true)
+ whenever(velocityTracker.getAxisVelocity(config.velocityAxis))
+ .thenReturn(config.maxVelocityToScale)
sliderHapticFeedbackProvider =
SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, config, clock)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
index d421004..1b4573d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
@@ -34,6 +34,7 @@
import com.android.systemui.plugins.clocks.ClockFaceConfig
import com.android.systemui.plugins.clocks.ClockFaceController
import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
@@ -66,6 +67,8 @@
@Mock private lateinit var largeClock: ClockFaceController
@Mock private lateinit var clockFaceConfig: ClockFaceConfig
@Mock private lateinit var eventController: ClockEventController
+ @Mock private lateinit var splitShadeStateController: SplitShadeStateController
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
@@ -92,6 +95,7 @@
keyguardInteractor,
keyguardClockInteractor,
scope.backgroundScope,
+ splitShadeStateController,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
index 6248bb1..1a303b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
@@ -55,6 +55,7 @@
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.util.NotificationChannels;
import com.android.systemui.util.settings.FakeGlobalSettings;
@@ -77,7 +78,6 @@
public static final String FORMATTED_45M = "0h 45m";
public static final String FORMATTED_HOUR = "1h 0m";
private final NotificationManager mMockNotificationManager = mock(NotificationManager.class);
- private final GlobalSettings mGlobalSettings = new FakeGlobalSettings();
private PowerNotificationWarnings mPowerNotificationWarnings;
@Mock
@@ -90,6 +90,10 @@
private UserTracker mUserTracker;
@Mock
private View mView;
+ @Mock
+ private SystemUIDialog.Factory mSystemUIDialogFactory;
+ @Mock
+ private SystemUIDialog mSystemUIDialog;
private BroadcastReceiver mReceiver;
@@ -113,9 +117,16 @@
when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
when(mUserTracker.getUserHandle()).thenReturn(
UserHandle.of(ActivityManager.getCurrentUser()));
- mPowerNotificationWarnings = new PowerNotificationWarnings(wrapper, starter,
- broadcastSender, () -> mBatteryController, mDialogLaunchAnimator, mUiEventLogger,
- mGlobalSettings, mUserTracker);
+ when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog);
+ mPowerNotificationWarnings = new PowerNotificationWarnings(
+ wrapper,
+ starter,
+ broadcastSender,
+ () -> mBatteryController,
+ mDialogLaunchAnimator,
+ mUiEventLogger,
+ mUserTracker,
+ mSystemUIDialogFactory);
BatteryStateSnapshot snapshot = new BatteryStateSnapshot(100, false, false, 1,
BatteryManager.BATTERY_HEALTH_GOOD, 5, 15);
mPowerNotificationWarnings.updateSnapshot(snapshot);
@@ -251,7 +262,7 @@
verify(mDialogLaunchAnimator, never()).showFromView(any(), any());
- assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue();
+ verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show();
mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss();
}
@@ -266,7 +277,7 @@
verify(mDialogLaunchAnimator, never()).showFromView(any(), any());
- assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue();
+ verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show();
mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss();
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
index f5a3bec..698868d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
@@ -27,6 +27,7 @@
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.app.IActivityManager;
import android.app.IForegroundServiceObserver;
@@ -53,6 +54,7 @@
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.DeviceConfigProxyFake;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
@@ -95,6 +97,10 @@
BroadcastDispatcher mBroadcastDispatcher;
@Mock
DumpManager mDumpManager;
+ @Mock
+ SystemUIDialog.Factory mSystemUIDialogFactory;
+ @Mock
+ SystemUIDialog mSystemUIDialog;
private FgsManagerController mFmc;
@@ -114,6 +120,7 @@
mSystemClock = new FakeSystemClock();
mMainExecutor = new FakeExecutor(mSystemClock);
mBackgroundExecutor = new FakeExecutor(mSystemClock);
+ when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog);
mUserProfiles = new ArrayList<>();
Mockito.doReturn(mUserProfiles).when(mUserTracker).getUserProfiles();
@@ -325,7 +332,8 @@
mDeviceConfigProxyFake,
mDialogLaunchAnimator,
mBroadcastDispatcher,
- mDumpManager
+ mDumpManager,
+ mSystemUIDialogFactory
);
fmc.init();
Assert.assertTrue(fmc.getIncludesUserVisibleJobs());
@@ -351,7 +359,8 @@
mDeviceConfigProxyFake,
mDialogLaunchAnimator,
mBroadcastDispatcher,
- mDumpManager
+ mDumpManager,
+ mSystemUIDialogFactory
);
fmc.init();
Assert.assertFalse(fmc.getIncludesUserVisibleJobs());
@@ -457,7 +466,8 @@
mDeviceConfigProxyFake,
mDialogLaunchAnimator,
mBroadcastDispatcher,
- mDumpManager
+ mDumpManager,
+ mSystemUIDialogFactory
);
result.init();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
index 51e95be..c109a1e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
@@ -32,7 +32,9 @@
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.policy.DataSaverController
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -49,8 +51,6 @@
@Mock private lateinit var mHost: QSHost
@Mock private lateinit var mMetricsLogger: MetricsLogger
- @Mock private lateinit var mStatusBarStateController: StatusBarStateController
- @Mock private lateinit var mActivityStarter: ActivityStarter
@Mock private lateinit var mQsLogger: QSLogger
private val falsingManager = FalsingManagerFake()
@Mock private lateinit var statusBarStateController: StatusBarStateController
@@ -58,6 +58,8 @@
@Mock private lateinit var dataSaverController: DataSaverController
@Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
@Mock private lateinit var uiEventLogger: QsEventLogger
+ @Mock private lateinit var systemUIDialogFactory: SystemUIDialog.Factory
+ @Mock private lateinit var systemUIDialog: SystemUIDialog
private lateinit var testableLooper: TestableLooper
private lateinit var tile: DataSaverTile
@@ -67,7 +69,8 @@
MockitoAnnotations.initMocks(this)
testableLooper = TestableLooper.get(this)
- Mockito.`when`(mHost.context).thenReturn(mContext)
+ whenever(mHost.context).thenReturn(mContext)
+ whenever(systemUIDialogFactory.create()).thenReturn(systemUIDialog)
tile =
DataSaverTile(
@@ -81,7 +84,8 @@
activityStarter,
mQsLogger,
dataSaverController,
- dialogLaunchAnimator
+ dialogLaunchAnimator,
+ systemUIDialogFactory
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
index 0a34810..945490f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
@@ -36,6 +36,7 @@
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -56,6 +57,8 @@
class UserSwitchDialogControllerTest : SysuiTestCase() {
@Mock
+ private lateinit var dialogFactory: SystemUIDialog.Factory
+ @Mock
private lateinit var dialog: SystemUIDialog
@Mock
private lateinit var falsingManager: FalsingManager
@@ -80,7 +83,8 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
- `when`(dialog.context).thenReturn(mContext)
+ whenever(dialog.context).thenReturn(mContext)
+ whenever(dialogFactory.create()).thenReturn(dialog)
controller = UserSwitchDialogController(
{ userDetailViewAdapter },
@@ -88,7 +92,7 @@
falsingManager,
dialogLaunchAnimator,
uiEventLogger,
- { dialog }
+ dialogFactory
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
index 273ce85..35bf775 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java
@@ -18,25 +18,42 @@
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotSame;
-import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.hardware.devicestate.DeviceStateManager;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.view.LayoutInflater;
+import android.view.View;
import android.widget.TextView;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.res.R;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.model.SysUiState;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -45,24 +62,49 @@
@Mock
private CommandQueue mCommandQueue;
+ @Mock
+ private SystemUIDialog.Factory mSystemUIDialogFactory;
+ @Mock
+ private SystemUIDialog mSystemUIDialog;
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+ @Mock
+ private SysUiState mSysUiState;
+ @Mock
+ private Resources mResources;
- private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+ LayoutInflater mLayoutInflater = LayoutInflater.from(mContext);
+ private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
private static final int CLOSED_BASE_STATE = 0;
private static final int OPEN_BASE_STATE = 1;
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true);
+ when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
+ when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog);
+ when(mSystemUIDialog.getContext()).thenReturn(mContext);
+ }
@Test
public void testClosedDialogIsShown() {
- RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
- mCommandQueue, mFakeExecutor);
+ RearDisplayDialogController controller = new RearDisplayDialogController(
+ mCommandQueue,
+ mFakeExecutor,
+ mResources,
+ mLayoutInflater,
+ mSystemUIDialogFactory);
controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
controller.setFoldedStates(new int[]{0});
controller.setAnimationRepeatCount(0);
controller.showRearDisplayDialog(CLOSED_BASE_STATE);
- assertTrue(controller.mRearDisplayEducationDialog.isShowing());
- TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+ verify(mSystemUIDialog).show();
+
+ View container = getDialogViewContainer();
+ TextView deviceClosedTitleTextView = container.findViewById(
R.id.rear_display_title_text_view);
assertEquals(deviceClosedTitleTextView.getText().toString(),
getContext().getResources().getString(
@@ -71,20 +113,28 @@
@Test
public void testClosedDialogIsRefreshedOnConfigurationChange() {
- RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
- mCommandQueue, mFakeExecutor);
+ RearDisplayDialogController controller = new RearDisplayDialogController(
+ mCommandQueue,
+ mFakeExecutor,
+ mResources,
+ mLayoutInflater,
+ mSystemUIDialogFactory);
controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
controller.setFoldedStates(new int[]{0});
controller.setAnimationRepeatCount(0);
controller.showRearDisplayDialog(CLOSED_BASE_STATE);
- assertTrue(controller.mRearDisplayEducationDialog.isShowing());
- TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+ verify(mSystemUIDialog).show();
+ View container = getDialogViewContainer();
+ TextView deviceClosedTitleTextView = container.findViewById(
R.id.rear_display_title_text_view);
+ reset(mSystemUIDialog);
+ when(mSystemUIDialog.isShowing()).thenReturn(true);
+ when(mSystemUIDialog.getContext()).thenReturn(mContext);
+
controller.onConfigChanged(new Configuration());
- assertTrue(controller.mRearDisplayEducationDialog.isShowing());
- TextView deviceClosedTitleTextView2 = controller.mRearDisplayEducationDialog.findViewById(
+ TextView deviceClosedTitleTextView2 = container.findViewById(
R.id.rear_display_title_text_view);
assertNotSame(deviceClosedTitleTextView, deviceClosedTitleTextView2);
@@ -92,22 +142,33 @@
@Test
public void testOpenDialogIsShown() {
- RearDisplayDialogController controller = new RearDisplayDialogController(mContext,
- mCommandQueue, mFakeExecutor);
+ RearDisplayDialogController controller = new RearDisplayDialogController(
+ mCommandQueue,
+ mFakeExecutor,
+ mResources,
+ mLayoutInflater,
+ mSystemUIDialogFactory);
controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback());
controller.setFoldedStates(new int[]{0});
controller.setAnimationRepeatCount(0);
controller.showRearDisplayDialog(OPEN_BASE_STATE);
- assertTrue(controller.mRearDisplayEducationDialog.isShowing());
- TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById(
+ verify(mSystemUIDialog).show();
+ View container = getDialogViewContainer();
+ TextView deviceClosedTitleTextView = container.findViewById(
R.id.rear_display_title_text_view);
assertEquals(deviceClosedTitleTextView.getText().toString(),
getContext().getResources().getString(
R.string.rear_display_unfolded_bottom_sheet_title));
}
+ private View getDialogViewContainer() {
+ ArgumentCaptor<View> viewCaptor = ArgumentCaptor.forClass(View.class);
+ verify(mSystemUIDialog).setView(viewCaptor.capture());
+
+ return viewCaptor.getValue();
+ }
/**
* Empty device state manager callbacks, so we can verify that the correct
* dialogs are being created regardless of device state of the test device.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 48baeb3..2220756 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -341,7 +341,7 @@
@Mock private JavaAdapter mJavaAdapter;
@Mock private CastController mCastController;
@Mock private SharedNotificationContainerInteractor mSharedNotificationContainerInteractor;
- @Mock private ActiveNotificationsInteractor mActiveNotificationsInteractor;
+ @Mock protected ActiveNotificationsInteractor mActiveNotificationsInteractor;
@Mock private KeyguardClockPositionAlgorithm mKeyguardClockPositionAlgorithm;
@Mock private NaturalScrollingSettingObserver mNaturalScrollingSettingObserver;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 28fe8e4..3cbb9bb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -456,11 +456,13 @@
enableSplitShade(/* enabled= */ true);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
mNotificationPanelViewController.updateResources();
assertThat(getConstraintSetLayout(R.id.keyguard_status_view).endToEnd)
.isEqualTo(R.id.qs_edge_guideline);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false);
mNotificationPanelViewController.updateResources();
assertThat(getConstraintSetLayout(R.id.keyguard_status_view).endToEnd)
.isEqualTo(ConstraintSet.PARENT_ID);
@@ -469,6 +471,7 @@
@Test
public void keyguardStatusView_splitShade_dozing_alwaysDozingOn_isCentered() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -480,6 +483,7 @@
@Test
public void keyguardStatusView_splitShade_dozing_alwaysDozingOff_isNotCentered() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -491,6 +495,7 @@
@Test
public void keyguardStatusView_splitShade_notDozing_alwaysDozingOn_isNotCentered() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -502,6 +507,7 @@
@Test
public void keyguardStatusView_splitShade_pulsing_isNotCentered() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
when(mNotificationListContainer.hasPulsingNotifications()).thenReturn(true);
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -514,6 +520,7 @@
@Test
public void keyguardStatusView_splitShade_notPulsing_isNotCentered() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
when(mNotificationListContainer.hasPulsingNotifications()).thenReturn(false);
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -529,6 +536,7 @@
// The conditions below would make the clock NOT be centered on split shade.
// On single shade it should always be centered though.
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
when(mNotificationListContainer.hasPulsingNotifications()).thenReturn(false);
mStatusBarStateController.setState(KEYGUARD);
setDozing(/* dozing= */ false, /* dozingAlwaysOn= */ false);
@@ -539,6 +547,7 @@
@Test
public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenNot() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -553,6 +562,7 @@
@Test
public void keyguardStatusView_willPlayDelayedDoze_notifiesKeyguardMediaController() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -564,6 +574,7 @@
@Test
public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenStillCenteredIfNoNotifs() {
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false);
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -700,10 +711,12 @@
mStatusBarStateController.setState(KEYGUARD);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false);
triggerPositionClockAndNotifications();
verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
triggerPositionClockAndNotifications();
verify(mKeyguardStatusViewController).displayClock(SMALL, /* animate */ true);
}
@@ -715,10 +728,12 @@
clearInvocations(mKeyguardStatusViewController);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false);
triggerPositionClockAndNotifications();
verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
triggerPositionClockAndNotifications();
verify(mKeyguardStatusViewController, times(2))
.displayClock(LARGE, /* animate */ true);
@@ -730,6 +745,7 @@
public void testHasNotifications_switchesToLargeClockWhenEnteringSplitShade() {
mStatusBarStateController.setState(KEYGUARD);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
enableSplitShade(/* enabled= */ true);
@@ -740,6 +756,7 @@
public void testNoNotifications_switchesToLargeClockWhenEnteringSplitShade() {
mStatusBarStateController.setState(KEYGUARD);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false);
enableSplitShade(/* enabled= */ true);
@@ -752,6 +769,7 @@
enableSplitShade(/* enabled= */ true);
clearInvocations(mKeyguardStatusViewController);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
enableSplitShade(/* enabled= */ false);
@@ -764,6 +782,7 @@
enableSplitShade(/* enabled= */ true);
clearInvocations(mKeyguardStatusViewController);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false);
enableSplitShade(/* enabled= */ false);
@@ -777,6 +796,7 @@
enableSplitShade(/* enabled= */ true);
when(mMediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
clearInvocations(mKeyguardStatusViewController);
mNotificationPanelViewController.setDozing(/* dozing= */ true, /* animate= */ false);
@@ -791,6 +811,7 @@
enableSplitShade(/* enabled= */ true);
when(mMediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
clearInvocations(mKeyguardStatusViewController);
mNotificationPanelViewController.setDozing(/* dozing= */ true, /* animate= */ false);
@@ -847,6 +868,7 @@
clearInvocations(mKeyguardStatusViewController);
when(mMediaDataManager.hasActiveMedia()).thenReturn(true);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
mNotificationPanelViewController.setDozing(true, false);
@@ -863,6 +885,7 @@
when(mResources.getBoolean(R.bool.force_small_clock_on_lockscreen)).thenReturn(true);
when(mMediaDataManager.hasActiveMedia()).thenReturn(false);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false);
clearInvocations(mKeyguardStatusViewController);
enableSplitShade(/* enabled= */ true);
@@ -881,6 +904,7 @@
when(mResources.getBoolean(R.bool.force_small_clock_on_lockscreen)).thenReturn(true);
when(mMediaDataManager.hasActiveMedia()).thenReturn(false);
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false);
clearInvocations(mKeyguardStatusViewController);
enableSplitShade(/* enabled= */ true);
@@ -898,11 +922,13 @@
// one notification + media player visible
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
triggerPositionClockAndNotifications();
verify(mKeyguardStatusViewController).displayClock(SMALL, /* animate */ true);
// only media player visible
when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
+ when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false);
triggerPositionClockAndNotifications();
verify(mKeyguardStatusViewController, times(2)).displayClock(SMALL, true);
verify(mKeyguardStatusViewController, never()).displayClock(LARGE, /* animate */ true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 0c6f456..757f16c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -598,6 +598,17 @@
@Test
@EnableFlags(FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS)
+ public void testEarlyUserSwitch() {
+ mLockscreenUserManager =
+ new TestNotificationLockscreenUserManager(mContext);
+ mBackgroundExecutor.runAllReady();
+ mLockscreenUserManager.mUserChangedCallback.onUserChanging(
+ mCurrentUser.id, mContext);
+ // no crash!
+ }
+
+ @Test
+ @EnableFlags(FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS)
public void testKeyguardManager_noPrivateNotifications() {
Mockito.clearInvocations(mDevicePolicyManager);
// User allows notifications
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
index 255cf6f..9b4a100 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
@@ -31,6 +31,7 @@
import com.android.systemui.statusbar.notification.collection.render.NotifStats
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
@@ -80,7 +81,7 @@
}
@Test
- @DisableFlags(NotificationIconContainerRefactor.FLAG_NAME)
+ @DisableFlags(NotificationIconContainerRefactor.FLAG_NAME, FooterViewRefactor.FLAG_NAME)
fun testUpdateNotificationIcons() {
afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
verify(notificationIconAreaController).updateNotificationIcons(eq(listOf(entry)))
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
index 29e737e..d23dae9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -33,6 +33,7 @@
import android.testing.TestableLooper;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.core.animation.AndroidXAnimatorIsolationRule;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
@@ -68,8 +69,20 @@
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
@Rule
- public SysuiTestableContext mContext = new SysuiTestableContext(
- InstrumentationRegistry.getContext(), getLeakCheck());
+ public SysuiTestableContext mContext = createTestableContext();
+
+ @NonNull
+ private SysuiTestableContext createTestableContext() {
+ SysuiTestableContext context = new SysuiTestableContext(
+ InstrumentationRegistry.getContext(), getLeakCheck());
+ if (isRobolectricTest()) {
+ // Manually associate a Display to context for Robolectric test. Similar to b/214297409
+ return context.createDefaultDisplayContext();
+ } else {
+ return context;
+ }
+ }
+
@Rule
public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
new DexmakerShareClassLoaderRule();
@@ -84,10 +97,6 @@
@Before
public void SysuiSetup() throws Exception {
- // Manually associate a Display to context for Robolectric test. Similar to b/214297409
- if (isRobolectricTest()) {
- mContext = mContext.createDefaultDisplayContext();
- }
mSysuiDependency = new SysuiTestDependency(mContext, shouldFailOnLeakedReceiver());
mDependency = mSysuiDependency.install();
mRealInstrumentation = InstrumentationRegistry.getInstrumentation();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
index d878683..5ca0439 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
@@ -20,6 +20,7 @@
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.splitShadeStateController
val Kosmos.keyguardClockViewModel by
Kosmos.Fixture {
@@ -27,5 +28,6 @@
keyguardInteractor = keyguardInteractor,
keyguardClockInteractor = keyguardClockInteractor,
applicationScope = applicationCoroutineScope,
+ splitShadeStateController = splitShadeStateController,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt
index 0c9ce0f..697b508 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt
@@ -17,6 +17,7 @@
package com.android.systemui.util
+import android.content.Context
import android.content.DialogInterface
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.mockito.any
@@ -27,13 +28,15 @@
import org.mockito.Mockito.verify
import org.mockito.stubbing.Stubber
-class FakeSystemUIDialogController {
+class FakeSystemUIDialogController(context: Context) {
val dialog: SystemUIDialog = mock()
+
private val clickListeners: MutableMap<Int, DialogInterface.OnClickListener> = mutableMapOf()
init {
+ whenever(dialog.context).thenReturn(context)
saveListener(DialogInterface.BUTTON_POSITIVE)
.whenever(dialog)
.setPositiveButton(any(), any())
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index a19920f..993b254 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -59,13 +59,6 @@
}
flag {
- name: "reduce_touch_exploration_sensitivity"
- namespace: "accessibility"
- description: "Reduces touch exploration sensitivity by only sending a hover event when the ifnger has moved the amount of pixels defined by the system's touch slop."
- bug: "303677860"
-}
-
-flag {
name: "scan_packages_without_lock"
namespace: "accessibility"
description: "Scans packages for accessibility service/activity info without holding the A11yMS lock"
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index fc8d4f8..c418485 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -882,22 +882,10 @@
final int pointerIndex = event.findPointerIndex(pointerId);
switch (event.getPointerCount()) {
case 1:
- // Touch exploration.
+ // Touch exploration.
sendTouchExplorationGestureStartAndHoverEnterIfNeeded(policyFlags);
- if (Flags.reduceTouchExplorationSensitivity()
- && mState.getLastInjectedHoverEvent() != null) {
- final MotionEvent lastEvent = mState.getLastInjectedHoverEvent();
- final float deltaX = lastEvent.getX() - rawEvent.getX();
- final float deltaY = lastEvent.getY() - rawEvent.getY();
- final double moveDelta = Math.hypot(deltaX, deltaY);
- if (moveDelta > mTouchSlop) {
- mDispatcher.sendMotionEvent(
- event, ACTION_HOVER_MOVE, rawEvent, pointerIdBits, policyFlags);
- }
- } else {
- mDispatcher.sendMotionEvent(
- event, ACTION_HOVER_MOVE, rawEvent, pointerIdBits, policyFlags);
- }
+ mDispatcher.sendMotionEvent(
+ event, ACTION_HOVER_MOVE, rawEvent, pointerIdBits, policyFlags);
break;
case 2:
if (mGestureDetector.isMultiFingerGesturesEnabled()
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index 52a8f9e..a6ed846 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -286,32 +286,33 @@
selfManaged, /* notifyOnDeviceNearby */ false, /* revoked */ false,
timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0);
- if (deviceProfile != null) {
- // If the "Device Profile" is specified, make the companion application a holder of the
- // corresponding role.
- addRoleHolderForAssociation(mService.getContext(), association, success -> {
- if (success) {
- addAssociationToStore(association, deviceProfile);
-
- sendCallbackAndFinish(association, callback, resultReceiver);
- } else {
- Slog.e(TAG, "Failed to add u" + userId + "\\" + packageName
- + " to the list of " + deviceProfile + " holders.");
-
- sendCallbackAndFinish(null, callback, resultReceiver);
- }
- });
- } else {
- addAssociationToStore(association, null);
-
- sendCallbackAndFinish(association, callback, resultReceiver);
- }
+ // Add role holder for association (if specified) and add new association to store.
+ maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver);
// Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since
// maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case
// that there are other devices with the same profile, so the role holder won't be removed.
}
+ public void maybeGrantRoleAndStoreAssociation(@NonNull AssociationInfo association,
+ @Nullable IAssociationRequestCallback callback,
+ @Nullable ResultReceiver resultReceiver) {
+ // If the "Device Profile" is specified, make the companion application a holder of the
+ // corresponding role.
+ // If it is null, then the operation will succeed without granting any role.
+ addRoleHolderForAssociation(mService.getContext(), association, success -> {
+ if (success) {
+ addAssociationToStore(association);
+ sendCallbackAndFinish(association, callback, resultReceiver);
+ } else {
+ Slog.e(TAG, "Failed to add u" + association.getUserId()
+ + "\\" + association.getPackageName()
+ + " to the list of " + association.getDeviceProfile() + " holders.");
+ sendCallbackAndFinish(null, callback, resultReceiver);
+ }
+ });
+ }
+
public void enableSystemDataSync(int associationId, int flags) {
AssociationInfo association = mAssociationStore.getAssociationById(associationId);
AssociationInfo updated = (new AssociationInfo.Builder(association))
@@ -326,15 +327,14 @@
mAssociationStore.updateAssociation(updated);
}
- private void addAssociationToStore(@NonNull AssociationInfo association,
- @Nullable String deviceProfile) {
+ private void addAssociationToStore(@NonNull AssociationInfo association) {
Slog.i(TAG, "New CDM association created=" + association);
mAssociationStore.addAssociation(association);
mService.updateSpecialAccessPermissionForAssociatedPackage(association);
- logCreateAssociation(deviceProfile);
+ logCreateAssociation(association.getDeviceProfile());
}
private void sendCallbackAndFinish(@Nullable AssociationInfo association,
diff --git a/services/companion/java/com/android/server/companion/RolesUtils.java b/services/companion/java/com/android/server/companion/RolesUtils.java
index 163f614..af9d2d7 100644
--- a/services/companion/java/com/android/server/companion/RolesUtils.java
+++ b/services/companion/java/com/android/server/companion/RolesUtils.java
@@ -47,6 +47,17 @@
return roleHolders.contains(packageName);
}
+ /**
+ * Attempt to add the association's companion app as the role holder for the device profile
+ * specified in the association. If the association does not have any device profile specified,
+ * then the operation will always be successful as a no-op.
+ *
+ * @param context
+ * @param associationInfo the association for which the role should be granted to the app
+ * @param roleGrantResult the result callback for adding role holder. True if successful, and
+ * false if failed. If the association does not have any device profile
+ * specified, then the operation will always be successful as a no-op.
+ */
static void addRoleHolderForAssociation(
@NonNull Context context, @NonNull AssociationInfo associationInfo,
@NonNull Consumer<Boolean> roleGrantResult) {
@@ -55,7 +66,11 @@
}
final String deviceProfile = associationInfo.getDeviceProfile();
- if (deviceProfile == null) return;
+ if (deviceProfile == null) {
+ // If no device profile is specified, then no-op and resolve callback with success.
+ roleGrantResult.accept(true);
+ return;
+ }
final RoleManager roleManager = context.getSystemService(RoleManager.class);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 0185c22..b20ed7c 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -163,7 +163,6 @@
"pixel_audio_android",
"pixel_bluetooth",
"pixel_system_sw_touch",
- "pixel_system_sw_usb",
"pixel_watch",
"platform_security",
"power",
diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING
index 575db01..e90910a 100644
--- a/services/core/java/com/android/server/am/TEST_MAPPING
+++ b/services/core/java/com/android/server/am/TEST_MAPPING
@@ -146,6 +146,15 @@
{ "include-filter": "android.app.cts.ServiceTest" },
{ "include-filter": "android.app.cts.ActivityManagerFgsBgStartTest" }
]
+ },
+ {
+ "name": "CtsStatsdAtomHostTestCases",
+ "options": [
+ { "include-filter": "android.cts.statsdatom.appexit.AppExitHostTest" },
+ { "exclude-annotation": "androidx.test.filters.LargeTest" },
+ { "exclude-annotation": "androidx.test.filters.FlakyTest" },
+ { "exclude-annotation": "org.junit.Ignore" }
+ ]
}
]
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
index 0bb61415..90da74c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java
@@ -147,7 +147,7 @@
gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchCallback()));
mLockoutTracker = new LockoutFrameworkImpl(getContext(),
userId -> mLockoutResetDispatcher.notifyLockoutResetCallbacks(
- getSensorProperties().sensorId));
+ getSensorProperties().sensorId), getHandler());
}
@Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
index 2f77275..0e05a79 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.RESET_FINGERPRINT_LOCKOUT;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@@ -81,19 +82,30 @@
@NonNull LockoutResetCallback lockoutResetCallback) {
this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId,
new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId),
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE),
+ null /* handler */);
+ }
+
+ public LockoutFrameworkImpl(@NonNull Context context,
+ @NonNull LockoutResetCallback lockoutResetCallback,
+ @NonNull Handler handler) {
+ this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId,
+ new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId),
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE),
+ handler);
}
@VisibleForTesting
LockoutFrameworkImpl(@NonNull Context context,
@NonNull LockoutResetCallback lockoutResetCallback,
- @NonNull Function<Integer, PendingIntent> lockoutResetIntent) {
+ @NonNull Function<Integer, PendingIntent> lockoutResetIntent,
+ @Nullable Handler handler) {
mLockoutResetCallback = lockoutResetCallback;
mTimedLockoutCleared = new SparseBooleanArray();
mFailedAttempts = new SparseIntArray();
mAlarmManager = context.getSystemService(AlarmManager.class);
mLockoutReceiver = new LockoutReceiver();
- mHandler = new Handler(Looper.getMainLooper());
+ mHandler = handler == null ? new Handler(Looper.getMainLooper()) : handler;
mLockoutResetIntent = lockoutResetIntent;
context.registerReceiver(mLockoutReceiver, new IntentFilter(ACTION_LOCKOUT_RESET),
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 56a94ec0..49f6070 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -1424,7 +1424,11 @@
String defaultIme = Settings.Secure.getStringForUser(getContext().getContentResolver(),
Settings.Secure.DEFAULT_INPUT_METHOD, userId);
if (!TextUtils.isEmpty(defaultIme)) {
- final String imePkg = ComponentName.unflattenFromString(defaultIme).getPackageName();
+ final ComponentName imeComponent = ComponentName.unflattenFromString(defaultIme);
+ if (imeComponent == null) {
+ return false;
+ }
+ final String imePkg = imeComponent.getPackageName();
return imePkg.equals(packageName);
}
return false;
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 087c525..36dac83 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -48,6 +48,7 @@
import android.hardware.input.IInputManager;
import android.hardware.input.IInputSensorEventListener;
import android.hardware.input.IKeyboardBacklightListener;
+import android.hardware.input.IStickyModifierStateListener;
import android.hardware.input.ITabletModeChangedListener;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
@@ -319,6 +320,9 @@
// Manages Keyboard backlight
private final KeyboardBacklightControllerInterface mKeyboardBacklightController;
+ // Manages Sticky modifier state
+ private final StickyModifierStateController mStickyModifierStateController;
+
// Manages Keyboard modifier keys remapping
private final KeyRemapper mKeyRemapper;
@@ -464,6 +468,7 @@
? new KeyboardBacklightController(mContext, mNative, mDataStore,
injector.getLooper(), injector.getUEventManager())
: new KeyboardBacklightControllerInterface() {};
+ mStickyModifierStateController = new StickyModifierStateController();
mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
mUseDevInputEventForAudioJack =
@@ -2827,6 +2832,33 @@
yPosition)).sendToTarget();
}
+ @Override
+ @EnforcePermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ public void registerStickyModifierStateListener(
+ @NonNull IStickyModifierStateListener listener) {
+ super.registerStickyModifierStateListener_enforcePermission();
+ Objects.requireNonNull(listener);
+ mStickyModifierStateController.registerStickyModifierStateListener(listener,
+ Binder.getCallingPid());
+ }
+
+ @Override
+ @EnforcePermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ public void unregisterStickyModifierStateListener(
+ @NonNull IStickyModifierStateListener listener) {
+ super.unregisterStickyModifierStateListener_enforcePermission();
+ Objects.requireNonNull(listener);
+ mStickyModifierStateController.unregisterStickyModifierStateListener(listener,
+ Binder.getCallingPid());
+ }
+
+ // Native callback
+ @SuppressWarnings("unused")
+ void notifyStickyModifierStateChanged(int modifierState, int lockedModifierState) {
+ mStickyModifierStateController.notifyStickyModifierStateChanged(modifierState,
+ lockedModifierState);
+ }
+
// Native callback.
@SuppressWarnings("unused")
boolean isInputMethodConnectionActive() {
diff --git a/services/core/java/com/android/server/input/StickyModifierStateController.java b/services/core/java/com/android/server/input/StickyModifierStateController.java
new file mode 100644
index 0000000..5a22c10
--- /dev/null
+++ b/services/core/java/com/android/server/input/StickyModifierStateController.java
@@ -0,0 +1,133 @@
+/*
+ * 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.input;
+
+import android.annotation.BinderThread;
+import android.hardware.input.IStickyModifierStateListener;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * A thread-safe component of {@link InputManagerService} responsible for managing the sticky
+ * modifier state for A11y Sticky keys feature.
+ */
+final class StickyModifierStateController {
+
+ private static final String TAG = "ModifierStateController";
+
+ // To enable these logs, run:
+ // 'adb shell setprop log.tag.ModifierStateController DEBUG' (requires restart)
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ // List of currently registered sticky modifier state listeners
+ @GuardedBy("mStickyModifierStateListenerRecords")
+ private final SparseArray<StickyModifierStateListenerRecord>
+ mStickyModifierStateListenerRecords = new SparseArray<>();
+
+ public void notifyStickyModifierStateChanged(int modifierState, int lockedModifierState) {
+ if (DEBUG) {
+ Slog.d(TAG, "Sticky modifier state changed, modifierState = " + modifierState
+ + ", lockedModifierState = " + lockedModifierState);
+ }
+
+ synchronized (mStickyModifierStateListenerRecords) {
+ for (int i = 0; i < mStickyModifierStateListenerRecords.size(); i++) {
+ mStickyModifierStateListenerRecords.valueAt(i).notifyStickyModifierStateChanged(
+ modifierState, lockedModifierState);
+ }
+ }
+ }
+
+ /** Register the sticky modifier state listener for a process. */
+ @BinderThread
+ public void registerStickyModifierStateListener(IStickyModifierStateListener listener,
+ int pid) {
+ synchronized (mStickyModifierStateListenerRecords) {
+ if (mStickyModifierStateListenerRecords.get(pid) != null) {
+ throw new IllegalStateException("The calling process has already registered "
+ + "a StickyModifierStateListener.");
+ }
+ StickyModifierStateListenerRecord record = new StickyModifierStateListenerRecord(pid,
+ listener);
+ try {
+ listener.asBinder().linkToDeath(record, 0);
+ } catch (RemoteException ex) {
+ throw new RuntimeException(ex);
+ }
+ mStickyModifierStateListenerRecords.put(pid, record);
+ }
+ }
+
+ /** Unregister the sticky modifier state listener for a process. */
+ @BinderThread
+ public void unregisterStickyModifierStateListener(IStickyModifierStateListener listener,
+ int pid) {
+ synchronized (mStickyModifierStateListenerRecords) {
+ StickyModifierStateListenerRecord record = mStickyModifierStateListenerRecords.get(pid);
+ if (record == null) {
+ throw new IllegalStateException("The calling process has no registered "
+ + "StickyModifierStateListener.");
+ }
+ if (record.mListener.asBinder() != listener.asBinder()) {
+ throw new IllegalStateException("The calling process has a different registered "
+ + "StickyModifierStateListener.");
+ }
+ record.mListener.asBinder().unlinkToDeath(record, 0);
+ mStickyModifierStateListenerRecords.remove(pid);
+ }
+ }
+
+ private void onStickyModifierStateListenerDied(int pid) {
+ synchronized (mStickyModifierStateListenerRecords) {
+ mStickyModifierStateListenerRecords.remove(pid);
+ }
+ }
+
+ // A record of a registered sticky modifier state listener from one process.
+ private class StickyModifierStateListenerRecord implements IBinder.DeathRecipient {
+ public final int mPid;
+ public final IStickyModifierStateListener mListener;
+
+ StickyModifierStateListenerRecord(int pid, IStickyModifierStateListener listener) {
+ mPid = pid;
+ mListener = listener;
+ }
+
+ @Override
+ public void binderDied() {
+ if (DEBUG) {
+ Slog.d(TAG, "Sticky modifier state listener for pid " + mPid + " died.");
+ }
+ onStickyModifierStateListenerDied(mPid);
+ }
+
+ public void notifyStickyModifierStateChanged(int modifierState, int lockedModifierState) {
+ try {
+ mListener.onStickyModifierStateChanged(modifierState, lockedModifierState);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to notify process " + mPid
+ + " that sticky modifier state changed, assuming it died.", ex);
+ binderDied();
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
index 66807ae..f96bb8fb 100644
--- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
+++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
@@ -52,6 +52,7 @@
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
+import java.util.function.IntConsumer;
// TODO(b/210039666): See if we can make this class thread-safe.
final class HandwritingModeController {
@@ -84,14 +85,14 @@
private boolean mDelegatorFromDefaultHomePackage;
private Runnable mDelegationIdleTimeoutRunnable;
private Handler mDelegationIdleTimeoutHandler;
-
+ private IntConsumer mPointerToolTypeConsumer;
private HandwritingEventReceiverSurface mHandwritingSurface;
private int mCurrentRequestId;
@AnyThread
HandwritingModeController(Context context, Looper uiThreadLooper,
- Runnable inkWindowInitRunnable) {
+ Runnable inkWindowInitRunnable, IntConsumer toolTypeConsumer) {
mContext = context;
mLooper = uiThreadLooper;
mCurrentDisplayId = Display.INVALID_DISPLAY;
@@ -100,6 +101,7 @@
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mCurrentRequestId = 0;
mInkWindowInitRunnable = inkWindowInitRunnable;
+ mPointerToolTypeConsumer = toolTypeConsumer;
}
/**
@@ -355,6 +357,11 @@
return false;
}
final MotionEvent event = (MotionEvent) ev;
+ if (mPointerToolTypeConsumer != null && event.getAction() == MotionEvent.ACTION_DOWN) {
+ int toolType = event.getToolType(event.getActionIndex());
+ // notify IME of change in tool type.
+ mPointerToolTypeConsumer.accept(toolType);
+ }
if (!event.isStylusPointer()) {
return false;
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 4f998ee..24bcb4e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -124,6 +124,7 @@
import android.view.WindowManager.LayoutParams;
import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputConnection;
@@ -206,6 +207,7 @@
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.IntConsumer;
/**
* This class provides a system service that manages input methods.
@@ -1713,8 +1715,11 @@
com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
mNonPreemptibleInputMethods = mRes.getStringArray(
com.android.internal.R.array.config_nonPreemptibleInputMethods);
+ IntConsumer toolTypeConsumer =
+ Flags.useHandwritingListenerForTooltype()
+ ? toolType -> onUpdateEditorToolType(toolType) : null;
mHwController = new HandwritingModeController(mContext, thread.getLooper(),
- new InkWindowInitializer());
+ new InkWindowInitializer(), toolTypeConsumer);
registerDeviceListenerAndCheckStylusSupport();
}
@@ -1735,6 +1740,15 @@
}
}
+ private void onUpdateEditorToolType(int toolType) {
+ synchronized (ImfLock.class) {
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null) {
+ curMethod.updateEditorToolType(toolType);
+ }
+ }
+ }
+
@GuardedBy("ImfLock.class")
private void resetDefaultImeLocked(Context context) {
// Do not reset the default (current) IME when it is a 3rd-party IME
@@ -3525,7 +3539,8 @@
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME);
mCurStatsToken = null;
- if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
+ if (!Flags.useHandwritingListenerForTooltype()
+ && lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
curMethod.updateEditorToolType(lastClickToolType);
}
mVisibilityApplier.performShowIme(windowToken, statsToken,
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
index ae889d8..21e7bef 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
@@ -231,18 +231,21 @@
}
private boolean shouldBind() {
- if (mRunning) {
- boolean shouldBind =
- mLastDiscoveryPreference != null
- && !mLastDiscoveryPreference.getPreferredFeatures().isEmpty();
- if (mIsSelfScanOnlyProvider) {
- shouldBind &= mLastDiscoveryPreferenceIncludesThisPackage;
- }
- shouldBind |= mIsManagerScanning;
- shouldBind |= !getSessionInfos().isEmpty();
- return shouldBind;
+ if (!mRunning) {
+ return false;
}
- return false;
+ if (!getSessionInfos().isEmpty() || mIsManagerScanning) {
+ // We bind if any manager is scanning (regardless of whether an app is scanning) to give
+ // the opportunity for providers to publish routing sessions that were established
+ // directly between the app and the provider (typically via AndroidX MediaRouter). See
+ // b/176774510#comment20 for more information.
+ return true;
+ }
+ boolean anAppIsScanning =
+ mLastDiscoveryPreference != null
+ && !mLastDiscoveryPreference.getPreferredFeatures().isEmpty();
+ return anAppIsScanning
+ && (mLastDiscoveryPreferenceIncludesThisPackage || !mIsSelfScanOnlyProvider);
}
private void bind() {
diff --git a/services/core/java/com/android/server/pdb/TEST_MAPPING b/services/core/java/com/android/server/pdb/TEST_MAPPING
index 1aa8601..9e98023 100644
--- a/services/core/java/com/android/server/pdb/TEST_MAPPING
+++ b/services/core/java/com/android/server/pdb/TEST_MAPPING
@@ -1,5 +1,5 @@
{
- "postsubmit": [
+ "presubmit": [
{
"name": "FrameworksServicesTests",
"options": [
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 3cb2420..0555d90 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -78,6 +78,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.AuxiliaryResolveInfo;
import android.content.pm.ComponentInfo;
+import android.content.pm.Flags;
import android.content.pm.InstallSourceInfo;
import android.content.pm.InstantAppRequest;
import android.content.pm.InstantAppResolveInfo;
@@ -1511,6 +1512,13 @@
packageInfo.packageName = packageInfo.applicationInfo.packageName =
resolveExternalPackageName(p);
+ if (Flags.provideInfoOfApkInApex()) {
+ final String apexModuleName = ps.getApexModuleName();
+ if (apexModuleName != null) {
+ packageInfo.setApexPackageName(
+ mApexManager.getActivePackageNameForApexModuleName(apexModuleName));
+ }
+ }
return packageInfo;
} else if ((flags & (MATCH_UNINSTALLED_PACKAGES | MATCH_ARCHIVED_PACKAGES)) != 0
&& PackageUserStateUtils.isAvailable(state, flags)) {
diff --git a/services/core/java/com/android/server/pm/ModuleInfoProvider.java b/services/core/java/com/android/server/pm/ModuleInfoProvider.java
index 230f555..6561d46 100644
--- a/services/core/java/com/android/server/pm/ModuleInfoProvider.java
+++ b/services/core/java/com/android/server/pm/ModuleInfoProvider.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.content.Context;
+import android.content.pm.Flags;
import android.content.pm.IPackageManager;
import android.content.pm.ModuleInfo;
import android.content.pm.PackageInfo;
@@ -165,6 +166,10 @@
mi.setApexModuleName(
mApexManager.getApexModuleNameForPackageName(modulePackageName));
+ if (Flags.provideInfoOfApkInApex()) {
+ mi.setApkInApexPackageNames(mApexManager.getApksInApex(modulePackageName));
+ }
+
mModuleInfo.put(modulePackageName, mi);
}
} catch (XmlPullParserException | IOException e) {
diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java
index 7b5192c..e3aba0f 100644
--- a/services/core/java/com/android/server/utils/AnrTimer.java
+++ b/services/core/java/com/android/server/utils/AnrTimer.java
@@ -16,21 +16,30 @@
package com.android.server.utils;
+import static android.text.TextUtils.formatSimple;
+
import android.annotation.NonNull;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.os.Trace;
+import android.text.TextUtils;
import android.text.format.TimeMigrationUtils;
+import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.Keep;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.RingBuffer;
+import java.lang.ref.WeakReference;
import java.io.PrintWriter;
import java.util.Arrays;
+import java.util.ArrayList;
import java.util.Objects;
/**
@@ -60,9 +69,14 @@
* is restarted with the extension timeout. If extensions are disabled or if the extension is zero,
* the client process is notified of the expiration.
*
+ * <p>Instances use native resources but not system resources when the feature is enabled.
+ * Instances should be explicitly closed unless they are being closed as part of process
+ * exit. (So, instances in system server generally need not be explicitly closed since they are
+ * created during process start and will last until process exit.)
+ *
* @hide
*/
-public class AnrTimer<V> {
+public class AnrTimer<V> implements AutoCloseable {
/**
* The log tag.
@@ -87,6 +101,12 @@
private static final long TRACE_TAG = Trace.TRACE_TAG_ACTIVITY_MANAGER;
/**
+ * Enable tracing from the time a timer expires until it is accepted or discarded. This is
+ * used to diagnose long latencies in the client.
+ */
+ private static final boolean ENABLE_TRACING = false;
+
+ /**
* Return true if the feature is enabled. By default, the value is take from the Flags class
* but it can be changed for local testing.
*/
@@ -103,6 +123,9 @@
}
}
+ /** The default injector. */
+ private static final Injector sDefaultInjector = new Injector();
+
/**
* An error is defined by its issue, the operation that detected the error, the tag of the
* affected service, a short stack of the bad call, and the stringified arg associated with
@@ -160,41 +183,46 @@
/** A lock for the AnrTimer instance. */
private final Object mLock = new Object();
- /**
- * The total number of timers started.
- */
+ /** The map from client argument to the associated timer ID. */
+ @GuardedBy("mLock")
+ private final ArrayMap<V, Integer> mTimerIdMap = new ArrayMap<>();
+
+ /** Reverse map from timer ID to client argument. */
+ @GuardedBy("mLock")
+ private final SparseArray<V> mTimerArgMap = new SparseArray<>();
+
+ /** The highwater mark of started, but not closed, timers. */
+ @GuardedBy("mLock")
+ private int mMaxStarted = 0;
+
+ /** The total number of timers started. */
@GuardedBy("mLock")
private int mTotalStarted = 0;
- /**
- * The total number of errors detected.
- */
+ /** The total number of errors detected. */
@GuardedBy("mLock")
private int mTotalErrors = 0;
- /**
- * The handler for messages sent from this instance.
- */
+ /** The total number of timers that have expired. */
+ @GuardedBy("mLock")
+ private int mTotalExpired = 0;
+
+ /** The handler for messages sent from this instance. */
private final Handler mHandler;
- /**
- * The message type for messages sent from this interface.
- */
+ /** The message type for messages sent from this interface. */
private final int mWhat;
- /**
- * A label that identifies the AnrTimer associated with a Timer in log messages.
- */
+ /** A label that identifies the AnrTimer associated with a Timer in log messages. */
private final String mLabel;
- /**
- * Whether this timer instance supports extending timeouts.
- */
+ /** Whether this timer instance supports extending timeouts. */
private final boolean mExtend;
- /**
- * The top-level switch for the feature enabled or disabled.
- */
+ /** The injector used to create this instance. This is only used for testing. */
+ private final Injector mInjector;
+
+ /** The top-level switch for the feature enabled or disabled. */
private final FeatureSwitch mFeature;
/**
@@ -223,7 +251,27 @@
mWhat = what;
mLabel = label;
mExtend = extend;
- mFeature = new FeatureDisabled();
+ mInjector = injector;
+ boolean enabled = mInjector.anrTimerServiceEnabled() && nativeTimersSupported();
+ mFeature = createFeatureSwitch(enabled);
+ }
+
+ // Return the correct feature. FeatureEnabled is returned if and only if the feature is
+ // flag-enabled and if the native shadow was successfully created. Otherwise, FeatureDisabled
+ // is returned.
+ private FeatureSwitch createFeatureSwitch(boolean enabled) {
+ if (!enabled) {
+ return new FeatureDisabled();
+ } else {
+ try {
+ return new FeatureEnabled();
+ } catch (RuntimeException e) {
+ // Something went wrong in the native layer. Log the error and fall back on the
+ // feature-disabled logic.
+ Log.e(TAG, e.toString());
+ return new FeatureDisabled();
+ }
+ }
}
/**
@@ -245,7 +293,7 @@
* @param extend A flag to indicate if expired timers can be granted extensions.
*/
public AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend) {
- this(handler, what, label, extend, new Injector());
+ this(handler, what, label, extend, sDefaultInjector);
}
/**
@@ -272,19 +320,44 @@
}
/**
+ * Start a trace on the timer. The trace is laid down in the AnrTimerTrack.
+ */
+ private void traceBegin(int timerId, int pid, int uid, String what) {
+ if (ENABLE_TRACING) {
+ final String label = formatSimple("%s(%d,%d,%s)", what, pid, uid, mLabel);
+ final int cookie = timerId;
+ Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK, label, cookie);
+ }
+ }
+
+ /**
+ * End a trace on the timer.
+ */
+ private void traceEnd(int timerId) {
+ if (ENABLE_TRACING) {
+ final int cookie = timerId;
+ Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK, cookie);
+ }
+ }
+
+ /**
* The FeatureSwitch class provides a quick switch between feature-enabled behavior and
* feature-disabled behavior.
*/
private abstract class FeatureSwitch {
abstract void start(@NonNull V arg, int pid, int uid, long timeoutMs);
- abstract void cancel(@NonNull V arg);
+ abstract boolean cancel(@NonNull V arg);
- abstract void accept(@NonNull V arg);
+ abstract boolean accept(@NonNull V arg);
- abstract void discard(@NonNull V arg);
+ abstract boolean discard(@NonNull V arg);
abstract boolean enabled();
+
+ abstract void dump(PrintWriter pw, boolean verbose);
+
+ abstract void close();
}
/**
@@ -301,18 +374,21 @@
/** Cancel a timer by removing the message from the client's handler. */
@Override
- void cancel(@NonNull V arg) {
+ boolean cancel(@NonNull V arg) {
mHandler.removeMessages(mWhat, arg);
+ return true;
}
/** accept() is a no-op when the feature is disabled. */
@Override
- void accept(@NonNull V arg) {
+ boolean accept(@NonNull V arg) {
+ return true;
}
/** discard() is a no-op when the feature is disabled. */
@Override
- void discard(@NonNull V arg) {
+ boolean discard(@NonNull V arg) {
+ return true;
}
/** The feature is not enabled. */
@@ -320,12 +396,179 @@
boolean enabled() {
return false;
}
+
+ /** dump() is a no-op when the feature is disabled. */
+ @Override
+ void dump(PrintWriter pw, boolean verbose) {
+ }
+
+ /** close() is a no-op when the feature is disabled. */
+ @Override
+ void close() {
+ }
+ }
+
+ /**
+ * A static list of AnrTimer instances. The list is traversed by dumpsys. Only instances
+ * using native resources are included.
+ */
+ @GuardedBy("sAnrTimerList")
+ private static final LongSparseArray<WeakReference<AnrTimer>> sAnrTimerList =
+ new LongSparseArray<>();
+
+ /**
+ * The FeatureEnabled class enables the AnrTimer logic. It is used when the AnrTimer service
+ * is enabled via Flags.anrTimerServiceEnabled.
+ */
+ private class FeatureEnabled extends FeatureSwitch {
+
+ /**
+ * The native timer that supports this instance. The value is set to non-zero when the
+ * native timer is created and it is set back to zero when the native timer is freed.
+ */
+ private long mNative = 0;
+
+ /** Fetch the native tag (an integer) for the given label. */
+ FeatureEnabled() {
+ mNative = nativeAnrTimerCreate(mLabel);
+ if (mNative == 0) throw new IllegalArgumentException("unable to create native timer");
+ synchronized (sAnrTimerList) {
+ sAnrTimerList.put(mNative, new WeakReference(AnrTimer.this));
+ }
+ }
+
+ /**
+ * Start a timer.
+ */
+ @Override
+ void start(@NonNull V arg, int pid, int uid, long timeoutMs) {
+ synchronized (mLock) {
+ if (mTimerIdMap.containsKey(arg)) {
+ // There is an existing timer. Cancel it.
+ cancel(arg);
+ }
+ int timerId = nativeAnrTimerStart(mNative, pid, uid, timeoutMs, mExtend);
+ if (timerId > 0) {
+ mTimerIdMap.put(arg, timerId);
+ mTimerArgMap.put(timerId, arg);
+ mTotalStarted++;
+ mMaxStarted = Math.max(mMaxStarted, mTimerIdMap.size());
+ } else {
+ throw new RuntimeException("unable to start timer");
+ }
+ }
+ }
+
+ /**
+ * Cancel a timer. No error is reported if the timer is not found because some clients
+ * cancel timers from common code that runs even if a timer was never started.
+ */
+ @Override
+ boolean cancel(@NonNull V arg) {
+ synchronized (mLock) {
+ Integer timer = removeLocked(arg);
+ if (timer == null) {
+ return false;
+ }
+ if (!nativeAnrTimerCancel(mNative, timer)) {
+ // There may be an expiration message in flight. Cancel it.
+ mHandler.removeMessages(mWhat, arg);
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Accept a timer in the framework-level handler. The timeout has been accepted and the
+ * timeout handler is executing.
+ */
+ @Override
+ boolean accept(@NonNull V arg) {
+ synchronized (mLock) {
+ Integer timer = removeLocked(arg);
+ if (timer == null) {
+ notFoundLocked("accept", arg);
+ return false;
+ }
+ nativeAnrTimerAccept(mNative, timer);
+ traceEnd(timer);
+ return true;
+ }
+ }
+
+ /**
+ * Discard a timer in the framework-level handler. For whatever reason, the timer is no
+ * longer interesting. No statistics are collected. Return false if the time was not
+ * found.
+ */
+ @Override
+ boolean discard(@NonNull V arg) {
+ synchronized (mLock) {
+ Integer timer = removeLocked(arg);
+ if (timer == null) {
+ notFoundLocked("discard", arg);
+ return false;
+ }
+ nativeAnrTimerDiscard(mNative, timer);
+ traceEnd(timer);
+ return true;
+ }
+ }
+
+ /** The feature is enabled. */
+ @Override
+ boolean enabled() {
+ return true;
+ }
+
+ /** Dump statistics from the native layer. */
+ @Override
+ void dump(PrintWriter pw, boolean verbose) {
+ synchronized (mLock) {
+ if (mNative != 0) {
+ nativeAnrTimerDump(mNative, verbose);
+ } else {
+ pw.println("closed");
+ }
+ }
+ }
+
+ /** Free native resources. */
+ @Override
+ void close() {
+ // Remove self from the list of active timers.
+ synchronized (sAnrTimerList) {
+ sAnrTimerList.remove(mNative);
+ }
+ synchronized (mLock) {
+ if (mNative != 0) nativeAnrTimerClose(mNative);
+ mNative = 0;
+ }
+ }
+
+ /**
+ * Delete the entries associated with arg from the maps and return the ID of the timer, if
+ * any.
+ */
+ @GuardedBy("mLock")
+ private Integer removeLocked(V arg) {
+ Integer r = mTimerIdMap.remove(arg);
+ if (r != null) {
+ synchronized (mTimerArgMap) {
+ mTimerArgMap.remove(r);
+ }
+ }
+ return r;
+ }
}
/**
* Start a timer associated with arg. The same object must be used to cancel, accept, or
* discard a timer later. If a timer already exists with the same arg, then the existing timer
- * is canceled and a new timer is created.
+ * is canceled and a new timer is created. The timeout is signed but negative delays are
+ * nonsensical. Rather than throw an exception, timeouts less than 0ms are forced to 0ms. This
+ * allows a client to deliver an immediate timeout via the AnrTimer.
*
* @param arg The key by which the timer is known. This is never examined or modified.
* @param pid The Linux process ID of the target being timed.
@@ -333,25 +576,39 @@
* @param timeoutMs The timer timeout, in milliseconds.
*/
public void start(@NonNull V arg, int pid, int uid, long timeoutMs) {
+ if (timeoutMs < 0) timeoutMs = 0;
mFeature.start(arg, pid, uid, timeoutMs);
}
/**
* Cancel the running timer associated with arg. The timer is forgotten. If the timer has
- * expired, the call is treated as a discard. No errors are reported if the timer does not
- * exist or if the timer has expired.
+ * expired, the call is treated as a discard. The function returns true if a running timer was
+ * found, and false if an expired timer was found or if no timer was found. After this call,
+ * the timer does not exist.
+ *
+ * Note: the return value is always true if the feature is not enabled.
+ *
+ * @param arg The key by which the timer is known. This is never examined or modified.
+ * @return True if a running timer was canceled.
*/
- public void cancel(@NonNull V arg) {
- mFeature.cancel(arg);
+ public boolean cancel(@NonNull V arg) {
+ return mFeature.cancel(arg);
}
/**
* Accept the expired timer associated with arg. This indicates that the caller considers the
- * timer expiration to be a true ANR. (See {@link #discard} for an alternate response.) It is
- * an error to accept a running timer, however the running timer will be canceled.
+ * timer expiration to be a true ANR. (See {@link #discard} for an alternate response.) The
+ * function returns true if an expired timer was found and false if a running timer was found or
+ * if no timer was found. After this call, the timer does not exist. It is an error to accept
+ * a running timer, however, the running timer will be canceled.
+ *
+ * Note: the return value is always true if the feature is not enabled.
+ *
+ * @param arg The key by which the timer is known. This is never examined or modified.
+ * @return True if an expired timer was accepted.
*/
- public void accept(@NonNull V arg) {
- mFeature.accept(arg);
+ public boolean accept(@NonNull V arg) {
+ return mFeature.accept(arg);
}
/**
@@ -359,11 +616,57 @@
* timer expiration to be a false ANR. ((See {@link #accept} for an alternate response.) One
* reason to discard an expired timer is if the process being timed was also being debugged:
* such a process could be stopped at a breakpoint and its failure to respond would not be an
- * error. It is an error to discard a running timer, however the running timer will be
- * canceled.
+ * error. After this call thie timer does not exist. It is an error to discard a running timer,
+ * however the running timer will be canceled.
+ *
+ * Note: the return value is always true if the feature is not enabled.
+ *
+ * @param arg The key by which the timer is known. This is never examined or modified.
+ * @return True if an expired timer was discarded.
*/
- public void discard(@NonNull V arg) {
- mFeature.discard(arg);
+ public boolean discard(@NonNull V arg) {
+ return mFeature.discard(arg);
+ }
+
+ /**
+ * The notifier that a timer has fired. The timerId and original pid/uid are supplied. This
+ * method is called from native code. This method takes mLock so that a timer cannot expire
+ * in the middle of another operation (like start or cancel).
+ */
+ @Keep
+ private boolean expire(int timerId, int pid, int uid) {
+ traceBegin(timerId, pid, uid, "expired");
+ V arg = null;
+ synchronized (mLock) {
+ arg = mTimerArgMap.get(timerId);
+ if (arg == null) {
+ Log.e(TAG, formatSimple("failed to expire timer %s:%d : arg not found",
+ mLabel, timerId));
+ mTotalErrors++;
+ return false;
+ }
+ mTotalExpired++;
+ }
+ mHandler.sendMessage(Message.obtain(mHandler, mWhat, arg));
+ return true;
+ }
+
+ /**
+ * Close the object and free any native resources.
+ */
+ public void close() {
+ mFeature.close();
+ }
+
+ /**
+ * Ensure any native resources are freed when the object is GC'ed. Best practice is to close
+ * the object explicitly, but overriding finalize() avoids accidental leaks.
+ */
+ @SuppressWarnings("Finalize")
+ @Override
+ protected void finalize() throws Throwable {
+ close();
+ super.finalize();
}
/**
@@ -373,8 +676,11 @@
synchronized (mLock) {
pw.format("timer: %s\n", mLabel);
pw.increaseIndent();
- pw.format("started=%d errors=%d\n", mTotalStarted, mTotalErrors);
+ pw.format("started=%d maxStarted=%d running=%d expired=%d errors=%d\n",
+ mTotalStarted, mMaxStarted, mTimerIdMap.size(),
+ mTotalExpired, mTotalErrors);
pw.decreaseIndent();
+ mFeature.dump(pw, false);
}
}
@@ -386,6 +692,13 @@
}
/**
+ * The current time in milliseconds.
+ */
+ private static long now() {
+ return SystemClock.uptimeMillis();
+ }
+
+ /**
* Dump all errors to the output stream.
*/
private static void dumpErrors(IndentingPrintWriter ipw) {
@@ -422,23 +735,89 @@
mTotalErrors++;
}
- /**
- * Log an error about a timer not found.
- */
+ /** Record an error about a timer not found. */
@GuardedBy("mLock")
private void notFoundLocked(String operation, Object arg) {
recordErrorLocked(operation, "notFound", arg);
}
- /**
- * Dumpsys output.
- */
- public static void dump(@NonNull PrintWriter pw, boolean verbose) {
+ /** Dumpsys output, allowing for overrides. */
+ @VisibleForTesting
+ static void dump(@NonNull PrintWriter pw, boolean verbose, @NonNull Injector injector) {
+ if (!injector.anrTimerServiceEnabled()) return;
+
final IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
ipw.println("AnrTimer statistics");
ipw.increaseIndent();
+ synchronized (sAnrTimerList) {
+ final int size = sAnrTimerList.size();
+ ipw.println("reporting " + size + " timers");
+ for (int i = 0; i < size; i++) {
+ AnrTimer a = sAnrTimerList.valueAt(i).get();
+ if (a != null) a.dump(ipw);
+ }
+ }
if (verbose) dumpErrors(ipw);
ipw.format("AnrTimerEnd\n");
ipw.decreaseIndent();
}
+
+ /** Dumpsys output. There is no output if the feature is not enabled. */
+ public static void dump(@NonNull PrintWriter pw, boolean verbose) {
+ dump(pw, verbose, sDefaultInjector);
+ }
+
+ /**
+ * Return true if the native timers are supported. Native timers are supported if the method
+ * nativeAnrTimerSupported() can be executed and it returns true.
+ */
+ private static boolean nativeTimersSupported() {
+ try {
+ return nativeAnrTimerSupported();
+ } catch (java.lang.UnsatisfiedLinkError e) {
+ return false;
+ }
+ }
+
+ /**
+ * Native methods
+ */
+
+ /** Return true if the native AnrTimer code is operational. */
+ private static native boolean nativeAnrTimerSupported();
+
+ /**
+ * Create a new native timer with the given key and name. The key is not used by the native
+ * code but it is returned to the Java layer in the expiration handler. The name is only for
+ * logging. Unlike the other methods, this is an instance method: the "this" parameter is
+ * passed into the native layer.
+ */
+ private native long nativeAnrTimerCreate(String name);
+
+ /** Release the native resources. No further operations are premitted. */
+ private static native int nativeAnrTimerClose(long service);
+
+ /** Start a timer and return its ID. Zero is returned on error. */
+ private static native int nativeAnrTimerStart(long service, int pid, int uid, long timeoutMs,
+ boolean extend);
+
+ /**
+ * Cancel a timer by ID. Return true if the timer was running and canceled. Return false if
+ * the timer was not found or if the timer had already expired.
+ */
+ private static native boolean nativeAnrTimerCancel(long service, int timerId);
+
+ /** Accept an expired timer by ID. Return true if the timer was found. */
+ private static native boolean nativeAnrTimerAccept(long service, int timerId);
+
+ /** Discard an expired timer by ID. Return true if the timer was found. */
+ private static native boolean nativeAnrTimerDiscard(long service, int timerId);
+
+ /** Prod the native library to log a few statistics. */
+ private static native void nativeAnrTimerDump(long service, boolean verbose);
+
+ // This is not a native method but it is a native interface, in the sense that it is called from
+ // the native layer to report timer expiration. The function must return true if the expiration
+ // message is delivered to the upper layers and false if it could not be delivered.
+ // private boolean expire(int timerId, int pid, int uid);
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index f43c1b0..3959a5e 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -3691,19 +3691,13 @@
return false;
}
- // If the app is using legacy-entry (not auto-enter), then we will get a client-request
- // that was actually a server-request (via pause(userLeaving=true)). This happens when
- // the app is PAUSING, so detect that case here.
- boolean originallyFromClient = fromClient
- && (!r.isState(PAUSING) || params.isAutoEnterEnabled());
-
- // If PiP2 flag is on and client-request to enter PiP came via onUserLeaveHint(),
- // we request a direct transition from Shell to TRANSIT_PIP_LEGACY to get the startWct
- // with the right entry bounds.
- if (isPip2ExperimentEnabled() && !originallyFromClient && !params.isAutoEnterEnabled()) {
+ // If PiP2 flag is on and client-request to enter PiP comes in,
+ // we request a direct transition from Shell to TRANSIT_PIP to get the startWct
+ // with the right entry bounds. So PiP activity isn't moved to a pinned task until after
+ // Shell calls back into Core with the entry bounds passed through.
+ if (isPip2ExperimentEnabled()) {
final Transition legacyEnterPipTransition = new Transition(TRANSIT_PIP,
- 0 /* flags */, getTransitionController(),
- mWindowManager.mSyncEngine);
+ 0 /* flags */, getTransitionController(), mWindowManager.mSyncEngine);
legacyEnterPipTransition.setPipActivity(r);
getTransitionController().startCollectOrQueue(legacyEnterPipTransition, (deferred) -> {
getTransitionController().requestStartTransition(legacyEnterPipTransition,
@@ -3712,6 +3706,12 @@
return true;
}
+ // If the app is using legacy-entry (not auto-enter), then we will get a client-request
+ // that was actually a server-request (via pause(userLeaving=true)). This happens when
+ // the app is PAUSING, so detect that case here.
+ boolean originallyFromClient = fromClient
+ && (!r.isState(PAUSING) || params.isAutoEnterEnabled());
+
// Create a transition only for this pip entry if it is coming from the app without the
// system requesting that the app enter-pip. If the system requested it, that means it
// should be part of that transition if possible.
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index eed46fe..fc3a338 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -61,6 +61,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.Preconditions;
import com.android.server.UiThread;
import com.android.server.am.PendingIntentRecord;
import com.android.window.flags.Flags;
@@ -219,6 +220,9 @@
private final WindowProcessController mCallerApp;
private final WindowProcessController mRealCallerApp;
private final boolean mIsCallForResult;
+ private final ActivityOptions mCheckedOptions;
+ private BalVerdict mResultForCaller;
+ private BalVerdict mResultForRealCaller;
private BalState(int callingUid, int callingPid, final String callingPackage,
int realCallingUid, int realCallingPid,
@@ -239,6 +243,7 @@
mIntent = intent;
mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid);
mIsCallForResult = resultRecord != null;
+ mCheckedOptions = checkedOptions;
if (balRequireOptInByPendingIntentCreator() // auto-opt in introduced with this feature
&& (originatingPendingIntent == null // not a PendingIntent
|| mIsCallForResult) // sent for result
@@ -369,8 +374,19 @@
return mCallingUid == mRealCallingUid;
}
- private String dump(BalVerdict resultIfPiCreatorAllowsBal,
- BalVerdict resultIfPiSenderAllowsBal) {
+ public void setResultForCaller(BalVerdict resultForCaller) {
+ Preconditions.checkState(mResultForCaller == null,
+ "mResultForCaller can only be set once");
+ this.mResultForCaller = resultForCaller;
+ }
+
+ public void setResultForRealCaller(BalVerdict resultForRealCaller) {
+ Preconditions.checkState(mResultForRealCaller == null,
+ "mResultForRealCaller can only be set once");
+ this.mResultForRealCaller = resultForRealCaller;
+ }
+
+ private String dump() {
StringBuilder sb = new StringBuilder(2048);
sb.append("[callingPackage: ")
.append(getDebugPackageName(mCallingPackage, mCallingUid));
@@ -392,7 +408,7 @@
sb.append("; balAllowedByPiCreator: ").append(mBalAllowedByPiCreator);
sb.append("; balAllowedByPiCreatorWithHardening: ")
.append(mBalAllowedByPiCreatorWithHardening);
- sb.append("; resultIfPiCreatorAllowsBal: ").append(resultIfPiCreatorAllowsBal);
+ sb.append("; resultIfPiCreatorAllowsBal: ").append(mResultForCaller);
sb.append("; hasRealCaller: ").append(hasRealCaller());
sb.append("; isCallForResult: ").append(mIsCallForResult);
sb.append("; isPendingIntent: ").append(isPendingIntent());
@@ -416,7 +432,7 @@
.append(mRealCallerApp.hasActivityInVisibleTask());
}
sb.append("; balAllowedByPiSender: ").append(mBalAllowedByPiSender);
- sb.append("; resultIfPiSenderAllowsBal: ").append(resultIfPiSenderAllowsBal);
+ sb.append("; resultIfPiSenderAllowsBal: ").append(mResultForRealCaller);
}
sb.append("]");
return sb.toString();
@@ -559,23 +575,25 @@
// realCallingSdkSandboxUidToAppUid should probably just be used instead (or in addition
// to realCallingUid when calculating resultForRealCaller below.
if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
- BalVerdict balVerdict = new BalVerdict(BAL_ALLOW_SDK_SANDBOX, /*background*/ false,
- "uid in SDK sandbox has visible (non-toast) window");
- return statsLog(balVerdict, state);
+ state.setResultForRealCaller(new BalVerdict(BAL_ALLOW_SDK_SANDBOX,
+ /*background*/ false,
+ "uid in SDK sandbox has visible (non-toast) window"));
+ return allowBasedOnRealCaller(state);
}
}
BalVerdict resultForCaller = checkBackgroundActivityStartAllowedByCaller(state);
+ state.setResultForCaller(resultForCaller);
if (!state.hasRealCaller()) {
if (resultForCaller.allows()) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Background activity start allowed. "
- + state.dump(resultForCaller, resultForCaller));
+ + state.dump());
}
- return statsLog(resultForCaller, state);
+ return allowBasedOnCaller(state);
}
- return abortLaunch(state, resultForCaller, resultForCaller);
+ return abortLaunch(state);
}
// The realCaller result is only calculated for PendingIntents (indicated by a valid
@@ -589,6 +607,8 @@
? resultForCaller
: checkBackgroundActivityStartAllowedBySender(state, checkedOptions)
.setBasedOnRealCaller();
+ state.setResultForRealCaller(resultForRealCaller);
+
if (state.isPendingIntent()) {
resultForCaller.setOnlyCreatorAllows(
resultForCaller.allows() && resultForRealCaller.blocks());
@@ -600,18 +620,18 @@
== ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Activity start explicitly allowed by caller. "
- + state.dump(resultForCaller, resultForRealCaller));
+ + state.dump());
}
- return statsLog(resultForCaller, state);
+ return allowBasedOnCaller(state);
}
if (resultForRealCaller.allows()
&& checkedOptions.getPendingIntentBackgroundActivityStartMode()
== ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Activity start explicitly allowed by real caller. "
- + state.dump(resultForCaller, resultForRealCaller));
+ + state.dump());
}
- return statsLog(resultForRealCaller, state);
+ return allowBasedOnRealCaller(state);
}
// Handle PendingIntent cases with default behavior next
boolean callerCanAllow = resultForCaller.allows()
@@ -626,26 +646,24 @@
// Will be allowed even with BAL hardening.
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Activity start allowed by caller. "
- + state.dump(resultForCaller, resultForRealCaller));
+ + state.dump());
}
- // return the realCaller result for backwards compatibility
- return statsLog(resultForRealCaller, state);
+ return allowBasedOnCaller(state);
}
if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) {
Slog.wtf(TAG,
"With Android 15 BAL hardening this activity start may be blocked"
+ " if the PI creator upgrades target_sdk to 35+"
+ " AND the PI sender upgrades target_sdk to 34+! "
- + state.dump(resultForCaller, resultForRealCaller));
+ + state.dump());
showBalRiskToast();
- // return the realCaller result for backwards compatibility
- return statsLog(resultForRealCaller, state);
+ return allowBasedOnCaller(state);
}
Slog.wtf(TAG,
"Without Android 15 BAL hardening this activity start would be allowed"
+ " (missing opt in by PI creator or sender)! "
- + state.dump(resultForCaller, resultForRealCaller));
- return abortLaunch(state, resultForCaller, resultForRealCaller);
+ + state.dump());
+ return abortLaunch(state);
}
if (callerCanAllow) {
// Allowed before V by creator
@@ -653,24 +671,24 @@
// Will be allowed even with BAL hardening.
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Activity start allowed by caller. "
- + state.dump(resultForCaller, resultForRealCaller));
+ + state.dump());
}
- return statsLog(resultForCaller, state);
+ return allowBasedOnCaller(state);
}
if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) {
Slog.wtf(TAG,
"With Android 15 BAL hardening this activity start may be blocked"
+ " if the PI creator upgrades target_sdk to 35+! "
+ " (missing opt in by PI creator)! "
- + state.dump(resultForCaller, resultForRealCaller));
+ + state.dump());
showBalRiskToast();
- return statsLog(resultForCaller, state);
+ return allowBasedOnCaller(state);
}
Slog.wtf(TAG,
"Without Android 15 BAL hardening this activity start would be allowed"
+ " (missing opt in by PI creator)! "
- + state.dump(resultForCaller, resultForRealCaller));
- return abortLaunch(state, resultForCaller, resultForRealCaller);
+ + state.dump());
+ return abortLaunch(state);
}
if (realCallerCanAllow) {
// Allowed before U by sender
@@ -679,23 +697,38 @@
"With Android 14 BAL hardening this activity start will be blocked"
+ " if the PI sender upgrades target_sdk to 34+! "
+ " (missing opt in by PI sender)! "
- + state.dump(resultForCaller, resultForRealCaller));
+ + state.dump());
showBalRiskToast();
- return statsLog(resultForRealCaller, state);
+ return allowBasedOnRealCaller(state);
}
Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed"
+ " (missing opt in by PI sender)! "
- + state.dump(resultForCaller, resultForRealCaller));
- return abortLaunch(state, resultForCaller, resultForRealCaller);
+ + state.dump());
+ return abortLaunch(state);
}
// neither the caller not the realCaller can allow or have explicitly opted out
- return abortLaunch(state, resultForCaller, resultForRealCaller);
+ return abortLaunch(state);
}
- private BalVerdict abortLaunch(BalState state, BalVerdict resultForCaller,
- BalVerdict resultForRealCaller) {
+ private BalVerdict allowBasedOnCaller(BalState state) {
+ if (DEBUG_ACTIVITY_STARTS) {
+ Slog.d(TAG, "Background activity launch allowed based on caller. "
+ + state.dump());
+ }
+ return statsLog(state.mResultForCaller, state);
+ }
+
+ private BalVerdict allowBasedOnRealCaller(BalState state) {
+ if (DEBUG_ACTIVITY_STARTS) {
+ Slog.d(TAG, "Background activity launch allowed based on real caller. "
+ + state.dump());
+ }
+ return statsLog(state.mResultForRealCaller, state);
+ }
+
+ private BalVerdict abortLaunch(BalState state) {
Slog.w(TAG, "Background activity launch blocked! "
- + state.dump(resultForCaller, resultForRealCaller));
+ + state.dump());
showBalBlockedToast();
return statsLog(BalVerdict.BLOCK, state);
}
@@ -1471,24 +1504,36 @@
&& (callingUid == Process.SYSTEM_UID || realCallingUid == Process.SYSTEM_UID)) {
String activityName = intent != null
? requireNonNull(intent.getComponent()).flattenToShortString() : "";
- FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
- activityName,
- BAL_ALLOW_PENDING_INTENT,
- callingUid,
- realCallingUid);
+ writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT,
+ state);
}
if (code == BAL_ALLOW_PERMISSION || code == BAL_ALLOW_FOREGROUND
- || code == BAL_ALLOW_SAW_PERMISSION) {
+ || code == BAL_ALLOW_SAW_PERMISSION) {
// We don't need to know which activity in this case.
- FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
- /*activityName*/ "",
- code,
- callingUid,
- realCallingUid);
+ writeBalAllowedLog("", code, state);
+
}
return finalVerdict;
}
+ private static void writeBalAllowedLog(String activityName, int code, BalState state) {
+ FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
+ activityName,
+ code,
+ state.mCallingUid,
+ state.mRealCallingUid,
+ state.mResultForCaller == null ? BAL_BLOCK : state.mResultForCaller.getRawCode(),
+ state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts(),
+ state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+ != ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED,
+ state.mResultForRealCaller == null ? BAL_BLOCK
+ : state.mResultForRealCaller.getRawCode(),
+ state.mBalAllowedByPiSender.allowsBackgroundActivityStarts(),
+ state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
+ != ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED
+ );
+ }
+
/**
* Called whenever an activity finishes. Stores the record, so it can be used by ASM grace
* period checks.
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index b19f3d8..dfa9dce 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -79,6 +79,7 @@
":lib_cachedAppOptimizer_native",
":lib_gameManagerService_native",
":lib_oomConnection_native",
+ ":lib_anrTimer_native",
],
include_dirs: [
@@ -246,3 +247,10 @@
name: "lib_oomConnection_native",
srcs: ["com_android_server_am_OomConnection.cpp"],
}
+
+filegroup {
+ name: "lib_anrTimer_native",
+ srcs: [
+ "com_android_server_utils_AnrTimer.cpp",
+ ],
+}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 9ba0a2a..afb0b20 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -114,6 +114,7 @@
jmethodID notifyFocusChanged;
jmethodID notifySensorEvent;
jmethodID notifySensorAccuracy;
+ jmethodID notifyStickyModifierStateChanged;
jmethodID notifyStylusGestureStarted;
jmethodID isInputMethodConnectionActive;
jmethodID notifyVibratorState;
@@ -270,7 +271,8 @@
class NativeInputManager : public virtual InputReaderPolicyInterface,
public virtual InputDispatcherPolicyInterface,
public virtual PointerControllerPolicyInterface,
- public virtual PointerChoreographerPolicyInterface {
+ public virtual PointerChoreographerPolicyInterface,
+ public virtual InputFilterPolicyInterface {
protected:
virtual ~NativeInputManager();
@@ -388,6 +390,10 @@
PointerControllerInterface::ControllerType type) override;
void notifyPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override;
+ /* --- InputFilterPolicyInterface implementation --- */
+ void notifyStickyModifierStateChanged(uint32_t modifierState,
+ uint32_t lockedModifierState) override;
+
private:
sp<InputManagerInterface> mInputManager;
@@ -477,7 +483,7 @@
mServiceObj = env->NewGlobalRef(serviceObj);
- InputManager* im = new InputManager(this, *this, *this);
+ InputManager* im = new InputManager(this, *this, *this, *this);
mInputManager = im;
defaultServiceManager()->addService(String16("inputflinger"), im);
}
@@ -806,6 +812,14 @@
checkAndClearExceptionFromCallback(env, "onPointerDisplayIdChanged");
}
+void NativeInputManager::notifyStickyModifierStateChanged(uint32_t modifierState,
+ uint32_t lockedModifierState) {
+ JNIEnv* env = jniEnv();
+ env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyStickyModifierStateChanged,
+ modifierState, lockedModifierState);
+ checkAndClearExceptionFromCallback(env, "notifyStickyModifierStateChanged");
+}
+
sp<SurfaceControl> NativeInputManager::getParentSurfaceForPointers(int displayId) {
JNIEnv* env = jniEnv();
jlong nativeSurfaceControlPtr =
@@ -2957,6 +2971,9 @@
GET_METHOD_ID(gServiceClassInfo.onPointerDisplayIdChanged, clazz, "onPointerDisplayIdChanged",
"(IFF)V");
+ GET_METHOD_ID(gServiceClassInfo.notifyStickyModifierStateChanged, clazz,
+ "notifyStickyModifierStateChanged", "(II)V");
+
GET_METHOD_ID(gServiceClassInfo.onPointerDownOutsideFocus, clazz,
"onPointerDownOutsideFocus", "(Landroid/os/IBinder;)V");
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
new file mode 100644
index 0000000..97b18fa
--- /dev/null
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -0,0 +1,918 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <time.h>
+#include <pthread.h>
+#include <sys/timerfd.h>
+#include <inttypes.h>
+
+#include <algorithm>
+#include <list>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+#define LOG_TAG "AnrTimerService"
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include "android_runtime/AndroidRuntime.h"
+#include "core_jni_helpers.h"
+
+#include <utils/Mutex.h>
+#include <utils/Timers.h>
+
+#include <utils/Log.h>
+#include <utils/Timers.h>
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android-base/unique_fd.h>
+
+using ::android::base::StringPrintf;
+
+
+// Native support is unavailable on WIN32 platforms. This macro preemptively disables it.
+#ifdef _WIN32
+#define NATIVE_SUPPORT 0
+#else
+#define NATIVE_SUPPORT 1
+#endif
+
+namespace android {
+
+// using namespace android;
+
+// Almost nothing in this module needs to be in the android namespace.
+namespace {
+
+// If not on a Posix system, create stub timerfd methods. These are defined to allow
+// compilation. They are not functional. Also, they do not leak outside this compilation unit.
+#ifdef _WIN32
+int timer_create() {
+ return -1;
+}
+int timer_settime(int, int, void const *, void *) {
+ return -1;
+}
+#else
+int timer_create() {
+ return timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
+}
+int timer_settime(int fd, int flags, const struct itimerspec *new_value,
+ struct itimerspec *_Nullable old_value) {
+ return timerfd_settime(fd, flags, new_value, old_value);
+}
+#endif
+
+// A local debug flag that gates a set of log messages for debug only. This is normally const
+// false so the debug statements are not included in the image. The flag can be set true in a
+// unit test image to debug test failures.
+const bool DEBUG = false;
+
+// Return the current time in nanoseconds. This time is relative to system boot.
+nsecs_t now() {
+ return systemTime(SYSTEM_TIME_MONOTONIC);
+}
+
+/**
+ * This class encapsulates the anr timer service. The service manages a list of individual
+ * timers. A timer is either Running or Expired. Once started, a timer may be canceled or
+ * accepted. Both actions collect statistics about the timer and then delete it. An expired
+ * timer may also be discarded, which deletes the timer without collecting any statistics.
+ *
+ * All public methods in this class are thread-safe.
+ */
+class AnrTimerService {
+ private:
+ class ProcessStats;
+ class Timer;
+
+ public:
+
+ // The class that actually runs the clock.
+ class Ticker;
+
+ // A timer is identified by a timer_id_t. Timer IDs are unique in the moment.
+ using timer_id_t = uint32_t;
+
+ // A manifest constant. No timer is ever created with this ID.
+ static const timer_id_t NOTIMER = 0;
+
+ // A notifier is called with a timer ID, the timer's tag, and the client's cookie. The pid
+ // and uid that were originally assigned to the timer are passed as well.
+ using notifier_t = bool (*)(timer_id_t, int pid, int uid, void* cookie, jweak object);
+
+ enum Status {
+ Invalid,
+ Running,
+ Expired,
+ Canceled
+ };
+
+ /**
+ * Create a timer service. The service is initialized with a name used for logging. The
+ * constructor is also given the notifier callback, and two cookies for the callback: the
+ * traditional void* and an int.
+ */
+ AnrTimerService(char const* label, notifier_t notifier, void* cookie, jweak jtimer, Ticker*);
+
+ // Delete the service and clean up memory.
+ ~AnrTimerService();
+
+ // Start a timer and return the associated timer ID. It does not matter if the same pid/uid
+ // are already in the running list. Once start() is called, one of cancel(), accept(), or
+ // discard() must be called to clean up the internal data structures.
+ timer_id_t start(int pid, int uid, nsecs_t timeout, bool extend);
+
+ // Cancel a timer and remove it from all lists. This is called when the event being timed
+ // has occurred. If the timer was Running, the function returns true. The other
+ // possibilities are that the timer was Expired or non-existent; in both cases, the function
+ // returns false.
+ bool cancel(timer_id_t timerId);
+
+ // Accept a timer and remove it from all lists. This is called when the upper layers accept
+ // that a timer has expired. If the timer was Expired, the function returns true. The
+ // other possibilities are tha the timer was Running or non-existing; in both cases, the
+ // function returns false.
+ bool accept(timer_id_t timerId);
+
+ // Discard a timer without collecting any statistics. This is called when the upper layers
+ // recognize that a timer expired but decide the expiration is not significant. If the
+ // timer was Expired, the function returns true. The other possibilities are tha the timer
+ // was Running or non-existing; in both cases, the function returns false.
+ bool discard(timer_id_t timerId);
+
+ // A timer has expired.
+ void expire(timer_id_t);
+
+ // Dump a small amount of state to the log file.
+ void dump(bool verbose) const;
+
+ // Return the Java object associated with this instance.
+ jweak jtimer() const {
+ return notifierObject_;
+ }
+
+ private:
+ // The service cannot be copied.
+ AnrTimerService(AnrTimerService const &) = delete;
+
+ // Insert a timer into the running list. The lock must be held by the caller.
+ void insert(const Timer&);
+
+ // Remove a timer from the lists and return it. The lock must be held by the caller.
+ Timer remove(timer_id_t timerId);
+
+ // Return a string representation of a status value.
+ static char const *statusString(Status);
+
+ // The name of this service, for logging.
+ std::string const label_;
+
+ // The callback that is invoked when a timer expires.
+ notifier_t const notifier_;
+
+ // The two cookies passed to the notifier.
+ void* notifierCookie_;
+ jweak notifierObject_;
+
+ // The global lock
+ mutable Mutex lock_;
+
+ // The list of all timers that are still running. This is sorted by ID for fast lookup.
+ std::set<Timer> running_;
+
+ // The maximum number of active timers.
+ size_t maxActive_;
+
+ // Simple counters
+ struct Counters {
+ // The number of timers started, canceled, accepted, discarded, and expired.
+ size_t started;
+ size_t canceled;
+ size_t accepted;
+ size_t discarded;
+ size_t expired;
+
+ // The number of times there were zero active timers.
+ size_t drained;
+
+ // The number of times a protocol error was seen.
+ size_t error;
+ };
+
+ Counters counters_;
+
+ // The clock used by this AnrTimerService.
+ Ticker *ticker_;
+};
+
+class AnrTimerService::ProcessStats {
+ public:
+ nsecs_t cpu_time;
+ nsecs_t cpu_delay;
+
+ ProcessStats() :
+ cpu_time(0),
+ cpu_delay(0) {
+ }
+
+ // Collect all statistics for a process. Return true if the fill succeeded and false if it
+ // did not. If there is any problem, the statistics are zeroed.
+ bool fill(int pid) {
+ cpu_time = 0;
+ cpu_delay = 0;
+
+ char path[PATH_MAX];
+ snprintf(path, sizeof(path), "/proc/%u/schedstat", pid);
+ ::android::base::unique_fd fd(open(path, O_RDONLY | O_CLOEXEC));
+ if (!fd.ok()) {
+ return false;
+ }
+ char buffer[128];
+ ssize_t len = read(fd, buffer, sizeof(buffer));
+ if (len <= 0) {
+ return false;
+ }
+ if (len >= sizeof(buffer)) {
+ ALOGE("proc file too big: %s", path);
+ return false;
+ }
+ buffer[len] = 0;
+ unsigned long t1;
+ unsigned long t2;
+ if (sscanf(buffer, "%lu %lu", &t1, &t2) != 2) {
+ return false;
+ }
+ cpu_time = t1;
+ cpu_delay = t2;
+ return true;
+ }
+};
+
+class AnrTimerService::Timer {
+ public:
+ // A unique ID assigned when the Timer is created.
+ timer_id_t const id;
+
+ // The creation parameters. The timeout is the original, relative timeout.
+ int const pid;
+ int const uid;
+ nsecs_t const timeout;
+ bool const extend;
+
+ // The state of this timer.
+ Status status;
+
+ // The scheduled timeout. This is an absolute time. It may be extended.
+ nsecs_t scheduled;
+
+ // True if this timer has been extended.
+ bool extended;
+
+ // Bookkeeping for extensions. The initial state of the process. This is collected only if
+ // the timer is extensible.
+ ProcessStats initial;
+
+ // The default constructor is used to create timers that are Invalid, representing the "not
+ // found" condition when a collection is searched.
+ Timer() :
+ id(NOTIMER),
+ pid(0),
+ uid(0),
+ timeout(0),
+ extend(false),
+ status(Invalid),
+ scheduled(0),
+ extended(false) {
+ }
+
+ // This constructor creates a timer with the specified id. This can be used as the argument
+ // to find().
+ Timer(timer_id_t id) :
+ id(id),
+ pid(0),
+ uid(0),
+ timeout(0),
+ extend(false),
+ status(Invalid),
+ scheduled(0),
+ extended(false) {
+ }
+
+ // Create a new timer. This starts the timer.
+ Timer(int pid, int uid, nsecs_t timeout, bool extend) :
+ id(nextId()),
+ pid(pid),
+ uid(uid),
+ timeout(timeout),
+ extend(extend),
+ status(Running),
+ scheduled(now() + timeout),
+ extended(false) {
+ if (extend && pid != 0) {
+ initial.fill(pid);
+ }
+ }
+
+ // Cancel a timer. Return the headroom (which may be negative). This does not, as yet,
+ // account for extensions.
+ void cancel() {
+ ALOGW_IF(DEBUG && status != Running, "cancel %s", toString().c_str());
+ status = Canceled;
+ }
+
+ // Expire a timer. Return true if the timer is expired and false otherwise. The function
+ // returns false if the timer is eligible for extension. If the function returns false, the
+ // scheduled time is updated.
+ bool expire() {
+ ALOGI_IF(DEBUG, "expire %s", toString().c_str());
+ nsecs_t extension = 0;
+ if (extend && !extended) {
+ // Only one extension is permitted.
+ extended = true;
+ ProcessStats current;
+ current.fill(pid);
+ extension = current.cpu_delay - initial.cpu_delay;
+ if (extension < 0) extension = 0;
+ if (extension > timeout) extension = timeout;
+ }
+ if (extension == 0) {
+ status = Expired;
+ } else {
+ scheduled += extension;
+ }
+ return status == Expired;
+ }
+
+ // Accept a timeout.
+ void accept() {
+ }
+
+ // Discard a timeout.
+ void discard() {
+ }
+
+ // Timers are sorted by id, which is unique. This provides fast lookups.
+ bool operator<(Timer const &r) const {
+ return id < r.id;
+ }
+
+ bool operator==(timer_id_t r) const {
+ return id == r;
+ }
+
+ std::string toString() const {
+ return StringPrintf("timer id=%d pid=%d status=%s", id, pid, statusString(status));
+ }
+
+ std::string toString(nsecs_t now) const {
+ uint32_t ms = nanoseconds_to_milliseconds(now - scheduled);
+ return StringPrintf("timer id=%d pid=%d status=%s scheduled=%ums",
+ id, pid, statusString(status), -ms);
+ }
+
+ static int maxId() {
+ return idGen;
+ }
+
+ private:
+ // Get the next free ID. NOTIMER is never returned.
+ static timer_id_t nextId() {
+ timer_id_t id = idGen.fetch_add(1);
+ while (id == NOTIMER) {
+ id = idGen.fetch_add(1);
+ }
+ return id;
+ }
+
+ // IDs start at 1. A zero ID is invalid.
+ static std::atomic<timer_id_t> idGen;
+};
+
+// IDs start at 1.
+std::atomic<AnrTimerService::timer_id_t> AnrTimerService::Timer::idGen(1);
+
+/**
+ * Manage a set of timers and notify clients when there is a timeout.
+ */
+class AnrTimerService::Ticker {
+ private:
+ struct Entry {
+ const nsecs_t scheduled;
+ const timer_id_t id;
+ AnrTimerService* const service;
+
+ Entry(nsecs_t scheduled, timer_id_t id, AnrTimerService* service) :
+ scheduled(scheduled), id(id), service(service) {};
+
+ bool operator<(const Entry &r) const {
+ return scheduled == r.scheduled ? id < r.id : scheduled < r.scheduled;
+ }
+ };
+
+ public:
+
+ // Construct the ticker. This creates the timerfd file descriptor and starts the monitor
+ // thread. The monitor thread is given a unique name.
+ Ticker() {
+ timerFd_ = timer_create();
+ if (timerFd_ < 0) {
+ ALOGE("failed to create timerFd: %s", strerror(errno));
+ return;
+ }
+
+ if (pthread_create(&watcher_, 0, run, this) != 0) {
+ ALOGE("failed to start thread: %s", strerror(errno));
+ watcher_ = 0;
+ ::close(timerFd_);
+ return;
+ }
+
+ // 16 is a magic number from the kernel. Thread names may not be longer than this many
+ // bytes, including the terminating null. The snprintf() method will truncate properly.
+ char name[16];
+ snprintf(name, sizeof(name), "AnrTimerService");
+ pthread_setname_np(watcher_, name);
+
+ ready_ = true;
+ }
+
+ ~Ticker() {
+ // Closing the file descriptor will close the monitor process, if any.
+ if (timerFd_ >= 0) ::close(timerFd_);
+ timerFd_ = -1;
+ watcher_ = 0;
+ }
+
+ // Insert a timer. Unless canceled, the timer will expire at the scheduled time. If it
+ // expires, the service will be notified with the id.
+ void insert(nsecs_t scheduled, timer_id_t id, AnrTimerService *service) {
+ Entry e(scheduled, id, service);
+ AutoMutex _l(lock_);
+ timer_id_t front = headTimerId();
+ running_.insert(e);
+ if (front != headTimerId()) restartLocked();
+ maxRunning_ = std::max(maxRunning_, running_.size());
+ }
+
+ // Remove a timer. The timer is identified by its scheduled timeout and id. Technically,
+ // the id is sufficient (because timer IDs are unique) but using the timeout is more
+ // efficient.
+ void remove(nsecs_t scheduled, timer_id_t id) {
+ Entry key(scheduled, id, 0);
+ AutoMutex _l(lock_);
+ timer_id_t front = headTimerId();
+ auto found = running_.find(key);
+ if (found != running_.end()) running_.erase(found);
+ if (front != headTimerId()) restartLocked();
+ }
+
+ // Remove every timer associated with the service.
+ void remove(AnrTimerService const* service) {
+ AutoMutex _l(lock_);
+ timer_id_t front = headTimerId();
+ for (auto i = running_.begin(); i != running_.end(); i++) {
+ if (i->service == service) {
+ running_.erase(i);
+ }
+ }
+ if (front != headTimerId()) restartLocked();
+ }
+
+ // Return the number of timers still running.
+ size_t running() const {
+ AutoMutex _l(lock_);
+ return running_.size();
+ }
+
+ // Return the high-water mark of timers running.
+ size_t maxRunning() const {
+ AutoMutex _l(lock_);
+ return maxRunning_;
+ }
+
+ private:
+
+ // Return the head of the running list. The lock must be held by the caller.
+ timer_id_t headTimerId() {
+ return running_.empty() ? NOTIMER : running_.cbegin()->id;
+ }
+
+ // A simple wrapper that meets the requirements of pthread_create.
+ static void* run(void* arg) {
+ reinterpret_cast<Ticker*>(arg)->monitor();
+ ALOGI("monitor exited");
+ return 0;
+ }
+
+ // Loop (almost) forever. Whenever the timerfd expires, expire as many entries as
+ // possible. The loop terminates when the read fails; this generally indicates that the
+ // file descriptor has been closed and the thread can exit.
+ void monitor() {
+ uint64_t token = 0;
+ while (read(timerFd_, &token, sizeof(token)) == sizeof(token)) {
+ // Move expired timers into the local ready list. This is done inside
+ // the lock. Then, outside the lock, expire them.
+ nsecs_t current = now();
+ std::vector<Entry> ready;
+ {
+ AutoMutex _l(lock_);
+ while (!running_.empty()) {
+ Entry timer = *(running_.begin());
+ if (timer.scheduled <= current) {
+ ready.push_back(timer);
+ running_.erase(running_.cbegin());
+ } else {
+ break;
+ }
+ }
+ restartLocked();
+ }
+ // Call the notifiers outside the lock. Calling the notifiers with the lock held
+ // can lead to deadlock, if the Java-side handler also takes a lock. Note that the
+ // timerfd is already running.
+ for (auto i = ready.begin(); i != ready.end(); i++) {
+ Entry e = *i;
+ e.service->expire(e.id);
+ }
+ }
+ }
+
+ // Restart the ticker. The caller must be holding the lock. This method updates the
+ // timerFd_ to expire at the time of the first Entry in the running list. This method does
+ // not check to see if the currently programmed expiration time is different from the
+ // scheduled expiration time of the first entry.
+ void restartLocked() {
+ if (!running_.empty()) {
+ Entry const x = *(running_.cbegin());
+ nsecs_t delay = x.scheduled - now();
+ // Force a minimum timeout of 10ns.
+ if (delay < 10) delay = 10;
+ time_t sec = nanoseconds_to_seconds(delay);
+ time_t ns = delay - seconds_to_nanoseconds(sec);
+ struct itimerspec setting = {
+ .it_interval = { 0, 0 },
+ .it_value = { sec, ns },
+ };
+ timer_settime(timerFd_, 0, &setting, nullptr);
+ restarted_++;
+ ALOGI_IF(DEBUG, "restarted timerfd for %ld.%09ld", sec, ns);
+ } else {
+ const struct itimerspec setting = {
+ .it_interval = { 0, 0 },
+ .it_value = { 0, 0 },
+ };
+ timer_settime(timerFd_, 0, &setting, nullptr);
+ drained_++;
+ ALOGI_IF(DEBUG, "drained timer list");
+ }
+ }
+
+ // The usual lock.
+ mutable Mutex lock_;
+
+ // True if the object was initialized properly. Android does not support throwing C++
+ // exceptions, so clients should check this flag after constructing the object. This is
+ // effectively const after the instance has been created.
+ bool ready_ = false;
+
+ // The file descriptor of the timer.
+ int timerFd_ = -1;
+
+ // The thread that monitors the timer.
+ pthread_t watcher_ = 0;
+
+ // The number of times the timer was restarted.
+ size_t restarted_ = 0;
+
+ // The number of times the timer list was exhausted.
+ size_t drained_ = 0;
+
+ // The highwater mark of timers that are running.
+ size_t maxRunning_ = 0;
+
+ // The list of timers that are scheduled. This set is sorted by timeout and then by timer
+ // ID. A set is sufficient (as opposed to a multiset) because timer IDs are unique.
+ std::set<Entry> running_;
+};
+
+
+AnrTimerService::AnrTimerService(char const* label,
+ notifier_t notifier, void* cookie, jweak jtimer, Ticker* ticker) :
+ label_(label),
+ notifier_(notifier),
+ notifierCookie_(cookie),
+ notifierObject_(jtimer),
+ ticker_(ticker) {
+
+ // Zero the statistics
+ maxActive_ = 0;
+ memset(&counters_, 0, sizeof(counters_));
+
+ ALOGI_IF(DEBUG, "initialized %s", label);
+}
+
+AnrTimerService::~AnrTimerService() {
+ AutoMutex _l(lock_);
+ ticker_->remove(this);
+}
+
+char const *AnrTimerService::statusString(Status s) {
+ switch (s) {
+ case Invalid: return "invalid";
+ case Running: return "running";
+ case Expired: return "expired";
+ case Canceled: return "canceled";
+ }
+ return "unknown";
+}
+
+AnrTimerService::timer_id_t AnrTimerService::start(int pid, int uid,
+ nsecs_t timeout, bool extend) {
+ ALOGI_IF(DEBUG, "starting");
+ AutoMutex _l(lock_);
+ Timer t(pid, uid, timeout, extend);
+ insert(t);
+ counters_.started++;
+
+ ALOGI_IF(DEBUG, "started timer %u timeout=%zu", t.id, static_cast<size_t>(timeout));
+ return t.id;
+}
+
+bool AnrTimerService::cancel(timer_id_t timerId) {
+ ALOGI_IF(DEBUG, "canceling %u", timerId);
+ if (timerId == NOTIMER) return false;
+ AutoMutex _l(lock_);
+ Timer timer = remove(timerId);
+
+ bool result = timer.status == Running;
+ if (timer.status != Invalid) {
+ timer.cancel();
+ } else {
+ counters_.error++;
+ }
+ counters_.canceled++;
+ ALOGI_IF(DEBUG, "canceled timer %u", timerId);
+ return result;
+}
+
+bool AnrTimerService::accept(timer_id_t timerId) {
+ ALOGI_IF(DEBUG, "accepting %u", timerId);
+ if (timerId == NOTIMER) return false;
+ AutoMutex _l(lock_);
+ Timer timer = remove(timerId);
+
+ bool result = timer.status == Expired;
+ if (timer.status == Expired) {
+ timer.accept();
+ } else {
+ counters_.error++;
+ }
+ counters_.accepted++;
+ ALOGI_IF(DEBUG, "accepted timer %u", timerId);
+ return result;
+}
+
+bool AnrTimerService::discard(timer_id_t timerId) {
+ ALOGI_IF(DEBUG, "discarding %u", timerId);
+ if (timerId == NOTIMER) return false;
+ AutoMutex _l(lock_);
+ Timer timer = remove(timerId);
+
+ bool result = timer.status == Expired;
+ if (timer.status == Expired) {
+ timer.discard();
+ } else {
+ counters_.error++;
+ }
+ counters_.discarded++;
+ ALOGI_IF(DEBUG, "discarded timer %u", timerId);
+ return result;
+}
+
+// Hold the lock in order to manage the running list.
+// the listener.
+void AnrTimerService::expire(timer_id_t timerId) {
+ ALOGI_IF(DEBUG, "expiring %u", timerId);
+ // Save the timer attributes for the notification
+ int pid = 0;
+ int uid = 0;
+ bool expired = false;
+ {
+ AutoMutex _l(lock_);
+ Timer t = remove(timerId);
+ expired = t.expire();
+ if (t.status == Invalid) {
+ ALOGW_IF(DEBUG, "error: expired invalid timer %u", timerId);
+ return;
+ } else {
+ // The timer is either Running (because it was extended) or expired (and is awaiting an
+ // accept or discard).
+ insert(t);
+ }
+ }
+
+ // Deliver the notification outside of the lock.
+ if (expired) {
+ if (!notifier_(timerId, pid, uid, notifierCookie_, notifierObject_)) {
+ AutoMutex _l(lock_);
+ // Notification failed, which means the listener will never call accept() or
+ // discard(). Do not reinsert the timer.
+ remove(timerId);
+ }
+ }
+ ALOGI_IF(DEBUG, "expired timer %u", timerId);
+}
+
+void AnrTimerService::insert(const Timer& t) {
+ running_.insert(t);
+ if (t.status == Running) {
+ // Only forward running timers to the ticker. Expired timers are handled separately.
+ ticker_->insert(t.scheduled, t.id, this);
+ maxActive_ = std::max(maxActive_, running_.size());
+ }
+}
+
+AnrTimerService::Timer AnrTimerService::remove(timer_id_t timerId) {
+ Timer key(timerId);
+ auto found = running_.find(key);
+ if (found != running_.end()) {
+ Timer result = *found;
+ running_.erase(found);
+ ticker_->remove(result.scheduled, result.id);
+ return result;
+ }
+ return Timer();
+}
+
+void AnrTimerService::dump(bool verbose) const {
+ AutoMutex _l(lock_);
+ ALOGI("timer %s ops started=%zu canceled=%zu accepted=%zu discarded=%zu expired=%zu",
+ label_.c_str(),
+ counters_.started, counters_.canceled, counters_.accepted,
+ counters_.discarded, counters_.expired);
+ ALOGI("timer %s stats max-active=%zu/%zu running=%zu/%zu errors=%zu",
+ label_.c_str(),
+ maxActive_, ticker_->maxRunning(), running_.size(), ticker_->running(),
+ counters_.error);
+
+ if (verbose) {
+ nsecs_t time = now();
+ for (auto i = running_.begin(); i != running_.end(); i++) {
+ Timer t = *i;
+ ALOGI(" running %s", t.toString(time).c_str());
+ }
+ }
+}
+
+/**
+ * True if the native methods are supported in this process. Native methods are supported only
+ * if the initialization succeeds.
+ */
+bool nativeSupportEnabled = false;
+
+/**
+ * Singleton/globals for the anr timer. Among other things, this includes a Ticker* and a use
+ * count. The JNI layer creates a single Ticker for all operational AnrTimers. The Ticker is
+ * created when the first AnrTimer is created, and is deleted when the last AnrTimer is closed.
+ */
+static Mutex gAnrLock;
+struct AnrArgs {
+ jclass clazz = NULL;
+ jmethodID func = NULL;
+ JavaVM* vm = NULL;
+ AnrTimerService::Ticker* ticker = nullptr;
+ int tickerUseCount = 0;;
+};
+static AnrArgs gAnrArgs;
+
+// The cookie is the address of the AnrArgs object to which the notification should be sent.
+static bool anrNotify(AnrTimerService::timer_id_t timerId, int pid, int uid,
+ void* cookie, jweak jtimer) {
+ AutoMutex _l(gAnrLock);
+ AnrArgs* target = reinterpret_cast<AnrArgs* >(cookie);
+ JNIEnv *env;
+ if (target->vm->AttachCurrentThread(&env, 0) != JNI_OK) {
+ ALOGE("failed to attach thread to JavaVM");
+ return false;
+ }
+ jboolean r = false;
+ jobject timer = env->NewGlobalRef(jtimer);
+ if (timer != nullptr) {
+ r = env->CallBooleanMethod(timer, target->func, timerId, pid, uid);
+ env->DeleteGlobalRef(timer);
+ }
+ target->vm->DetachCurrentThread();
+ return r;
+}
+
+jboolean anrTimerSupported(JNIEnv* env, jclass) {
+ return nativeSupportEnabled;
+}
+
+jlong anrTimerCreate(JNIEnv* env, jobject jtimer, jstring jname) {
+ if (!nativeSupportEnabled) return 0;
+ AutoMutex _l(gAnrLock);
+ if (!gAnrArgs.ticker) {
+ gAnrArgs.ticker = new AnrTimerService::Ticker();
+ }
+ gAnrArgs.tickerUseCount++;
+
+ ScopedUtfChars name(env, jname);
+ jobject timer = env->NewWeakGlobalRef(jtimer);
+ AnrTimerService* service =
+ new AnrTimerService(name.c_str(), anrNotify, &gAnrArgs, timer, gAnrArgs.ticker);
+ return reinterpret_cast<jlong>(service);
+}
+
+AnrTimerService *toService(jlong pointer) {
+ return reinterpret_cast<AnrTimerService*>(pointer);
+}
+
+jint anrTimerClose(JNIEnv* env, jclass, jlong ptr) {
+ if (!nativeSupportEnabled) return -1;
+ if (ptr == 0) return -1;
+ AutoMutex _l(gAnrLock);
+ AnrTimerService *s = toService(ptr);
+ env->DeleteWeakGlobalRef(s->jtimer());
+ delete s;
+ if (--gAnrArgs.tickerUseCount <= 0) {
+ delete gAnrArgs.ticker;
+ gAnrArgs.ticker = nullptr;
+ }
+ return 0;
+}
+
+jint anrTimerStart(JNIEnv* env, jclass, jlong ptr,
+ jint pid, jint uid, jlong timeout, jboolean extend) {
+ if (!nativeSupportEnabled) return 0;
+ // On the Java side, timeouts are expressed in milliseconds and must be converted to
+ // nanoseconds before being passed to the library code.
+ return toService(ptr)->start(pid, uid, milliseconds_to_nanoseconds(timeout), extend);
+}
+
+jboolean anrTimerCancel(JNIEnv* env, jclass, jlong ptr, jint timerId) {
+ if (!nativeSupportEnabled) return false;
+ return toService(ptr)->cancel(timerId);
+}
+
+jboolean anrTimerAccept(JNIEnv* env, jclass, jlong ptr, jint timerId) {
+ if (!nativeSupportEnabled) return false;
+ return toService(ptr)->accept(timerId);
+}
+
+jboolean anrTimerDiscard(JNIEnv* env, jclass, jlong ptr, jint timerId) {
+ if (!nativeSupportEnabled) return false;
+ return toService(ptr)->discard(timerId);
+}
+
+jint anrTimerDump(JNIEnv *env, jclass, jlong ptr, jboolean verbose) {
+ if (!nativeSupportEnabled) return -1;
+ toService(ptr)->dump(verbose);
+ return 0;
+}
+
+static const JNINativeMethod methods[] = {
+ {"nativeAnrTimerSupported", "()Z", (void*) anrTimerSupported},
+ {"nativeAnrTimerCreate", "(Ljava/lang/String;)J", (void*) anrTimerCreate},
+ {"nativeAnrTimerClose", "(J)I", (void*) anrTimerClose},
+ {"nativeAnrTimerStart", "(JIIJZ)I", (void*) anrTimerStart},
+ {"nativeAnrTimerCancel", "(JI)Z", (void*) anrTimerCancel},
+ {"nativeAnrTimerAccept", "(JI)Z", (void*) anrTimerAccept},
+ {"nativeAnrTimerDiscard", "(JI)Z", (void*) anrTimerDiscard},
+ {"nativeAnrTimerDump", "(JZ)V", (void*) anrTimerDump},
+};
+
+} // anonymous namespace
+
+int register_android_server_utils_AnrTimer(JNIEnv* env)
+{
+ static const char *className = "com/android/server/utils/AnrTimer";
+ jniRegisterNativeMethods(env, className, methods, NELEM(methods));
+
+ jclass service = FindClassOrDie(env, className);
+ gAnrArgs.clazz = MakeGlobalRefOrDie(env, service);
+ gAnrArgs.func = env->GetMethodID(gAnrArgs.clazz, "expire", "(III)Z");
+ env->GetJavaVM(&gAnrArgs.vm);
+
+ nativeSupportEnabled = NATIVE_SUPPORT;
+
+ return 0;
+}
+
+} // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 11734da..f3158d1 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -52,6 +52,7 @@
int register_android_server_HardwarePropertiesManagerService(JNIEnv* env);
int register_android_server_SyntheticPasswordManager(JNIEnv* env);
int register_android_hardware_display_DisplayViewport(JNIEnv* env);
+int register_android_server_utils_AnrTimer(JNIEnv *env);
int register_android_server_am_OomConnection(JNIEnv* env);
int register_android_server_am_CachedAppOptimizer(JNIEnv* env);
int register_android_server_am_LowMemDetector(JNIEnv* env);
@@ -113,6 +114,7 @@
register_android_server_storage_AppFuse(env);
register_android_server_SyntheticPasswordManager(env);
register_android_hardware_display_DisplayViewport(env);
+ register_android_server_utils_AnrTimer(env);
register_android_server_am_OomConnection(env);
register_android_server_am_CachedAppOptimizer(env);
register_android_server_am_LowMemDetector(env);
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index 6899ad4..31409ab 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -109,7 +109,7 @@
PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
/*defaultProviderId=*/flattenedPrimaryProviders),
- providerDataList);
+ providerDataList, /*isRequestForAllOptions=*/ false);
mClientCallback.onPendingIntent(mPendingIntent);
} catch (RemoteException e) {
mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
index 3c190bf..f092dcc 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
@@ -150,9 +150,12 @@
*
* @param requestInfo the information about the request
* @param providerDataList the list of provider data from remote providers
+ * @param isRequestForAllOptions whether the bottom sheet should directly navigate to the
+ * all options page
*/
public PendingIntent createPendingIntent(
- RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) {
+ RequestInfo requestInfo, ArrayList<ProviderData> providerDataList,
+ boolean isRequestForAllOptions) {
List<CredentialProviderInfo> allProviders =
CredentialProviderInfoFactory.getCredentialProviderServices(
mContext,
@@ -168,7 +171,8 @@
disabledProvider.getComponentName().flattenToString())).toList();
Intent intent = IntentFactory.createCredentialSelectorIntent(requestInfo, providerDataList,
- new ArrayList<>(disabledProviderDataList), mResultReceiver)
+ new ArrayList<>(disabledProviderDataList), mResultReceiver,
+ isRequestForAllOptions)
.setAction(UUID.randomUUID().toString());
//TODO: Create unique pending intent using request code and cancel any pre-existing pending
// intents
diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
index ca5600e..d165171 100644
--- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
@@ -106,7 +106,8 @@
mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)),
- providerDataList);
+ providerDataList,
+ /*isRequestForAllOptions=*/ true);
List<GetCredentialProviderData> candidateProviderDataList = new ArrayList<>();
for (ProviderData providerData : providerDataList) {
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index c9e691e..3f57c80 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -99,21 +99,24 @@
protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
mRequestSessionMetric.collectUiCallStartTime(System.nanoTime());
mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.USER_INTERACTION);
- Binder.withCleanCallingIdentity(()-> {
- try {
+ Binder.withCleanCallingIdentity(() -> {
+ try {
cancelExistingPendingIntent();
- mPendingIntent = mCredentialManagerUi.createPendingIntent(
- RequestInfo.newGetRequestInfo(
- mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
- PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
- Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)),
- providerDataList);
- mClientCallback.onPendingIntent(mPendingIntent);
- } catch (RemoteException e) {
- mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
- mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.TERMINATED);
- String exception = GetCredentialException.TYPE_UNKNOWN;
- mRequestSessionMetric.collectFrameworkException(exception);
+ mPendingIntent = mCredentialManagerUi.createPendingIntent(
+ RequestInfo.newGetRequestInfo(
+ mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
+ PermissionUtils.hasPermission(mContext,
+ mClientAppInfo.getPackageName(),
+ Manifest.permission
+ .CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)),
+ providerDataList,
+ /*isRequestForAllOptions=*/ false);
+ mClientCallback.onPendingIntent(mPendingIntent);
+ } catch (RemoteException e) {
+ mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
+ mCredentialManagerUi.setStatus(CredentialManagerUi.UiStatus.TERMINATED);
+ String exception = GetCredentialException.TYPE_UNKNOWN;
+ mRequestSessionMetric.collectFrameworkException(exception);
respondToClientWithErrorAndFinish(exception, "Unable to instantiate selector");
}
});
diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
index f447c1f..fbfc9ca 100644
--- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
@@ -192,7 +192,7 @@
mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)),
- providerDataList);
+ providerDataList, /*isRequestForAllOptions=*/ false);
} else {
return null;
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index a490013..f288103 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -6243,9 +6243,8 @@
final long id = mInjector.binderClearCallingIdentity();
try {
- final KeyChainConnection keyChainConnection =
- KeyChain.bindAsUser(mContext, caller.getUserHandle());
- try {
+ try (KeyChainConnection keyChainConnection =
+ KeyChain.bindAsUser(mContext, caller.getUserHandle())) {
IKeyChainService keyChain = keyChainConnection.getService();
if (!keyChain.installKeyPair(privKey, cert, chain, alias, KeyStore.UID_SELF)) {
logInstallKeyPairFailure(caller, isCredentialManagementApp);
@@ -6263,10 +6262,8 @@
? CREDENTIAL_MANAGEMENT_APP : NOT_CREDENTIAL_MANAGEMENT_APP)
.write();
return true;
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Installing certificate", e);
- } finally {
- keyChainConnection.close();
}
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while installing certificate", e);
@@ -6313,9 +6310,8 @@
final long id = Binder.clearCallingIdentity();
try {
- final KeyChainConnection keyChainConnection =
- KeyChain.bindAsUser(mContext, caller.getUserHandle());
- try {
+ try (KeyChainConnection keyChainConnection =
+ KeyChain.bindAsUser(mContext, caller.getUserHandle())) {
IKeyChainService keyChain = keyChainConnection.getService();
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.REMOVE_KEY_PAIR)
@@ -6325,10 +6321,8 @@
? CREDENTIAL_MANAGEMENT_APP : NOT_CREDENTIAL_MANAGEMENT_APP)
.write();
return keyChain.removeKeyPair(alias);
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Removing keypair", e);
- } finally {
- keyChainConnection.close();
}
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while removing keypair", e);
@@ -6355,7 +6349,7 @@
try (KeyChainConnection keyChainConnection =
KeyChain.bindAsUser(mContext, caller.getUserHandle())) {
return keyChainConnection.getService().containsKeyPair(alias);
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Querying keypair", e);
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while querying keypair", e);
@@ -6417,7 +6411,7 @@
}
}
return false;
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Querying grant to wifi auth.", e);
return false;
}
@@ -6497,7 +6491,7 @@
}
result.put(uid, new ArraySet<String>(packages));
}
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Querying keypair grants", e);
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while querying keypair grants", e);
@@ -6667,7 +6661,7 @@
.write();
return true;
}
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "KeyChain error while generating a keypair", e);
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while generating keypair", e);
@@ -6742,7 +6736,7 @@
} catch (InterruptedException e) {
Slogf.w(LOG_TAG, "Interrupted while setting keypair certificate", e);
Thread.currentThread().interrupt();
- } catch (RemoteException e) {
+ } catch (RemoteException | AssertionError e) {
Slogf.e(LOG_TAG, "Failed setting keypair certificate", e);
} finally {
mInjector.binderRestoreCallingIdentity(id);
@@ -7227,7 +7221,7 @@
connection.getService().getCredentialManagementAppPolicy();
return policy != null && !policy.getAppAndUriMappings().isEmpty()
&& containsAlias(policy, alias);
- } catch (RemoteException | InterruptedException e) {
+ } catch (RemoteException | InterruptedException | AssertionError e) {
return false;
}
});
diff --git a/services/tests/servicestests/jni/Android.bp b/services/tests/servicestests/jni/Android.bp
index 174beb8..c30e4eb 100644
--- a/services/tests/servicestests/jni/Android.bp
+++ b/services/tests/servicestests/jni/Android.bp
@@ -23,6 +23,7 @@
":lib_cachedAppOptimizer_native",
":lib_gameManagerService_native",
":lib_oomConnection_native",
+ ":lib_anrTimer_native",
"onload.cpp",
],
@@ -55,4 +56,4 @@
"android.hardware.graphics.mapper@4.0",
"android.hidl.token@1.0-utils",
],
-}
\ No newline at end of file
+}
diff --git a/services/tests/servicestests/jni/onload.cpp b/services/tests/servicestests/jni/onload.cpp
index f160b3d..25487c5 100644
--- a/services/tests/servicestests/jni/onload.cpp
+++ b/services/tests/servicestests/jni/onload.cpp
@@ -27,6 +27,7 @@
int register_android_server_am_CachedAppOptimizer(JNIEnv* env);
int register_android_server_app_GameManagerService(JNIEnv* env);
int register_android_server_am_OomConnection(JNIEnv* env);
+int register_android_server_utils_AnrTimer(JNIEnv *env);
};
using namespace android;
@@ -44,5 +45,6 @@
register_android_server_am_CachedAppOptimizer(env);
register_android_server_app_GameManagerService(env);
register_android_server_am_OomConnection(env);
+ register_android_server_utils_AnrTimer(env);
return JNI_VERSION_1_4;
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
index efcdbd4..1cd61e9 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
@@ -44,10 +44,6 @@
import android.graphics.PointF;
import android.os.Looper;
import android.os.SystemClock;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.testing.DexmakerShareClassLoaderRule;
import android.view.InputDevice;
import android.view.MotionEvent;
@@ -60,7 +56,6 @@
import com.android.server.accessibility.AccessibilityManagerService;
import com.android.server.accessibility.AccessibilityTraceManager;
import com.android.server.accessibility.EventStreamTransformation;
-import com.android.server.accessibility.Flags;
import com.android.server.accessibility.utils.GestureLogParser;
import com.android.server.testutils.OffsettableClock;
@@ -81,7 +76,6 @@
import java.util.ArrayList;
import java.util.List;
-
@RunWith(AndroidJUnit4.class)
public class TouchExplorerTest {
@@ -125,9 +119,6 @@
public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
new DexmakerShareClassLoaderRule();
- @Rule
- public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
/**
* {@link TouchExplorer#sendDownForAllNotInjectedPointers} injecting events with the same object
* is resulting {@link ArgumentCaptor} to capture events with last state. Before implementation
@@ -170,16 +161,11 @@
goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER);
// Wait for transiting to touch exploring state.
mHandler.fastForward(2 * USER_INTENT_TIMEOUT);
- assertState(STATE_TOUCH_EXPLORING);
- // Manually construct the next move event. Using moveEachPointers() will batch the move
- // event which produces zero movement for some reason.
- float[] x = new float[1];
- float[] y = new float[1];
- x[0] = mLastEvent.getX(0) + mTouchSlop;
- y[0] = mLastEvent.getY(0) + mTouchSlop;
- send(manyPointerEvent(ACTION_MOVE, x, y));
+ moveEachPointers(mLastEvent, p(10, 10));
+ send(mLastEvent);
goToStateClearFrom(STATE_TOUCH_EXPLORING_1FINGER);
assertCapturedEvents(ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT);
+ assertState(STATE_TOUCH_EXPLORING);
}
/**
@@ -187,8 +173,7 @@
* change the coordinates.
*/
@Test
- @RequiresFlagsEnabled(Flags.FLAG_REDUCE_TOUCH_EXPLORATION_SENSITIVITY)
- public void testOneFingerMoveWithExtraMoveEvents_generatesOneMoveEvent() {
+ public void testOneFingerMoveWithExtraMoveEvents() {
goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER);
// Inject a set of move events that have the same coordinates as the down event.
moveEachPointers(mLastEvent, p(0, 0));
@@ -196,33 +181,7 @@
// Wait for transition to touch exploring state.
mHandler.fastForward(2 * USER_INTENT_TIMEOUT);
// Now move for real.
- moveAtLeastTouchSlop(mLastEvent);
- send(mLastEvent);
- // One more move event with no change.
- moveEachPointers(mLastEvent, p(0, 0));
- send(mLastEvent);
- goToStateClearFrom(STATE_TOUCH_EXPLORING_1FINGER);
- assertCapturedEvents(
- ACTION_HOVER_ENTER,
- ACTION_HOVER_MOVE,
- ACTION_HOVER_EXIT);
- }
-
- /**
- * Test the case where ACTION_DOWN is followed by a number of ACTION_MOVE events that do not
- * change the coordinates.
- */
- @Test
- @RequiresFlagsDisabled(Flags.FLAG_REDUCE_TOUCH_EXPLORATION_SENSITIVITY)
- public void testOneFingerMoveWithExtraMoveEvents_generatesThreeMoveEvent() {
- goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER);
- // Inject a set of move events that have the same coordinates as the down event.
- moveEachPointers(mLastEvent, p(0, 0));
- send(mLastEvent);
- // Wait for transition to touch exploring state.
- mHandler.fastForward(2 * USER_INTENT_TIMEOUT);
- // Now move for real.
- moveAtLeastTouchSlop(mLastEvent);
+ moveEachPointers(mLastEvent, p(10, 10));
send(mLastEvent);
// One more move event with no change.
moveEachPointers(mLastEvent, p(0, 0));
@@ -283,7 +242,7 @@
moveEachPointers(mLastEvent, p(0, 0), p(0, 0));
send(mLastEvent);
// Now move for real.
- moveEachPointers(mLastEvent, p(mTouchSlop, mTouchSlop), p(mTouchSlop, mTouchSlop));
+ moveEachPointers(mLastEvent, p(10, 10), p(10, 10));
send(mLastEvent);
goToStateClearFrom(STATE_DRAGGING_2FINGERS);
assertCapturedEvents(ACTION_DOWN, ACTION_MOVE, ACTION_MOVE, ACTION_MOVE, ACTION_UP);
@@ -292,7 +251,7 @@
@Test
public void testUpEvent_OneFingerMove_clearStateAndInjectHoverEvents() {
goFromStateClearTo(STATE_TOUCH_EXPLORING_1FINGER);
- moveAtLeastTouchSlop(mLastEvent);
+ moveEachPointers(mLastEvent, p(10, 10));
send(mLastEvent);
// Wait 10 ms to make sure that hover enter and exit are not scheduled for the same moment.
mHandler.fastForward(10);
@@ -318,7 +277,7 @@
// Wait for the finger moving to the second view.
mHandler.fastForward(oneThirdUserIntentTimeout);
- moveAtLeastTouchSlop(mLastEvent);
+ moveEachPointers(mLastEvent, p(10, 10));
send(mLastEvent);
// Wait for the finger lifting from the second view.
@@ -443,6 +402,7 @@
// Manually construct the next move event. Using moveEachPointers() will batch the move
// event onto the pointer up event which will mean that the move event still has a pointer
// count of 3.
+ // Todo: refactor to avoid using batching as there is no special reason to do it that way.
float[] x = new float[2];
float[] y = new float[2];
x[0] = mLastEvent.getX(0) + 100;
@@ -774,9 +734,6 @@
}
}
- private void moveAtLeastTouchSlop(MotionEvent event) {
- moveEachPointers(event, p(2 * mTouchSlop, 0));
- }
/**
* A {@link android.os.Handler} that doesn't process messages until {@link #fastForward(int)} is
* invoked.
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java
index 59c94dc..89a4961 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java
@@ -65,7 +65,6 @@
import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintResetLockoutClient;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
@@ -248,7 +247,6 @@
}
@Test
- @Ignore("b/317403648")
public void lockoutPermanentResetViaClient() {
setLockoutPermanent();
diff --git a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
index 861d14a..6c085e0 100644
--- a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
+++ b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
@@ -23,17 +23,21 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.util.Log;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.SmallTest;
import com.android.internal.annotations.GuardedBy;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -45,6 +49,9 @@
@RunWith(Parameterized.class)
public class AnrTimerTest {
+ // A log tag.
+ private static final String TAG = "AnrTimerTest";
+
// The commonly used message timeout key.
private static final int MSG_TIMEOUT = 1;
@@ -63,9 +70,7 @@
}
}
- /**
- * The test handler is a self-contained object for a single test.
- */
+ /** The test helper is a self-contained object for a single test. */
private static class Helper {
final Object mLock = new Object();
@@ -114,7 +119,7 @@
/**
* Force AnrTimer to use the test parameter for the feature flag.
*/
- class TestInjector extends AnrTimer.Injector {
+ private class TestInjector extends AnrTimer.Injector {
@Override
boolean anrTimerServiceEnabled() {
return mEnabled;
@@ -124,9 +129,9 @@
/**
* An instrumented AnrTimer.
*/
- private static class TestAnrTimer extends AnrTimer<TestArg> {
+ private class TestAnrTimer extends AnrTimer<TestArg> {
private TestAnrTimer(Handler h, int key, String tag) {
- super(h, key, tag);
+ super(h, key, tag, false, new TestInjector());
}
TestAnrTimer(Helper helper) {
@@ -173,35 +178,103 @@
@Test
public void testSimpleTimeout() throws Exception {
Helper helper = new Helper(1);
- TestAnrTimer timer = new TestAnrTimer(helper);
- TestArg t = new TestArg(1, 1);
- timer.start(t, 10);
- // Delivery is immediate but occurs on a different thread.
- assertTrue(helper.await(5000));
- TestArg[] result = helper.messages(1);
- validate(t, result[0]);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ // One-time check that the injector is working as expected.
+ assertEquals(mEnabled, timer.serviceEnabled());
+ TestArg t = new TestArg(1, 1);
+ timer.start(t, 10);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(1);
+ validate(t, result[0]);
+ }
}
/**
- * Verify that if three timers are scheduled, they are delivered in time order.
+ * Verify that a restarted timer is delivered exactly once. The initial timer value is very
+ * large, to ensure it does not expire before the timer can be restarted.
+ */
+ @Test
+ public void testTimerRestart() throws Exception {
+ Helper helper = new Helper(1);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ TestArg t = new TestArg(1, 1);
+ timer.start(t, 10000);
+ // Briefly pause.
+ assertFalse(helper.await(10));
+ timer.start(t, 10);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(1);
+ validate(t, result[0]);
+ }
+ }
+
+ /**
+ * Verify that a restarted timer is delivered exactly once. The initial timer value is very
+ * large, to ensure it does not expire before the timer can be restarted.
+ */
+ @Test
+ public void testTimerZero() throws Exception {
+ Helper helper = new Helper(1);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ TestArg t = new TestArg(1, 1);
+ timer.start(t, 0);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(1);
+ validate(t, result[0]);
+ }
+ }
+
+ /**
+ * Verify that if three timers are scheduled on a single AnrTimer, they are delivered in time
+ * order.
*/
@Test
public void testMultipleTimers() throws Exception {
// Expect three messages.
Helper helper = new Helper(3);
- TestAnrTimer timer = new TestAnrTimer(helper);
TestArg t1 = new TestArg(1, 1);
TestArg t2 = new TestArg(1, 2);
TestArg t3 = new TestArg(1, 3);
- timer.start(t1, 50);
- timer.start(t2, 60);
- timer.start(t3, 40);
- // Delivery is immediate but occurs on a different thread.
- assertTrue(helper.await(5000));
- TestArg[] result = helper.messages(3);
- validate(t3, result[0]);
- validate(t1, result[1]);
- validate(t2, result[2]);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ timer.start(t1, 50);
+ timer.start(t2, 60);
+ timer.start(t3, 40);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(3);
+ validate(t3, result[0]);
+ validate(t1, result[1]);
+ validate(t2, result[2]);
+ }
+ }
+
+ /**
+ * Verify that if three timers are scheduled on three separate AnrTimers, they are delivered
+ * in time order.
+ */
+ @Test
+ public void testMultipleServices() throws Exception {
+ // Expect three messages.
+ Helper helper = new Helper(3);
+ TestArg t1 = new TestArg(1, 1);
+ TestArg t2 = new TestArg(1, 2);
+ TestArg t3 = new TestArg(1, 3);
+ try (TestAnrTimer x1 = new TestAnrTimer(helper);
+ TestAnrTimer x2 = new TestAnrTimer(helper);
+ TestAnrTimer x3 = new TestAnrTimer(helper)) {
+ x1.start(t1, 50);
+ x2.start(t2, 60);
+ x3.start(t3, 40);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(3);
+ validate(t3, result[0]);
+ validate(t1, result[1]);
+ validate(t2, result[2]);
+ }
}
/**
@@ -211,20 +284,109 @@
public void testCancelTimer() throws Exception {
// Expect two messages.
Helper helper = new Helper(2);
- TestAnrTimer timer = new TestAnrTimer(helper);
TestArg t1 = new TestArg(1, 1);
TestArg t2 = new TestArg(1, 2);
TestArg t3 = new TestArg(1, 3);
- timer.start(t1, 50);
- timer.start(t2, 60);
- timer.start(t3, 40);
- // Briefly pause.
- assertFalse(helper.await(10));
- timer.cancel(t1);
- // Delivery is immediate but occurs on a different thread.
- assertTrue(helper.await(5000));
- TestArg[] result = helper.messages(2);
- validate(t3, result[0]);
- validate(t2, result[1]);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ timer.start(t1, 50);
+ timer.start(t2, 60);
+ timer.start(t3, 40);
+ // Briefly pause.
+ assertFalse(helper.await(10));
+ timer.cancel(t1);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(2);
+ validate(t3, result[0]);
+ validate(t2, result[1]);
+ }
+ }
+
+ /**
+ * Return the dump string.
+ */
+ private String getDumpOutput() {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ AnrTimer.dump(pw, true, new TestInjector());
+ pw.close();
+ return sw.getBuffer().toString();
+ }
+
+ /**
+ * Verify the dump output.
+ */
+ @Test
+ public void testDumpOutput() throws Exception {
+ String r1 = getDumpOutput();
+ assertEquals(false, r1.contains("timer:"));
+
+ Helper helper = new Helper(2);
+ TestArg t1 = new TestArg(1, 1);
+ TestArg t2 = new TestArg(1, 2);
+ TestArg t3 = new TestArg(1, 3);
+ try (TestAnrTimer timer = new TestAnrTimer(helper)) {
+ timer.start(t1, 5000);
+ timer.start(t2, 5000);
+ timer.start(t3, 5000);
+
+ String r2 = getDumpOutput();
+ // There are timers in the list if and only if the feature is enabled.
+ final boolean expected = mEnabled;
+ assertEquals(expected, r2.contains("timer:"));
+ }
+
+ String r3 = getDumpOutput();
+ assertEquals(false, r3.contains("timer:"));
+ }
+
+ /**
+ * Verify that GC works as expected. This test will almost certainly be flaky, since it
+ * relies on the finalizers running, which is a best-effort on the part of the JVM.
+ * Therefore, the test is marked @Ignore. Remove that annotation to run the test locally.
+ */
+ @Ignore
+ @Test
+ public void testGarbageCollection() throws Exception {
+ if (!mEnabled) return;
+
+ String r1 = getDumpOutput();
+ assertEquals(false, r1.contains("timer:"));
+
+ Helper helper = new Helper(2);
+ TestArg t1 = new TestArg(1, 1);
+ TestArg t2 = new TestArg(1, 2);
+ TestArg t3 = new TestArg(1, 3);
+ // The timer is explicitly not closed. It is, however, scoped to the next block.
+ {
+ TestAnrTimer timer = new TestAnrTimer(helper);
+ timer.start(t1, 5000);
+ timer.start(t2, 5000);
+ timer.start(t3, 5000);
+
+ String r2 = getDumpOutput();
+ // There are timers in the list if and only if the feature is enabled.
+ final boolean expected = mEnabled;
+ assertEquals(expected, r2.contains("timer:"));
+ }
+
+ // Try to make finalizers run. The timer object above should be a candidate. Finalizers
+ // are run on their own thread, so pause this thread to give that thread some time.
+ String r3 = getDumpOutput();
+ for (int i = 0; i < 10 && r3.contains("timer:"); i++) {
+ Log.i(TAG, "requesting finalization " + i);
+ System.gc();
+ System.runFinalization();
+ Thread.sleep(4 * 1000);
+ r3 = getDumpOutput();
+ }
+
+ // The timer was not explicitly closed but it should have been implicitly closed by GC.
+ assertEquals(false, r3.contains("timer:"));
+ }
+
+ // TODO: [b/302724778] Remove manual JNI load
+ static {
+ System.loadLibrary("servicestestjni");
}
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 39779b0..f1edd9a 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -303,7 +303,6 @@
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
@@ -14061,7 +14060,6 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne()
throws RemoteException {
mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
@@ -14073,7 +14071,8 @@
mService.addNotification(nr1);
// Create old notification.
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel specific notifications via listener.
@@ -14091,16 +14090,17 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_old_cancelOne() throws RemoteException {
mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
.FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
// Create old notifications.
- final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr1);
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel specific notifications via listener.
@@ -14119,7 +14119,6 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne_flagDisabled()
throws RemoteException {
mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
@@ -14131,7 +14130,8 @@
mService.addNotification(nr1);
// Create old notification.
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel specific notifications via listener.
@@ -14150,7 +14150,6 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll()
throws RemoteException {
mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
@@ -14162,7 +14161,8 @@
mService.addNotification(nr1);
// Create old notification.
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel all notifications via listener.
@@ -14179,16 +14179,17 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_old_cancelAll() throws RemoteException {
mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
.FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
// Create old notifications.
- final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr1);
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel all notifications via listener.
@@ -14206,7 +14207,6 @@
}
@Test
- @Ignore("b/316989461")
public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll_flagDisabled()
throws RemoteException {
mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
@@ -14218,7 +14218,8 @@
mService.addNotification(nr1);
// Create old notification.
- final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
+ final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel,
+ System.currentTimeMillis() - 60000);
mService.addNotification(nr2);
// Cancel all notifications via listener.
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 8a9c05d..c82f751 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -88,6 +88,7 @@
import android.app.ActivityOptions;
import android.app.AppOpsManager;
import android.app.BackgroundStartPrivileges;
+import android.app.ComponentOptions.BackgroundActivityStartMode;
import android.app.IApplicationThread;
import android.app.PictureInPictureParams;
import android.content.ComponentName;
@@ -914,24 +915,78 @@
.mockStatic(FrameworkStatsLog.class)
.strictness(Strictness.LENIENT)
.startMocking();
- doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission(
- eq(START_ACTIVITIES_FROM_BACKGROUND),
- anyInt(), anyInt()));
- runAndVerifyBackgroundActivityStartsSubtest(
- "allowed_notAborted", false,
- UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
- UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
- false, true, false, false, false, false, false, false);
- verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
- "", // activity name
- BackgroundActivityStartController.BAL_ALLOW_PERMISSION,
- UNIMPORTANT_UID,
- UNIMPORTANT_UID2));
- mockingSession.finishMocking();
+ try {
+ doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission(
+ eq(START_ACTIVITIES_FROM_BACKGROUND),
+ anyInt(), anyInt()));
+ runAndVerifyBackgroundActivityStartsSubtest(
+ "allowed_notAborted", false,
+ UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+ UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
+ false, true, false, false, false, false, false, false);
+ verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
+ "", // activity name
+ BackgroundActivityStartController.BAL_ALLOW_PERMISSION,
+ UNIMPORTANT_UID,
+ UNIMPORTANT_UID2,
+ BackgroundActivityStartController.BAL_ALLOW_PERMISSION,
+ true, // opt in
+ false, // but no explicit opt in
+ BackgroundActivityStartController.BAL_BLOCK,
+ true, // opt in
+ false // but no explicit opt in
+ ));
+ } finally {
+ mockingSession.finishMocking();
+ }
}
/**
- * This test ensures proper logging for BAL_ALLOW_PENDING_INTENT.
+ * This test ensures proper logging for BAL_ALLOW_PENDING_INTENT, when the PendingIntent sender
+ * is the only reason BAL is allowed.
+ */
+ @Test
+ public void testBackgroundActivityStartsAllowed_loggingOnlyPendingIntentAllowed() {
+ doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
+ MockitoSession mockingSession = mockitoSession()
+ .mockStatic(ActivityTaskManagerService.class)
+ .mockStatic(FrameworkStatsLog.class)
+ .mockStatic(PendingIntentRecord.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+ try {
+ doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission(
+ eq(START_ACTIVITIES_FROM_BACKGROUND),
+ anyInt(), anyInt()));
+ doReturn(BackgroundStartPrivileges.allowBackgroundActivityStarts(null)).when(
+ () -> PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
+ anyObject(), anyInt(), anyObject()));
+ runAndVerifyBackgroundActivityStartsSubtest(
+ "allowed_notAborted", false,
+ UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+ Process.SYSTEM_UID, true, PROCESS_STATE_BOUND_TOP,
+ false, true, false, false, false, false, false, false,
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED);
+ verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
+ DEFAULT_COMPONENT_PACKAGE_NAME + "/" + DEFAULT_COMPONENT_PACKAGE_NAME,
+ BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT,
+ UNIMPORTANT_UID,
+ Process.SYSTEM_UID,
+ BackgroundActivityStartController.BAL_ALLOW_PERMISSION,
+ false, // opt in
+ true, // explicit opt out
+ BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW,
+ true, // opt in
+ false // but no explicit opt in
+ ));
+ } finally {
+ mockingSession.finishMocking();
+ }
+ }
+
+ /**
+ * This test ensures proper logging for BAL_ALLOW_PENDING_INTENT, when the PendingIntent sender
+ * is not the primary reason to allow BAL (but the creator).
*/
@Test
public void testBackgroundActivityStartsAllowed_loggingPendingIntentAllowed() {
@@ -942,23 +997,34 @@
.mockStatic(PendingIntentRecord.class)
.strictness(Strictness.LENIENT)
.startMocking();
- doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission(
- eq(START_ACTIVITIES_FROM_BACKGROUND),
- anyInt(), anyInt()));
- doReturn(BackgroundStartPrivileges.allowBackgroundActivityStarts(null)).when(
- () -> PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
- anyObject(), anyInt(), anyObject()));
- runAndVerifyBackgroundActivityStartsSubtest(
- "allowed_notAborted", false,
- UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
- Process.SYSTEM_UID, true, PROCESS_STATE_BOUND_TOP,
- false, true, false, false, false, false, false, false);
- verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
- DEFAULT_COMPONENT_PACKAGE_NAME + "/" + DEFAULT_COMPONENT_PACKAGE_NAME,
- BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT,
- UNIMPORTANT_UID,
- Process.SYSTEM_UID));
- mockingSession.finishMocking();
+ try {
+ doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission(
+ eq(START_ACTIVITIES_FROM_BACKGROUND),
+ anyInt(), anyInt()));
+ doReturn(BackgroundStartPrivileges.allowBackgroundActivityStarts(null)).when(
+ () -> PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
+ anyObject(), anyInt(), anyObject()));
+ runAndVerifyBackgroundActivityStartsSubtest(
+ "allowed_notAborted", false,
+ UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+ Process.SYSTEM_UID, true, PROCESS_STATE_BOUND_TOP,
+ false, true, false, false, false, false, false, false,
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+ verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
+ "",
+ BackgroundActivityStartController.BAL_ALLOW_PERMISSION,
+ UNIMPORTANT_UID,
+ Process.SYSTEM_UID,
+ BackgroundActivityStartController.BAL_ALLOW_PERMISSION,
+ true, // opt in
+ true, // explicit opt in
+ BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW,
+ true, // opt in
+ false // but no explicit opt in
+ ));
+ } finally {
+ mockingSession.finishMocking();
+ }
}
private void runAndVerifyBackgroundActivityStartsSubtest(String name, boolean shouldHaveAborted,
@@ -971,6 +1037,27 @@
boolean isCallingUidAffiliatedProfileOwner,
boolean isPinnedSingleInstance,
boolean hasSystemExemptAppOp) {
+ runAndVerifyBackgroundActivityStartsSubtest(name, shouldHaveAborted, callingUid,
+ callingUidHasVisibleWindow, callingUidProcState, realCallingUid,
+ realCallingUidHasVisibleWindow, realCallingUidProcState, hasForegroundActivities,
+ callerIsRecents, callerIsTempAllowed,
+ callerIsInstrumentingWithBackgroundActivityStartPrivileges,
+ isCallingUidDeviceOwner, isCallingUidAffiliatedProfileOwner, isPinnedSingleInstance,
+ hasSystemExemptAppOp,
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
+ }
+
+ private void runAndVerifyBackgroundActivityStartsSubtest(String name, boolean shouldHaveAborted,
+ int callingUid, boolean callingUidHasVisibleWindow, int callingUidProcState,
+ int realCallingUid, boolean realCallingUidHasVisibleWindow, int realCallingUidProcState,
+ boolean hasForegroundActivities, boolean callerIsRecents,
+ boolean callerIsTempAllowed,
+ boolean callerIsInstrumentingWithBackgroundActivityStartPrivileges,
+ boolean isCallingUidDeviceOwner,
+ boolean isCallingUidAffiliatedProfileOwner,
+ boolean isPinnedSingleInstance,
+ boolean hasSystemExemptAppOp,
+ @BackgroundActivityStartMode int pendingIntentCreatorBackgroundActivityStartMode) {
// window visibility
doReturn(callingUidHasVisibleWindow).when(mAtm).hasActiveVisibleWindow(callingUid);
doReturn(realCallingUidHasVisibleWindow).when(mAtm).hasActiveVisibleWindow(realCallingUid);
@@ -1022,7 +1109,10 @@
launchMode = LAUNCH_SINGLE_INSTANCE;
}
- final ActivityOptions options = spy(ActivityOptions.makeBasic());
+ ActivityOptions rawOptions = ActivityOptions.makeBasic()
+ .setPendingIntentCreatorBackgroundActivityStartMode(
+ pendingIntentCreatorBackgroundActivityStartMode);
+ final ActivityOptions options = spy(rawOptions);
ActivityRecord[] outActivity = new ActivityRecord[1];
ActivityStarter starter = prepareStarter(
FLAG_ACTIVITY_NEW_TASK, true, launchMode)
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 6497ee9..782d89c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -115,6 +115,9 @@
import android.os.RemoteException;
import android.os.SystemClock;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.view.Display;
@@ -146,6 +149,7 @@
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.wm.utils.WmDisplayCutout;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -172,6 +176,10 @@
@RunWith(WindowTestRunner.class)
public class DisplayContentTests extends WindowTestsBase {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+
@SetupWindows(addAllCommonWindows = true)
@Test
public void testForAllWindows() {
@@ -508,6 +516,7 @@
* Tests tapping on a root task in different display results in window gaining focus.
*/
@Test
+ @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_REMOVE_POINTER_EVENT_TRACKING_IN_WM)
public void testInputEventBringsCorrectDisplayInFocus() {
DisplayContent dc0 = mWm.getDefaultDisplayContentLocked();
// Create a second display
diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java
index df349f8..c958aba 100644
--- a/telephony/java/android/telephony/SmsManager.java
+++ b/telephony/java/android/telephony/SmsManager.java
@@ -563,7 +563,10 @@
* raw pdu of the status report is in the extended data ("pdu").
*
* @throws IllegalArgumentException if destinationAddress or text are empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendTextMessage(
String destinationAddress, String scAddress, String text,
PendingIntent sentIntent, PendingIntent deliveryIntent) {
@@ -581,8 +584,11 @@
* Used for logging and diagnostics purposes. The id may be 0.
*
* @throws IllegalArgumentException if destinationAddress or text are empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendTextMessage(
@NonNull String destinationAddress, @Nullable String scAddress, @NonNull String text,
@Nullable PendingIntent sentIntent, @Nullable PendingIntent deliveryIntent,
@@ -788,12 +794,16 @@
* </p>
*
* @see #sendTextMessage(String, String, String, PendingIntent, PendingIntent)
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
@SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@RequiresPermission(allOf = {
android.Manifest.permission.MODIFY_PHONE_STATE,
android.Manifest.permission.SEND_SMS
})
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendTextMessageWithoutPersisting(
String destinationAddress, String scAddress, String text,
PendingIntent sentIntent, PendingIntent deliveryIntent) {
@@ -908,7 +918,10 @@
* {@link #RESULT_REMOTE_EXCEPTION} for error.
*
* @throws IllegalArgumentException if the format is invalid.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void injectSmsPdu(
byte[] pdu, @SmsMessage.Format String format, PendingIntent receivedIntent) {
if (!format.equals(SmsMessage.FORMAT_3GPP) && !format.equals(SmsMessage.FORMAT_3GPP2)) {
@@ -940,6 +953,7 @@
* @return an <code>ArrayList</code> of strings that, in order, comprise the original message.
* @throws IllegalArgumentException if text is null.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public ArrayList<String> divideMessage(String text) {
if (null == text) {
throw new IllegalArgumentException("text is null");
@@ -1046,7 +1060,10 @@
* extended data ("pdu").
*
* @throws IllegalArgumentException if destinationAddress or data are empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultipartTextMessage(
String destinationAddress, String scAddress, ArrayList<String> parts,
ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents) {
@@ -1062,8 +1079,10 @@
* Used for logging and diagnostics purposes. The id may be 0.
*
* @throws IllegalArgumentException if destinationAddress or data are empty
- *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultipartTextMessage(
@NonNull String destinationAddress, @Nullable String scAddress,
@NonNull List<String> parts, @Nullable List<PendingIntent> sentIntents,
@@ -1089,7 +1108,11 @@
*
* @param packageName serves as the default package name if the package name that is
* associated with the user id is null.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultipartTextMessage(
@NonNull String destinationAddress, @Nullable String scAddress,
@NonNull List<String> parts, @Nullable List<PendingIntent> sentIntents,
@@ -1191,10 +1214,14 @@
* </p>
*
* @see #sendMultipartTextMessage(String, String, ArrayList, ArrayList, ArrayList)
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
* @hide
**/
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultipartTextMessageWithoutPersisting(
String destinationAddress, String scAddress, List<String> parts,
List<PendingIntent> sentIntents, List<PendingIntent> deliveryIntents) {
@@ -1498,7 +1525,10 @@
* raw pdu of the status report is in the extended data ("pdu").
*
* @throws IllegalArgumentException if destinationAddress or data are empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendDataMessage(
String destinationAddress, String scAddress, short destinationPort,
byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
@@ -1609,6 +1639,7 @@
* .{@link #createForSubscriptionId createForSubscriptionId(subId)} instead
*/
@Deprecated
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public static SmsManager getSmsManagerForSubscriptionId(int subId) {
return getSmsManagerForContextAndSubscriptionId(null, subId);
}
@@ -1626,6 +1657,7 @@
* @see SubscriptionManager#getActiveSubscriptionInfoList()
* @see SubscriptionManager#getDefaultSmsSubscriptionId()
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public @NonNull SmsManager createForSubscriptionId(int subId) {
return getSmsManagerForContextAndSubscriptionId(mContext, subId);
}
@@ -1651,7 +1683,11 @@
* @return associated subscription ID or {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID} if
* the default subscription id cannot be determined or the device has multiple active
* subscriptions and and no default is set ("ask every time") by the user.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public int getSubscriptionId() {
try {
return (mSubId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)
@@ -2018,10 +2054,14 @@
*
* @throws IllegalArgumentException if endMessageId < startMessageId
* @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} instead.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
* {@hide}
*/
@Deprecated
@SystemApi
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public boolean enableCellBroadcastRange(int startMessageId, int endMessageId,
@android.telephony.SmsCbMessage.MessageFormat int ranType) {
boolean success = false;
@@ -2079,11 +2119,15 @@
* @see #enableCellBroadcastRange(int, int, int)
*
* @throws IllegalArgumentException if endMessageId < startMessageId
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
+ *
* @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} instead.
* {@hide}
*/
@Deprecated
@SystemApi
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public boolean disableCellBroadcastRange(int startMessageId, int endMessageId,
@android.telephony.SmsCbMessage.MessageFormat int ranType) {
boolean success = false;
@@ -2223,7 +2267,11 @@
* @return the user-defined default SMS subscription id, or the active subscription id if
* there's only one active subscription available, otherwise
* {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID}.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public static int getDefaultSmsSubscriptionId() {
try {
return getISmsService().getPreferredSmsSubscription();
@@ -2271,10 +2319,14 @@
* </p>
*
* @return the total number of SMS records which can be stored on the SIM card.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
@RequiresPermission(anyOf = {android.Manifest.permission.READ_PHONE_STATE,
android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE})
@IntRange(from = 0)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public int getSmsCapacityOnIcc() {
int ret = 0;
try {
@@ -2819,7 +2871,10 @@
* <code>MMS_ERROR_DATA_DISABLED</code><br>
* <code>MMS_ERROR_MMS_DISABLED_BY_CARRIER</code><br>
* @throws IllegalArgumentException if contentUri is empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultimediaMessage(Context context, Uri contentUri, String locationUrl,
Bundle configOverrides, PendingIntent sentIntent) {
sendMultimediaMessage(context, contentUri, locationUrl, configOverrides, sentIntent,
@@ -2863,7 +2918,10 @@
* @param messageId an id that uniquely identifies the message requested to be sent.
* Used for logging and diagnostics purposes. The id may be 0.
* @throws IllegalArgumentException if contentUri is empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void sendMultimediaMessage(@NonNull Context context, @NonNull Uri contentUri,
@Nullable String locationUrl,
@SuppressWarnings("NullableCollection") @Nullable Bundle configOverrides,
@@ -2922,7 +2980,10 @@
* <code>MMS_ERROR_DATA_DISABLED</code><br>
* <code>MMS_ERROR_MMS_DISABLED_BY_CARRIER</code><br>
* @throws IllegalArgumentException if locationUrl or contentUri is empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void downloadMultimediaMessage(Context context, String locationUrl, Uri contentUri,
Bundle configOverrides, PendingIntent downloadedIntent) {
downloadMultimediaMessage(context, locationUrl, contentUri, configOverrides,
@@ -2968,7 +3029,10 @@
* @param messageId an id that uniquely identifies the message requested to be downloaded.
* Used for logging and diagnostics purposes. The id may be 0.
* @throws IllegalArgumentException if locationUrl or contentUri is empty
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void downloadMultimediaMessage(@NonNull Context context, @NonNull String locationUrl,
@NonNull Uri contentUri,
@SuppressWarnings("NullableCollection") @Nullable Bundle configOverrides,
@@ -3079,7 +3143,11 @@
*
* @return the bundle key/values pairs that contains MMS configuration values
* or an empty Bundle if they cannot be found.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
@NonNull public Bundle getCarrierConfigValues() {
try {
ISms iSms = getISmsService();
@@ -3115,7 +3183,11 @@
*
* @return Token to include in an SMS message. The token will be 11 characters long.
* @see android.provider.Telephony.Sms.Intents#getMessagesFromIntent
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public String createAppSpecificSmsToken(PendingIntent intent) {
try {
ISms iccSms = getISmsServiceOrThrow();
@@ -3233,7 +3305,11 @@
* message.
* @param intent this intent is sent when the matching SMS message is received.
* @return Token to include in an SMS message.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
@Nullable
public String createAppSpecificSmsTokenWithPackageInfo(
@Nullable String prefixes, @NonNull PendingIntent intent) {
@@ -3393,9 +3469,13 @@
* </p>
*
* @return the SMSC address string, null if failed.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
@SuppressAutoDoc // for carrier privileges and default SMS application.
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
@Nullable
public String getSmscAddress() {
String smsc = null;
@@ -3430,9 +3510,13 @@
*
* @param smsc the SMSC address string.
* @return true for success, false otherwise. Failure can be due modem returning an error.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
*/
@SuppressAutoDoc // for carrier privileges and default SMS application.
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public boolean setSmscAddress(@NonNull String smsc) {
try {
ISms iSms = getISmsService();
@@ -3455,10 +3539,14 @@
* {@link SmsManager#PREMIUM_SMS_CONSENT_ASK_USER},
* {@link SmsManager#PREMIUM_SMS_CONSENT_NEVER_ALLOW}, or
* {@link SmsManager#PREMIUM_SMS_CONSENT_ALWAYS_ALLOW}
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public @PremiumSmsConsent int getPremiumSmsConsent(@NonNull String packageName) {
int permission = 0;
try {
@@ -3479,10 +3567,14 @@
* @param permission one of {@link SmsManager#PREMIUM_SMS_CONSENT_ASK_USER},
* {@link SmsManager#PREMIUM_SMS_CONSENT_NEVER_ALLOW}, or
* {@link SmsManager#PREMIUM_SMS_CONSENT_ALWAYS_ALLOW}
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void setPremiumSmsConsent(
@NonNull String packageName, @PremiumSmsConsent int permission) {
try {
@@ -3498,11 +3590,15 @@
/**
* Reset all cell broadcast ranges. Previously enabled ranges will become invalid after this.
* @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} with empty list instead
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_MESSAGING}.
* @hide
*/
@Deprecated
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_CELL_BROADCASTS)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public void resetAllCellBroadcastRanges() {
try {
ISms iSms = getISmsService();
@@ -3530,6 +3626,8 @@
* available.
* @throws SecurityException if the caller does not have the required permission/privileges.
* @throws IllegalStateException in case of telephony service is not available.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@NonNull
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 6c8663a..5615602 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1757,6 +1757,9 @@
*
* @param subId The unique SubscriptionInfo key in database.
* @return SubscriptionInfo, maybe null if its not active.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
@@ -1790,6 +1793,8 @@
* @param iccId the IccId of SIM card
* @return SubscriptionInfo, maybe null if its not active
*
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
@@ -1826,6 +1831,9 @@
*
* @param slotIndex the slot which the subscription is inserted
* @return SubscriptionInfo, maybe null if its not active
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
@@ -1870,6 +1878,8 @@
* {@link SubscriptionInfo#getSubscriptionId()}.
*
* @throws SecurityException if callers do not hold the required permission.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@NonNull
@RequiresPermission(anyOf = {
@@ -1929,6 +1939,9 @@
* then by {@link SubscriptionInfo#getSubscriptionId}.
* </li>
* </ul>
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
// @RequiresPermission(TODO(b/308809058))
@@ -1972,6 +1985,8 @@
* This is similar to {@link #getActiveSubscriptionInfoList} except that it will return
* both active and hidden SubscriptionInfos.
*
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
public @NonNull List<SubscriptionInfo> getCompleteActiveSubscriptionInfoList() {
List<SubscriptionInfo> completeList = getActiveSubscriptionInfoList(
@@ -2056,6 +2071,9 @@
* <p>
* Permissions android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE is required
* for #getAvailableSubscriptionInfoList to be invoked.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -2097,6 +2115,9 @@
* if the list is non-empty the list is sorted by {@link SubscriptionInfo#getSimSlotIndex}
* then by {@link SubscriptionInfo#getSubscriptionId}.
* </ul>
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
*/
public List<SubscriptionInfo> getAccessibleSubscriptionInfoList() {
List<SubscriptionInfo> result = null;
@@ -2125,6 +2146,8 @@
*
* @see TelephonyManager#getCardIdForDefaultEuicc() for more information on the card ID.
*
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
* @hide
*/
@SystemApi
@@ -2155,6 +2178,8 @@
*
* @see TelephonyManager#getCardIdForDefaultEuicc() for more information on the card ID.
*
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
* @hide
*/
@SystemApi
@@ -2177,6 +2202,9 @@
* @return The current number of active subscriptions.
*
* @see #getActiveSubscriptionInfoList()
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
// @RequiresPermission(TODO(b/308809058))
@@ -2247,6 +2275,9 @@
* @param slotIndex the slot assigned to this subscription. It is ignored for subscriptionType
* of {@link #SUBSCRIPTION_TYPE_REMOTE_SIM}.
* @param subscriptionType the {@link #SUBSCRIPTION_TYPE}
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
@@ -2289,6 +2320,8 @@
* @throws NullPointerException if {@code uniqueId} is {@code null}.
* @throws SecurityException if callers do not hold the required permission.
*
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
@@ -2435,6 +2468,7 @@
* @deprecated Use {@link #getSubscriptionId(int)} instead.
* @hide
*/
+ @Deprecated
public static int[] getSubId(int slotIndex) {
if (!isValidSlotIndex(slotIndex)) {
return null;
@@ -2489,6 +2523,9 @@
* On a data only device or on error, will return INVALID_SUBSCRIPTION_ID.
*
* @return the default voice subscription Id.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
public static int getDefaultVoiceSubscriptionId() {
int subId = INVALID_SUBSCRIPTION_ID;
@@ -2516,6 +2553,9 @@
*
* @param subscriptionId A valid subscription ID to set as the system default, or
* {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID}
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -2535,6 +2575,9 @@
/**
* Same as {@link #setDefaultVoiceSubscriptionId(int)}, but preserved for backwards
* compatibility.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
public void setDefaultVoiceSubId(int subId) {
@@ -2578,6 +2621,8 @@
*
* @param subscriptionId the supplied subscription ID
*
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -2612,6 +2657,8 @@
*
* @param subscriptionId the supplied subscription ID
*
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -2634,6 +2681,9 @@
* Will return null on voice only devices, or on error.
*
* @return the SubscriptionInfo for the default data subscription.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@UnsupportedAppUsage
@@ -2720,6 +2770,9 @@
*
* @return the list of subId's that are active,
* is never null but the length may be 0.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -2738,6 +2791,9 @@
*
* @return the list of subId's that are active,
* is never null but the length may be 0.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -2987,6 +3043,9 @@
* @param context Context object
* @param subId Subscription Id of Subscription whose resources are required
* @return Resources associated with Subscription.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@NonNull
@@ -3069,6 +3128,9 @@
* @return {@code true} if the supplied subscription ID corresponds to an active subscription;
* {@code false} if it does not correspond to an active subscription; or throw a
* SecurityException if the caller hasn't got the right permission.
+ *i
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
public boolean isActiveSubscriptionId(int subscriptionId) {
@@ -3377,6 +3439,8 @@
*
* @throws IllegalStateException when subscription manager service is not available.
* @throws SecurityException when clients do not have MODIFY_PHONE_STATE permission.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -3454,6 +3518,9 @@
* {@link TelephonyManager#hasCarrierPrivileges}).
*
* @return the list of opportunistic subscription info. If none exists, an empty list.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
@@ -3489,8 +3556,12 @@
* PendingIntent)} and does not support Multiple Enabled Profile(MEP). Apps should use
* {@link EuiccManager#switchToSubscription(int, PendingIntent)} or
* {@link EuiccManager#switchToSubscription(int, int, PendingIntent)} instead.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
*/
@RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_EUICC)
@Deprecated
public void switchToSubscription(int subId, @NonNull PendingIntent callbackIntent) {
Preconditions.checkNotNull(callbackIntent, "callbackIntent cannot be null");
@@ -3518,6 +3589,9 @@
* @param opportunistic whether it’s opportunistic subscription.
* @param subId the unique SubscriptionInfo index in database
* @return {@code true} if the operation is succeed, {@code false} otherwise.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@@ -3554,6 +3628,8 @@
* outlined above.
* @throws IllegalArgumentException if any of the subscriptions in the list doesn't exist.
* @throws IllegalStateException if Telephony service is in bad state.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*
* @param subIdList list of subId that will be in the same group
* @return groupUUID a UUID assigned to the subscription group.
@@ -3598,6 +3674,8 @@
* outlined above.
* @throws IllegalArgumentException if the some subscriptions in the list doesn't exist.
* @throws IllegalStateException if Telephony service is in bad state.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*
* @param subIdList list of subId that need adding into the group
* @param groupUuid the groupUuid the subscriptions are being added to.
@@ -3647,6 +3725,8 @@
* @throws IllegalArgumentException if the some subscriptions in the list doesn't belong the
* specified group.
* @throws IllegalStateException if Telephony service is in bad state.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*
* @see #createSubscriptionGroup(List)
*/
@@ -3696,6 +3776,8 @@
* @throws IllegalStateException if Telephony service is in bad state.
* @throws SecurityException if the caller doesn't meet the requirements
* outlined above.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*
* @param groupUuid of which list of subInfo will be returned.
* @return list of subscriptionInfo that belong to the same group, including the given
@@ -3785,9 +3867,9 @@
Map<ParcelUuid, SubscriptionInfo> groupMap = new HashMap<>();
for (SubscriptionInfo info : availableList) {
- // Opportunistic subscriptions are considered invisible
+ // Grouped opportunistic subscriptions are considered invisible
// to users so they should never be returned.
- if (!isSubscriptionVisible(info)) continue;
+ if (info.getGroupUuid() != null && info.isOpportunistic()) continue;
ParcelUuid groupUuid = info.getGroupUuid();
if (groupUuid == null) {
@@ -3817,6 +3899,8 @@
*
* @return whether the operation is successful.
*
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -3844,6 +3928,9 @@
*
* @param subscriptionId which subscription to operate on.
* @param enabled whether uicc applications are enabled or disabled.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -3872,6 +3959,8 @@
*
* @return whether can disable subscriptions on physical SIMs.
*
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -3897,6 +3986,8 @@
*
* @param subscriptionId The subscription id.
*
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -3923,6 +4014,8 @@
* @param sharing The status sharing preference.
*
* @throws SecurityException if the caller doesn't have permissions required.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
public void setDeviceToDeviceStatusSharingPreference(int subscriptionId,
@@ -3941,6 +4034,8 @@
* @return The device to device status sharing preference
*
* @throws SecurityException if the caller doesn't have permissions required.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
public @DeviceToDeviceStatusSharingPreference int getDeviceToDeviceStatusSharingPreference(
int subscriptionId) {
@@ -3960,6 +4055,8 @@
* @param contacts The list of contacts that allow device to device status sharing.
*
* @throws SecurityException if the caller doesn't have permissions required.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
public void setDeviceToDeviceStatusSharingContacts(int subscriptionId,
@@ -3980,6 +4077,9 @@
* @param subscriptionId Subscription id.
*
* @return The list of contacts that allow device to device status sharing.
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
public @NonNull List<Uri> getDeviceToDeviceStatusSharingContacts(int subscriptionId) {
String result = getStringSubscriptionProperty(mContext, subscriptionId,
@@ -4012,6 +4112,8 @@
*
* @throws IllegalArgumentException if the provided slot index is invalid.
* @throws SecurityException if callers do not hold the required permission.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*
* @hide
*/
@@ -4152,6 +4254,8 @@
*
* @param data with the sim specific configs to be backed up.
*
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
* @hide
*/
@SystemApi
@@ -4206,6 +4310,8 @@
* @throws IllegalArgumentException if {@code source} is invalid.
* @throws IllegalStateException if the telephony process is not currently available.
* @throws SecurityException if the caller doesn't have permissions required.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*
* @see #PHONE_NUMBER_SOURCE_UICC
* @see #PHONE_NUMBER_SOURCE_CARRIER
@@ -4266,6 +4372,8 @@
*
* @throws IllegalStateException if the telephony process is not currently available.
* @throws SecurityException if the caller doesn't have permissions required.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*
* @see #getPhoneNumber(int, int)
*/
@@ -4309,6 +4417,8 @@
* @throws IllegalStateException if the telephony process is not currently available.
* @throws NullPointerException if {@code number} is {@code null}.
* @throws SecurityException if the caller doesn't have permissions required.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@RequiresPermission("carrier privileges")
public void setCarrierPhoneNumber(int subscriptionId, @NonNull String number) {
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
index 60b5ce7..80c1e5be 100644
--- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
@@ -810,6 +810,7 @@
private FpsRange convertCategory(int category) {
switch (category) {
+ case Surface.FRAME_RATE_CATEGORY_HIGH_HINT:
case Surface.FRAME_RATE_CATEGORY_HIGH:
return FRAME_RATE_CATEGORY_HIGH;
case Surface.FRAME_RATE_CATEGORY_NORMAL:
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
index 4b56c10..caaee63 100644
--- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
@@ -93,6 +93,12 @@
}
@Test
+ public void testSurfaceControlFrameRateCategoryHighHint() throws InterruptedException {
+ GraphicsActivity activity = mActivityRule.getActivity();
+ activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_HIGH_HINT);
+ }
+
+ @Test
public void testSurfaceControlFrameRateCategoryNormal() throws InterruptedException {
GraphicsActivity activity = mActivityRule.getActivity();
activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_NORMAL);
diff --git a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt
new file mode 100644
index 0000000..e2b0c36
--- /dev/null
+++ b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt
@@ -0,0 +1,215 @@
+/*
+ * 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 android.hardware.input
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.os.Handler
+import android.os.HandlerExecutor
+import android.os.test.TestLooper
+import android.platform.test.annotations.Presubmit
+import android.platform.test.flag.junit.SetFlagsRule
+import android.view.KeyEvent
+import androidx.test.core.app.ApplicationProvider
+import com.android.server.testutils.any
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnitRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+/**
+ * Tests for [InputManager.StickyModifierStateListener].
+ *
+ * Build/Install/Run:
+ * atest InputTests:StickyModifierStateListenerTest
+ */
+@Presubmit
+@RunWith(MockitoJUnitRunner::class)
+class StickyModifierStateListenerTest {
+
+ @get:Rule
+ val rule = SetFlagsRule()
+
+ private val testLooper = TestLooper()
+ private val executor = HandlerExecutor(Handler(testLooper.looper))
+ private var registeredListener: IStickyModifierStateListener? = null
+ private lateinit var context: Context
+ private lateinit var inputManager: InputManager
+ private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
+
+ @Mock
+ private lateinit var iInputManagerMock: IInputManager
+
+ @Before
+ fun setUp() {
+ // Enable Sticky keys feature
+ rule.enableFlags(com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG)
+ rule.enableFlags(com.android.input.flags.Flags.FLAG_ENABLE_INPUT_FILTER_RUST_IMPL)
+
+ context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+ inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManagerMock)
+ inputManager = InputManager(context)
+ `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
+ .thenReturn(inputManager)
+
+ // Handle sticky modifier state listener registration.
+ doAnswer {
+ val listener = it.getArgument(0) as IStickyModifierStateListener
+ if (registeredListener != null &&
+ registeredListener!!.asBinder() != listener.asBinder()) {
+ // There can only be one registered sticky modifier state listener per process.
+ fail("Trying to register a new listener when one already exists")
+ }
+ registeredListener = listener
+ null
+ }.`when`(iInputManagerMock).registerStickyModifierStateListener(any())
+
+ // Handle sticky modifier state listener being unregistered.
+ doAnswer {
+ val listener = it.getArgument(0) as IStickyModifierStateListener
+ if (registeredListener == null ||
+ registeredListener!!.asBinder() != listener.asBinder()) {
+ fail("Trying to unregister a listener that is not registered")
+ }
+ registeredListener = null
+ null
+ }.`when`(iInputManagerMock).unregisterStickyModifierStateListener(any())
+ }
+
+ @After
+ fun tearDown() {
+ if (this::inputManagerGlobalSession.isInitialized) {
+ inputManagerGlobalSession.close()
+ }
+ }
+
+ private fun notifyStickyModifierStateChanged(modifierState: Int, lockedModifierState: Int) {
+ registeredListener!!.onStickyModifierStateChanged(modifierState, lockedModifierState)
+ }
+
+ @Test
+ fun testListenerIsNotifiedOnModifierStateChanged() {
+ var callbackCount = 0
+
+ // Add a sticky modifier state listener
+ inputManager.registerStickyModifierStateListener(executor) {
+ callbackCount++
+ }
+
+ // Notifying sticky modifier state change will notify the listener.
+ notifyStickyModifierStateChanged(0, 0)
+ testLooper.dispatchNext()
+ assertEquals(1, callbackCount)
+ }
+
+ @Test
+ fun testListenerHasCorrectModifierStateNotified() {
+ // Add a sticky modifier state listener
+ inputManager.registerStickyModifierStateListener(executor) {
+ state: StickyModifierState ->
+ assertTrue(state.isAltModifierOn)
+ assertTrue(state.isAltModifierLocked)
+ assertTrue(state.isShiftModifierOn)
+ assertTrue(!state.isShiftModifierLocked)
+ assertTrue(!state.isCtrlModifierOn)
+ assertTrue(!state.isCtrlModifierLocked)
+ assertTrue(!state.isMetaModifierOn)
+ assertTrue(!state.isMetaModifierLocked)
+ assertTrue(!state.isAltGrModifierOn)
+ assertTrue(!state.isAltGrModifierLocked)
+ }
+
+ // Notifying sticky modifier state change will notify the listener.
+ notifyStickyModifierStateChanged(
+ KeyEvent.META_ALT_ON or KeyEvent.META_ALT_LEFT_ON or
+ KeyEvent.META_SHIFT_ON or KeyEvent.META_SHIFT_LEFT_ON,
+ KeyEvent.META_ALT_ON or KeyEvent.META_ALT_LEFT_ON
+ )
+ testLooper.dispatchNext()
+ }
+
+ @Test
+ fun testAddingListenersRegistersInternalCallbackListener() {
+ // Set up two callbacks.
+ val callback1 = InputManager.StickyModifierStateListener {}
+ val callback2 = InputManager.StickyModifierStateListener {}
+
+ assertNull(registeredListener)
+
+ // Adding the listener should register the callback with InputManagerService.
+ inputManager.registerStickyModifierStateListener(executor, callback1)
+ assertNotNull(registeredListener)
+
+ // Adding another listener should not register new internal listener.
+ val currListener = registeredListener
+ inputManager.registerStickyModifierStateListener(executor, callback2)
+ assertEquals(currListener, registeredListener)
+ }
+
+ @Test
+ fun testRemovingListenersUnregistersInternalCallbackListener() {
+ // Set up two callbacks.
+ val callback1 = InputManager.StickyModifierStateListener {}
+ val callback2 = InputManager.StickyModifierStateListener {}
+
+ inputManager.registerStickyModifierStateListener(executor, callback1)
+ inputManager.registerStickyModifierStateListener(executor, callback2)
+
+ // Only removing all listeners should remove the internal callback
+ inputManager.unregisterStickyModifierStateListener(callback1)
+ assertNotNull(registeredListener)
+ inputManager.unregisterStickyModifierStateListener(callback2)
+ assertNull(registeredListener)
+ }
+
+ @Test
+ fun testMultipleListeners() {
+ // Set up two callbacks.
+ var callbackCount1 = 0
+ var callbackCount2 = 0
+ val callback1 = InputManager.StickyModifierStateListener { _ -> callbackCount1++ }
+ val callback2 = InputManager.StickyModifierStateListener { _ -> callbackCount2++ }
+
+ // Add both sticky modifier state listeners
+ inputManager.registerStickyModifierStateListener(executor, callback1)
+ inputManager.registerStickyModifierStateListener(executor, callback2)
+
+ // Notifying sticky modifier state change trigger the both callbacks.
+ notifyStickyModifierStateChanged(0, 0)
+ testLooper.dispatchAll()
+ assertEquals(1, callbackCount1)
+ assertEquals(1, callbackCount2)
+
+ inputManager.unregisterStickyModifierStateListener(callback2)
+ // Notifying sticky modifier state change should still trigger callback1 but not callback2.
+ notifyStickyModifierStateChanged(0, 0)
+ testLooper.dispatchAll()
+ assertEquals(2, callbackCount1)
+ assertEquals(1, callbackCount2)
+ }
+}