Merge "Add comments to AppWidgetServiceImpl [Part 1]" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 5ef7759..7a1add3 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -242,6 +242,12 @@
aconfig_declarations: "com.android.text.flags-aconfig",
}
+rust_aconfig_library {
+ name: "libandroid_text_flags_rust",
+ crate_name: "android_text_flags",
+ aconfig_declarations: "com.android.text.flags-aconfig",
+}
+
// Location
aconfig_declarations {
name: "android.location.flags-aconfig",
diff --git a/core/java/android/app/servertransaction/ObjectPool.java b/core/java/android/app/servertransaction/ObjectPool.java
index 598bd8a..e86ca37 100644
--- a/core/java/android/app/servertransaction/ObjectPool.java
+++ b/core/java/android/app/servertransaction/ObjectPool.java
@@ -16,70 +16,39 @@
package android.app.servertransaction;
-import com.android.window.flags.Flags;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-
/**
* An object pool that can provide reused objects if available.
+ *
* @hide
+ * @deprecated This class is deprecated. Directly create new instances of objects instead of
+ * obtaining them from this pool.
+ * TODO(b/311089192): Clean up usages of the pool.
*/
+@Deprecated
class ObjectPool {
- private static final Object sPoolSync = new Object();
- private static final Map<Class, ArrayList<? extends ObjectPoolItem>> sPoolMap =
- new HashMap<>();
-
- private static final int MAX_POOL_SIZE = 50;
-
/**
* Obtain an instance of a specific class from the pool
- * @param itemClass The class of the object we're looking for.
+ *
+ * @param ignoredItemClass The class of the object we're looking for.
* @return An instance or null if there is none.
+ * @deprecated This method is deprecated. Directly create new instances of objects instead of
+ * obtaining them from this pool.
*/
- public static <T extends ObjectPoolItem> T obtain(Class<T> itemClass) {
- if (Flags.disableObjectPool()) {
- return null;
- }
- synchronized (sPoolSync) {
- @SuppressWarnings("unchecked")
- final ArrayList<T> itemPool = (ArrayList<T>) sPoolMap.get(itemClass);
- if (itemPool != null && !itemPool.isEmpty()) {
- return itemPool.remove(itemPool.size() - 1);
- }
- return null;
- }
+ @Deprecated
+ public static <T extends ObjectPoolItem> T obtain(Class<T> ignoredItemClass) {
+ return null;
}
/**
* Recycle the object to the pool. The object should be properly cleared before this.
- * @param item The object to recycle.
+ *
+ * @param ignoredItem The object to recycle.
* @see ObjectPoolItem#recycle()
+ * @deprecated This method is deprecated. The object pool is no longer used, so there's
+ * no need to recycle objects.
*/
- public static <T extends ObjectPoolItem> void recycle(T item) {
- if (Flags.disableObjectPool()) {
- return;
- }
- synchronized (sPoolSync) {
- @SuppressWarnings("unchecked")
- ArrayList<T> itemPool = (ArrayList<T>) sPoolMap.get(item.getClass());
- if (itemPool == null) {
- itemPool = new ArrayList<>();
- sPoolMap.put(item.getClass(), itemPool);
- }
- // Check if the item is already in the pool
- final int size = itemPool.size();
- for (int i = 0; i < size; i++) {
- if (itemPool.get(i) == item) {
- throw new IllegalStateException("Trying to recycle already recycled item");
- }
- }
-
- if (size < MAX_POOL_SIZE) {
- itemPool.add(item);
- }
- }
+ @Deprecated
+ public static <T extends ObjectPoolItem> void recycle(T ignoredItem) {
}
}
diff --git a/core/java/android/app/servertransaction/ObjectPoolItem.java b/core/java/android/app/servertransaction/ObjectPoolItem.java
index 17bd4f3..0141f6e 100644
--- a/core/java/android/app/servertransaction/ObjectPoolItem.java
+++ b/core/java/android/app/servertransaction/ObjectPoolItem.java
@@ -18,12 +18,20 @@
/**
* Base interface for all lifecycle items that can be put in object pool.
+ *
* @hide
+ * @deprecated This interface is deprecated. Objects should no longer be pooled.
+ * TODO(b/311089192): Clean up usages of this interface.
*/
+@Deprecated
public interface ObjectPoolItem {
/**
* Clear the contents of the item and putting it to a pool. The implementation should call
* {@link ObjectPool#recycle(ObjectPoolItem)} passing itself.
+ *
+ * @deprecated This method is deprecated. The object pool is no longer used, so there's
+ * no need to recycle objects.
*/
+ @Deprecated
void recycle();
}
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index 0be2d3e3..e95c6a4 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -277,7 +277,7 @@
if (service != null) {
return service;
} else {
- return Binder.allowBlocking(getIServiceManager().checkService(name));
+ return Binder.allowBlocking(getIServiceManager().checkService(name).getBinder());
}
} catch (RemoteException e) {
Log.e(TAG, "error in checkService", e);
@@ -425,7 +425,7 @@
private static IBinder rawGetService(String name) throws RemoteException {
final long start = sStatLogger.getTime();
- final IBinder binder = getIServiceManager().getService(name);
+ final IBinder binder = getIServiceManager().getService(name).getBinder();
final int time = (int) sStatLogger.logDurationStat(Stats.GET_SERVICE, start);
diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java
index 7b91dd5..6c9a5c7 100644
--- a/core/java/android/os/ServiceManagerNative.java
+++ b/core/java/android/os/ServiceManagerNative.java
@@ -58,12 +58,12 @@
}
@UnsupportedAppUsage
- public IBinder getService(String name) throws RemoteException {
+ public Service getService(String name) throws RemoteException {
// Same as checkService (old versions of servicemanager had both methods).
- return mServiceManager.checkService(name);
+ return checkService(name);
}
- public IBinder checkService(String name) throws RemoteException {
+ public Service checkService(String name) throws RemoteException {
return mServiceManager.checkService(name);
}
diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java
index 292e6bd..50b73a9 100644
--- a/core/java/android/os/StrictMode.java
+++ b/core/java/android/os/StrictMode.java
@@ -126,20 +126,18 @@
* method:
*
* <pre>
- * public void onCreate() {
- * StrictMode.setThreadPolicy(new {@link ThreadPolicy.Builder StrictMode.ThreadPolicy.Builder}()
- * .detectDiskReads()
- * .detectDiskWrites()
- * .detectNetwork() // or .detectAll() for all detectable problems
- * .penaltyLog()
- * .build());
- * StrictMode.setVmPolicy(new {@link VmPolicy.Builder StrictMode.VmPolicy.Builder}()
- * .detectLeakedSqlLiteObjects()
- * .detectLeakedClosableObjects()
- * .penaltyLog()
- * .penaltyDeath()
- * .build());
- * super.onCreate();
+ * override fun onCreate(savedInstanceState: Bundle?) {
+ * super.onCreate(savedInstanceState)
+ * StrictMode.setThreadPolicy(
+ * StrictMode.ThreadPolicy.Builder()
+ * .detectAll()
+ * .build()
+ * )
+ * StrictMode.setVmPolicy(
+ * StrictMode.VmPolicy.Builder()
+ * .detectAll()
+ * .build()
+ * )
* }
* </pre>
*
@@ -354,7 +352,7 @@
public static final int NETWORK_POLICY_LOG = 1;
/** {@hide} */
public static final int NETWORK_POLICY_REJECT = 2;
-
+
/**
* Detect explicit calls to {@link Runtime#gc()}.
*/
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 2562c8e..ff38920 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -11075,6 +11075,13 @@
public static final String MANDATORY_BIOMETRICS = "mandatory_biometrics";
/**
+ * Whether or not requirements for mandatory biometrics is satisfied.
+ * @hide
+ */
+ public static final String MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED =
+ "mandatory_biometrics_requirements_satisfied";
+
+ /**
* Whether or not active unlock triggers on wake.
* @hide
*/
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index e4fc1cd..fbeab84 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -787,7 +787,6 @@
*/
public void setInteractive(boolean interactive) {
mInteractive = interactive;
- updateAccessibilityMessage();
}
/**
@@ -1641,9 +1640,9 @@
if (mWindow == null) return;
if (mDreamAccessibility == null) {
final View rootView = mWindow.getDecorView();
- mDreamAccessibility = new DreamAccessibility(this, rootView);
+ mDreamAccessibility = new DreamAccessibility(this, rootView, this::wakeUp);
}
- mDreamAccessibility.updateAccessibilityConfiguration(isInteractive());
+ mDreamAccessibility.updateAccessibilityConfiguration();
}
private boolean getWindowFlagValue(int flag, boolean defaultValue) {
diff --git a/core/java/android/service/dreams/utils/DreamAccessibility.java b/core/java/android/service/dreams/utils/DreamAccessibility.java
index c38f41b..f504ff7 100644
--- a/core/java/android/service/dreams/utils/DreamAccessibility.java
+++ b/core/java/android/service/dreams/utils/DreamAccessibility.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.content.Context;
+import android.os.Bundle;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -32,22 +33,22 @@
private final Context mContext;
private final View mView;
private final View.AccessibilityDelegate mAccessibilityDelegate;
+ private final Runnable mDismissCallback;
- public DreamAccessibility(@NonNull Context context, @NonNull View view) {
+ public DreamAccessibility(@NonNull Context context, @NonNull View view,
+ @NonNull Runnable dismissCallback) {
mContext = context;
mView = view;
mAccessibilityDelegate = createNewAccessibilityDelegate(mContext);
+ mDismissCallback = dismissCallback;
}
/**
- * @param interactive
- * Removes and add accessibility configuration depending if the dream is interactive or not
+ * Adds default accessibility configuration if none exist on the dream
*/
- public void updateAccessibilityConfiguration(Boolean interactive) {
- if (!interactive) {
+ public void updateAccessibilityConfiguration() {
+ if (mView.getAccessibilityDelegate() == null) {
addAccessibilityConfiguration();
- } else {
- removeCustomAccessibilityAction();
}
}
@@ -58,31 +59,28 @@
mView.setAccessibilityDelegate(mAccessibilityDelegate);
}
- /**
- * Removes Configured the accessibility actions for the given root view.
- */
- private void removeCustomAccessibilityAction() {
- if (mView.getAccessibilityDelegate() == mAccessibilityDelegate) {
- mView.setAccessibilityDelegate(null);
- }
- }
-
private View.AccessibilityDelegate createNewAccessibilityDelegate(Context context) {
return new View.AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(host, info);
- for (AccessibilityNodeInfo.AccessibilityAction action : info.getActionList()) {
- if (action.getId() == AccessibilityNodeInfo.ACTION_CLICK) {
- info.removeAction(action);
- break;
- }
- }
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
- AccessibilityNodeInfo.ACTION_CLICK,
+ AccessibilityNodeInfo.ACTION_DISMISS,
context.getResources().getString(R.string.dream_accessibility_action_click)
));
}
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ switch(action){
+ case AccessibilityNodeInfo.ACTION_DISMISS:
+ if (mDismissCallback != null) {
+ mDismissCallback.run();
+ }
+ break;
+ }
+ return true;
+ }
};
}
}
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 6b1ea26..02c63db 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -231,3 +231,12 @@
}
}
+flag {
+ name: "portuguese_hyphenator"
+ namespace: "text"
+ description: "Portuguese taiored hyphenator"
+ bug: "344656282"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index b2c39b1..ceaca22 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -2798,9 +2798,10 @@
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
- final boolean cancelChild = resetCancelNextUpFlag(target.child)
- || intercepted;
- if (dispatchTransformedTouchEvent(ev, cancelChild,
+ final boolean cancelChild =
+ (target.child != null && resetCancelNextUpFlag(target.child))
+ || intercepted;
+ if (target.child != null && dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 9512347..0dadbe3 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -7438,8 +7438,7 @@
// If the user interacts with a visible element it is safe to assume they consent that
// something is going to start.
opts.setPendingIntentBackgroundActivityStartMode(
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
- opts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
return Pair.create(intent, opts);
}
}
diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
index 48fb2b3..f739622 100644
--- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -98,6 +98,13 @@
}
flag {
+ name: "scrolling_from_letterbox"
+ namespace: "large_screen_experiences_app_compat"
+ description: "Whether to enable app scrolling from gestures from letterbox area"
+ bug: "353697519"
+}
+
+flag {
name: "app_compat_refactoring"
namespace: "large_screen_experiences_app_compat"
description: "Whether the changes about app compat refactoring are enabled./n"
diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
index 918235b..e429cfc 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
@@ -21,12 +21,7 @@
import static android.app.servertransaction.TestUtils.referrerIntentList;
import static android.app.servertransaction.TestUtils.resultInfoList;
-import static com.android.window.flags.Flags.FLAG_DISABLE_OBJECT_POOL;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
import android.annotation.NonNull;
import android.app.ActivityOptions;
@@ -41,14 +36,11 @@
import android.os.IBinder;
import android.os.PersistableBundle;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.FlagsParameterization;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.window.ActivityWindowInfo;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import com.android.window.flags.Flags;
-
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -56,12 +48,8 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
-import java.util.List;
import java.util.function.Supplier;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
/**
* Tests for {@link ObjectPool}.
*
@@ -71,31 +59,19 @@
* <p>This test class is a part of Window Manager Service tests and specified in
* {@link com.android.server.wm.test.filters.FrameworksTestsFilter}.
*/
-@RunWith(ParameterizedAndroidJunit4.class)
+@RunWith(AndroidJUnit4.class)
@SmallTest
@Presubmit
public class ObjectPoolTests {
- @Parameters(name = "{0}")
- public static List<FlagsParameterization> getParams() {
- return FlagsParameterization.allCombinationsOf(FLAG_DISABLE_OBJECT_POOL);
- }
-
@Rule
public final MockitoRule mocks = MockitoJUnit.rule();
- @Rule
- public SetFlagsRule mSetFlagsRule;
-
@Mock
private IApplicationThread mApplicationThread;
@Mock
private IBinder mActivityToken;
- public ObjectPoolTests(FlagsParameterization flags) {
- mSetFlagsRule = new SetFlagsRule(flags);
- }
-
// 1. Check if two obtained objects from pool are not the same.
// 2. Check if the state of the object is cleared after recycling.
// 3. Check if the same object is obtained from pool after recycling.
@@ -219,30 +195,11 @@
item.recycle();
final ObjectPoolItem item2 = obtain.get();
- if (Flags.disableObjectPool()) {
- assertNotSame(item, item2); // Different instance.
- } else {
- assertSame(item, item2);
- }
+ assertNotSame(item, item2); // Different instance.
// Create new object when the pool is empty.
final ObjectPoolItem item3 = obtain.get();
assertNotSame(item, item3);
- if (Flags.disableObjectPool()) {
- // Skip recycle if flag enabled, compare unnecessary.
- return;
- }
- assertEquals(item, item3);
-
- // Reset fields after recycle.
- item.recycle();
-
- assertNotEquals(item, item3);
-
- // Recycled objects are equal.
- item3.recycle();
-
- assertEquals(item, item3);
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
index 8906e6d..88264f3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
@@ -34,7 +34,7 @@
import java.util.function.Consumer;
/**
- * Implementation of {@link androidx.window.util.DataProducer} that produces a
+ * Implementation of {@link androidx.window.util.BaseDataProducer} that produces a
* {@link String} that can be parsed to a {@link CommonFoldingFeature}.
* {@link RawFoldingFeatureProducer} searches for the value in two places. The first check is in
* settings where the {@link String} property is saved with the key
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index 859bc2c..84984a9 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -31,10 +31,12 @@
import android.app.WindowConfiguration;
import android.content.ComponentCallbacks;
import android.content.Context;
+import android.content.ContextWrapper;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.StrictMode;
import android.util.ArrayMap;
import android.util.Log;
@@ -136,14 +138,23 @@
|| containsConsumer(consumer)) {
return;
}
+ final IllegalArgumentException exception = new IllegalArgumentException(
+ "Context must be a UI Context with display association, which should be"
+ + " an Activity, WindowContext or InputMethodService");
if (!context.isUiContext()) {
- throw new IllegalArgumentException("Context must be a UI Context, which should be"
- + " an Activity, WindowContext or InputMethodService");
+ throw exception;
}
if (context.getAssociatedDisplayId() == INVALID_DISPLAY) {
- Log.w(TAG, "The registered Context is a UI Context but not associated with any"
- + " display. This Context may not receive any WindowLayoutInfo update");
+ // This is to identify if #isUiContext of a non-UI Context is overridden.
+ // #isUiContext is more likely to be overridden than #getAssociatedDisplayId
+ // since #isUiContext is a public API.
+ StrictMode.onIncorrectContextUsed("The registered Context is a UI Context "
+ + "but not associated with any display. "
+ + "This Context may not receive any WindowLayoutInfo update. "
+ + dumpAllBaseContextToString(context), exception);
}
+ Log.d(TAG, "Register WindowLayoutInfoListener on "
+ + dumpAllBaseContextToString(context));
mFoldingFeatureProducer.getData((features) -> {
WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features);
consumer.accept(newWindowLayout);
@@ -162,6 +173,16 @@
}
}
+ @NonNull
+ private String dumpAllBaseContextToString(@NonNull Context context) {
+ final StringBuilder builder = new StringBuilder("Context=" + context);
+ while ((context instanceof ContextWrapper wrapper) && wrapper.getBaseContext() != null) {
+ context = wrapper.getBaseContext();
+ builder.append(", of which baseContext=").append(context);
+ }
+ return builder.toString();
+ }
+
@Override
public void removeWindowLayoutInfoListener(
@NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) {
@@ -417,9 +438,19 @@
*/
private boolean shouldReportDisplayFeatures(@NonNull @UiContext Context context) {
int displayId = context.getAssociatedDisplayId();
+ if (!context.isUiContext() || displayId == INVALID_DISPLAY) {
+ // This could happen if a caller sets MutableContextWrapper's base Context to a non-UI
+ // Context.
+ StrictMode.onIncorrectContextUsed("Context is not a UI Context anymore."
+ + " Was the base context changed? It's suggested to unregister"
+ + " the windowLayoutInfo callback before changing the base Context."
+ + " UI Contexts are Activity, InputMethodService or context created"
+ + " with createWindowContext. " + dumpAllBaseContextToString(context),
+ new UnsupportedOperationException("Context is not a UI Context anymore."
+ + " Was the base context changed?"));
+ }
if (displayId != DEFAULT_DISPLAY) {
- // Display features are not supported on secondary displays or the context is not
- // associated with any display.
+ // Display features are not supported on secondary displays.
return false;
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
index fe60037..63828ab 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java
@@ -23,7 +23,7 @@
/**
* A base class that works with {@link BaseDataProducer} to add/remove a consumer that should
* only be used once when {@link BaseDataProducer#notifyDataChanged} is called.
- * @param <T> The type of data this producer returns through {@link DataProducer#getData}.
+ * @param <T> The type of data this producer returns through {@link BaseDataProducer#getData}.
*/
public class AcceptOnceConsumer<T> implements Consumer<T> {
private final Consumer<T> mCallback;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
index de52f09..cd26efd 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
@@ -26,13 +26,12 @@
import java.util.function.Consumer;
/**
- * Base class that provides the implementation for the callback mechanism of the
- * {@link DataProducer} API. This class is thread safe for adding, removing, and notifying
- * consumers.
+ * Base class that manages listeners when listening to a piece of data that changes. This class is
+ * thread safe for adding, removing, and notifying consumers.
*
- * @param <T> The type of data this producer returns through {@link DataProducer#getData}.
+ * @param <T> The type of data this producer returns through {@link BaseDataProducer#getData}.
*/
-public abstract class BaseDataProducer<T> implements DataProducer<T>,
+public abstract class BaseDataProducer<T> implements
AcceptOnceConsumer.AcceptOnceProducerCallback<T> {
private final Object mLock = new Object();
@@ -42,12 +41,17 @@
private final Set<Consumer<T>> mCallbacksToRemove = new HashSet<>();
/**
+ * Emits the first available data at that point in time.
+ * @param dataConsumer a {@link Consumer} that will receive one value.
+ */
+ public abstract void getData(@NonNull Consumer<T> dataConsumer);
+
+ /**
* Adds a callback to the set of callbacks listening for data. Data is delivered through
* {@link BaseDataProducer#notifyDataChanged(Object)}. This method is thread safe. Callers
* should ensure that callbacks are thread safe.
* @param callback that will receive data from the producer.
*/
- @Override
public final void addDataChangedCallback(@NonNull Consumer<T> callback) {
synchronized (mLock) {
mCallbacks.add(callback);
@@ -63,7 +67,6 @@
* @param callback that was registered in
* {@link BaseDataProducer#addDataChangedCallback(Consumer)}.
*/
- @Override
public final void removeDataChangedCallback(@NonNull Consumer<T> callback) {
synchronized (mLock) {
mCallbacks.remove(callback);
@@ -92,8 +95,8 @@
/**
* Called to notify all registered consumers that the data provided
- * by {@link DataProducer#getData} has changed. Calls to this are thread save but callbacks need
- * to ensure thread safety.
+ * by {@link BaseDataProducer#getData} has changed. Calls to this are thread save but callbacks
+ * need to ensure thread safety.
*/
protected void notifyDataChanged(T value) {
synchronized (mLock) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java
deleted file mode 100644
index ec301dc..0000000
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window.util;
-
-import android.annotation.NonNull;
-
-import java.util.function.Consumer;
-
-/**
- * Produces data through {@link DataProducer#getData} and provides a mechanism for receiving
- * a callback when the data managed by the produces has changed.
- *
- * @param <T> The type of data this producer returns through {@link DataProducer#getData}.
- */
-public interface DataProducer<T> {
- /**
- * Emits the first available data at that point in time.
- * @param dataConsumer a {@link Consumer} that will receive one value.
- */
- void getData(@NonNull Consumer<T> dataConsumer);
-
- /**
- * Adds a callback to be notified when the data returned
- * from {@link DataProducer#getData} has changed.
- */
- void addDataChangedCallback(@NonNull Consumer<T> callback);
-
- /** Removes a callback previously added with {@link #addDataChangedCallback(Consumer)}. */
- void removeDataChangedCallback(@NonNull Consumer<T> callback);
-}
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index c2ba064..39f6d8c 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -179,4 +179,8 @@
<!-- Whether pointer pilfer is required to start back animation. -->
<bool name="config_backAnimationRequiresPointerPilfer">true</bool>
+
+ <!-- This is to be overridden to define a list of packages mapped to web links which will be
+ parsed and utilized for desktop windowing's app-to-web feature. -->
+ <string name="generic_links_list" translatable="false"/>
</resources>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
index f0d80a0..d3fc49b 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
@@ -39,8 +39,6 @@
/**
* Determines state of flag based on the actual flag and desktop mode developer option overrides.
- *
- * Note, this method makes sure that a constant developer toggle overrides is read until reboot.
*/
fun isEnabled(context: Context): Boolean =
if (!Flags.showDesktopWindowingDevOption() ||
@@ -65,7 +63,7 @@
?: run {
val override = getToggleOverrideFromSystem(context)
// Cache toggle override the first time we encounter context. Override does not change
- // with context, as context is just used to fetch System Property and Settings.Global
+ // with context, as context is just used to fetch Settings.Global
cachedToggleOverride = override
Log.d(TAG, "Toggle override initialized to: $override")
override
@@ -74,29 +72,13 @@
return override
}
- private fun getToggleOverrideFromSystem(context: Context): ToggleOverride {
- // A non-persistent System Property is used to store override to ensure it remains
- // constant till reboot.
- val overrideFromSystemProperties: ToggleOverride? =
- System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, null).convertToToggleOverride()
- return overrideFromSystemProperties
- ?: run {
- // Read Setting Global if System Property is not present (just after reboot)
- // or not valid (user manually changed the value)
- val overrideFromSettingsGlobal =
- convertToToggleOverrideWithFallback(
- Settings.Global.getInt(
- context.contentResolver,
- Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES,
- ToggleOverride.OVERRIDE_UNSET.setting),
- ToggleOverride.OVERRIDE_UNSET)
- // Initialize System Property
- System.setProperty(
- SYSTEM_PROPERTY_OVERRIDE_KEY, overrideFromSettingsGlobal.setting.toString())
-
- overrideFromSettingsGlobal
- }
- }
+ private fun getToggleOverrideFromSystem(context: Context): ToggleOverride =
+ convertToToggleOverrideWithFallback(
+ Settings.Global.getInt(
+ context.contentResolver,
+ Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES,
+ ToggleOverride.OVERRIDE_UNSET.setting),
+ ToggleOverride.OVERRIDE_UNSET)
/**
* Override state of desktop mode developer option toggle.
@@ -113,27 +95,12 @@
OVERRIDE_ON(1)
}
- private fun String?.convertToToggleOverride(): ToggleOverride? {
- val intValue = this?.toIntOrNull() ?: return null
- return settingToToggleOverrideMap[intValue]
- ?: run {
- Log.w(TAG, "Unknown toggleOverride int $intValue")
- null
- }
- }
-
companion object {
private const val TAG = "DesktopModeFlags"
/**
- * Key for non-persistent System Property which is used to store desktop windowing developer
- * option overrides.
- */
- private const val SYSTEM_PROPERTY_OVERRIDE_KEY = "sys.wmshell.desktopmode.dev_toggle_override"
-
- /**
* Local cache for toggle override, which is initialized once on its first access. It needs to
- * be refreshed only on reboots as overridden state takes effect on reboots.
+ * be refreshed only on reboots as overridden state is expected to take effect on reboots.
*/
private var cachedToggleOverride: ToggleOverride? = null
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index fc4710f..a1ba24c 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -70,6 +70,10 @@
private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean(
"persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
+ private static final boolean USE_APP_TO_WEB_BUILD_TIME_GENERIC_LINKS =
+ SystemProperties.getBoolean(
+ "persist.wm.debug.use_app_to_web_build_time_generic_links", true);
+
/** Whether the desktop density override is enabled. */
public static final boolean DESKTOP_DENSITY_OVERRIDE_ENABLED =
SystemProperties.getBoolean("persist.wm.debug.desktop_mode_density_enabled", false);
@@ -176,6 +180,13 @@
}
/**
+ * Returns {@code true} if the app-to-web feature is using the build-time generic links list.
+ */
+ public static boolean useAppToWebBuildTimeGenericLinks() {
+ return USE_APP_TO_WEB_BUILD_TIME_GENERIC_LINKS;
+ }
+
+ /**
* Return {@code true} if the override desktop density is enabled.
*/
private static boolean isDesktopDensityOverrideEnabled() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParser.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParser.kt
new file mode 100644
index 0000000..56447de
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParser.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.apptoweb
+
+import android.content.Context
+import android.provider.DeviceConfig
+import android.webkit.URLUtil
+import com.android.internal.annotations.VisibleForTesting
+import com.android.wm.shell.R
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useAppToWebBuildTimeGenericLinks
+
+/**
+ * Retrieves the build-time or server-side generic links list and parses and stores the
+ * package-to-url pairs.
+ */
+class AppToWebGenericLinksParser(
+ private val context: Context,
+ @ShellMainThread private val mainExecutor: ShellExecutor
+) {
+ private val genericLinksMap: MutableMap<String, String> = mutableMapOf()
+
+ init {
+ // If using the server-side generic links list, register a listener
+ if (!useAppToWebBuildTimeGenericLinks()) {
+ DeviceConfigListener()
+ }
+
+ updateGenericLinksMap()
+ }
+
+ /** Returns the generic link associated with the [packageName] or null if there is none. */
+ fun getGenericLink(packageName: String): String? = genericLinksMap[packageName]
+
+ private fun updateGenericLinksMap() {
+ val genericLinksList =
+ if (useAppToWebBuildTimeGenericLinks()) {
+ context.resources.getString(R.string.generic_links_list)
+ } else {
+ DeviceConfig.getString(NAMESPACE, FLAG_GENERIC_LINKS, /* defaultValue= */ "")
+ } ?: return
+
+ parseGenericLinkList(genericLinksList)
+ }
+
+ private fun parseGenericLinkList(genericLinksList: String) {
+ val newEntries =
+ genericLinksList
+ .split(" ")
+ .filter { it.contains(':') }
+ .map {
+ val (packageName, url) = it.split(':', limit = 2)
+ return@map packageName to url
+ }
+ .filter { URLUtil.isNetworkUrl(it.second) }
+
+ genericLinksMap.clear()
+ genericLinksMap.putAll(newEntries)
+ }
+
+ /**
+ * Listens for changes to the server-side generic links list and updates the package to url map
+ * if [DesktopModeStatus#useBuildTimeGenericLinkList()] is set to false.
+ */
+ inner class DeviceConfigListener : DeviceConfig.OnPropertiesChangedListener {
+ init {
+ DeviceConfig.addOnPropertiesChangedListener(NAMESPACE, mainExecutor, this)
+ }
+
+ override fun onPropertiesChanged(properties: DeviceConfig.Properties) {
+ if (properties.keyset.contains(FLAG_GENERIC_LINKS)) {
+ updateGenericLinksMap()
+ }
+ }
+ }
+
+ companion object {
+ private const val NAMESPACE = DeviceConfig.NAMESPACE_APP_COMPAT_OVERRIDES
+ @VisibleForTesting const val FLAG_GENERIC_LINKS = "generic_links_flag"
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
index 64a1b0c..140d776 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
@@ -30,6 +30,7 @@
import android.os.RemoteException;
import android.util.ArraySet;
import android.util.Size;
+import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
@@ -42,9 +43,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
@@ -69,26 +68,36 @@
@Retention(RetentionPolicy.SOURCE)
public @interface StashType {}
+ public static final int NAMED_KCA_LAUNCHER_SHELF = 0;
+ public static final int NAMED_KCA_TABLETOP_MODE = 1;
+
+ @IntDef(prefix = { "NAMED_KCA_" }, value = {
+ NAMED_KCA_LAUNCHER_SHELF,
+ NAMED_KCA_TABLETOP_MODE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface NamedKca {}
+
private static final String TAG = PipBoundsState.class.getSimpleName();
- private final @NonNull Rect mBounds = new Rect();
- private final @NonNull Rect mMovementBounds = new Rect();
- private final @NonNull Rect mNormalBounds = new Rect();
- private final @NonNull Rect mExpandedBounds = new Rect();
- private final @NonNull Rect mNormalMovementBounds = new Rect();
- private final @NonNull Rect mExpandedMovementBounds = new Rect();
- private final @NonNull PipDisplayLayoutState mPipDisplayLayoutState;
+ @NonNull private final Rect mBounds = new Rect();
+ @NonNull private final Rect mMovementBounds = new Rect();
+ @NonNull private final Rect mNormalBounds = new Rect();
+ @NonNull private final Rect mExpandedBounds = new Rect();
+ @NonNull private final Rect mNormalMovementBounds = new Rect();
+ @NonNull private final Rect mExpandedMovementBounds = new Rect();
+ @NonNull private final PipDisplayLayoutState mPipDisplayLayoutState;
private final Point mMaxSize = new Point();
private final Point mMinSize = new Point();
- private final @NonNull Context mContext;
+ @NonNull private final Context mContext;
private float mAspectRatio;
private int mStashedState = STASH_TYPE_NONE;
private int mStashOffset;
- private @Nullable PipReentryState mPipReentryState;
+ @Nullable private PipReentryState mPipReentryState;
private final LauncherState mLauncherState = new LauncherState();
- private final @NonNull SizeSpecSource mSizeSpecSource;
- private @Nullable ComponentName mLastPipComponentName;
- private final @NonNull MotionBoundsState mMotionBoundsState = new MotionBoundsState();
+ @NonNull private final SizeSpecSource mSizeSpecSource;
+ @Nullable private ComponentName mLastPipComponentName;
+ @NonNull private final MotionBoundsState mMotionBoundsState = new MotionBoundsState();
private boolean mIsImeShowing;
private int mImeHeight;
private boolean mIsShelfShowing;
@@ -120,12 +129,18 @@
* as unrestricted keep clear area. Values in this map would be appended to
* {@link #getUnrestrictedKeepClearAreas()} and this is meant for internal usage only.
*/
- private final Map<String, Rect> mNamedUnrestrictedKeepClearAreas = new HashMap<>();
+ private final SparseArray<Rect> mNamedUnrestrictedKeepClearAreas = new SparseArray<>();
- private @Nullable Runnable mOnMinimalSizeChangeCallback;
- private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback;
- private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>();
- private List<Consumer<Float>> mOnAspectRatioChangedCallbacks = new ArrayList<>();
+ @Nullable private Runnable mOnMinimalSizeChangeCallback;
+ @Nullable private TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback;
+ private final List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>();
+ private final List<Consumer<Float>> mOnAspectRatioChangedCallbacks = new ArrayList<>();
+
+ /**
+ * This is used to set the launcher shelf height ahead of non-auto-enter-pip animation,
+ * to avoid the race condition. See also {@link #NAMED_KCA_LAUNCHER_SHELF}.
+ */
+ public final Rect mCachedLauncherShelfHeightKeepClearArea = new Rect();
// the size of the current bounds relative to the max size spec
private float mBoundsScale;
@@ -430,17 +445,32 @@
mUnrestrictedKeepClearAreas.addAll(unrestrictedAreas);
}
- /** Add a named unrestricted keep clear area. */
- public void addNamedUnrestrictedKeepClearArea(@NonNull String name, Rect unrestrictedArea) {
- mNamedUnrestrictedKeepClearAreas.put(name, unrestrictedArea);
+ /** Set a named unrestricted keep clear area. */
+ public void setNamedUnrestrictedKeepClearArea(
+ @NamedKca int tag, @Nullable Rect unrestrictedArea) {
+ if (unrestrictedArea == null) {
+ mNamedUnrestrictedKeepClearAreas.remove(tag);
+ } else {
+ mNamedUnrestrictedKeepClearAreas.put(tag, unrestrictedArea);
+ if (tag == NAMED_KCA_LAUNCHER_SHELF) {
+ mCachedLauncherShelfHeightKeepClearArea.set(unrestrictedArea);
+ }
+ }
}
- /** Remove a named unrestricted keep clear area. */
- public void removeNamedUnrestrictedKeepClearArea(@NonNull String name) {
- mNamedUnrestrictedKeepClearAreas.remove(name);
+ /**
+ * Forcefully set the keep-clear-area for launcher shelf height if applicable.
+ * This is used for entering PiP in button navigation mode to make sure the destination bounds
+ * calculation includes the shelf height, to avoid race conditions that such callback is sent
+ * from Launcher after the entering animation is started.
+ */
+ public void mayUseCachedLauncherShelfHeight() {
+ if (!mCachedLauncherShelfHeightKeepClearArea.isEmpty()) {
+ setNamedUnrestrictedKeepClearArea(
+ NAMED_KCA_LAUNCHER_SHELF, mCachedLauncherShelfHeightKeepClearArea);
+ }
}
-
/**
* @return restricted keep clear areas.
*/
@@ -454,9 +484,12 @@
*/
@NonNull
public Set<Rect> getUnrestrictedKeepClearAreas() {
- if (mNamedUnrestrictedKeepClearAreas.isEmpty()) return mUnrestrictedKeepClearAreas;
+ if (mNamedUnrestrictedKeepClearAreas.size() == 0) return mUnrestrictedKeepClearAreas;
final Set<Rect> unrestrictedAreas = new ArraySet<>(mUnrestrictedKeepClearAreas);
- unrestrictedAreas.addAll(mNamedUnrestrictedKeepClearAreas.values());
+ for (int i = 0; i < mNamedUnrestrictedKeepClearAreas.size(); i++) {
+ final int key = mNamedUnrestrictedKeepClearAreas.keyAt(i);
+ unrestrictedAreas.add(mNamedUnrestrictedKeepClearAreas.get(key));
+ }
return unrestrictedAreas;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 80f6a63..32526ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -35,6 +35,7 @@
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.BubbleData;
import com.android.wm.shell.bubbles.BubbleDataRepository;
@@ -223,7 +224,8 @@
Transitions transitions,
Optional<DesktopTasksController> desktopTasksController,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
- InteractionJankMonitor interactionJankMonitor) {
+ InteractionJankMonitor interactionJankMonitor,
+ AppToWebGenericLinksParser genericLinksParser) {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
return new DesktopModeWindowDecorViewModel(
context,
@@ -242,7 +244,8 @@
transitions,
desktopTasksController,
rootTaskDisplayAreaOrganizer,
- interactionJankMonitor);
+ interactionJankMonitor,
+ genericLinksParser);
}
return new CaptionWindowDecorViewModel(
context,
@@ -259,6 +262,15 @@
transitions);
}
+ @WMSingleton
+ @Provides
+ static AppToWebGenericLinksParser provideGenericLinksParser(
+ Context context,
+ @ShellMainThread ShellExecutor mainExecutor
+ ) {
+ return new AppToWebGenericLinksParser(context, mainExecutor);
+ }
+
//
// Freeform
//
@@ -534,7 +546,8 @@
MultiInstanceHelper multiInstanceHelper,
@ShellMainThread ShellExecutor mainExecutor,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
- Optional<RecentTasksController> recentTasksController) {
+ Optional<RecentTasksController> recentTasksController,
+ InteractionJankMonitor interactionJankMonitor) {
return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController,
displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
dragAndDropController, transitions, keyguardManager, enterDesktopTransitionHandler,
@@ -542,7 +555,8 @@
dragToDesktopTransitionHandler, desktopModeTaskRepository,
desktopModeLoggerTransitionObserver, launchAdjacentController,
recentsTransitionHandler, multiInstanceHelper,
- mainExecutor, desktopTasksLimiter, recentTasksController.orElse(null));
+ mainExecutor, desktopTasksLimiter, recentTasksController.orElse(null),
+ interactionJankMonitor);
}
@WMSingleton
@@ -568,9 +582,10 @@
Context context,
Transitions transitions,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
- Optional<DesktopTasksLimiter> desktopTasksLimiter) {
+ Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ InteractionJankMonitor interactionJankMonitor) {
return new DragToDesktopTransitionHandler(context, transitions,
- rootTaskDisplayAreaOrganizer);
+ rootTaskDisplayAreaOrganizer, interactionJankMonitor);
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index de901b5..9fd2c27 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -49,6 +49,9 @@
import android.window.WindowContainerTransaction
import androidx.annotation.BinderThread
import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
+import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
+import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.policy.ScreenDecorationsUtils
import com.android.internal.protolog.ProtoLog
import com.android.window.flags.Flags
@@ -123,7 +126,8 @@
private val multiInstanceHelper: MultiInstanceHelper,
@ShellMainThread private val mainExecutor: ShellExecutor,
private val desktopTasksLimiter: Optional<DesktopTasksLimiter>,
- private val recentTasksController: RecentTasksController?
+ private val recentTasksController: RecentTasksController?,
+ private val interactionJankMonitor: InteractionJankMonitor
) :
RemoteCallable<DesktopTasksController>,
Transitions.TransitionHandler,
@@ -378,12 +382,15 @@
fun startDragToDesktop(
taskInfo: RunningTaskInfo,
dragToDesktopValueAnimator: MoveToDesktopAnimator,
+ taskSurface: SurfaceControl,
) {
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"DesktopTasksController: startDragToDesktop taskId=%d",
taskInfo.taskId
)
+ interactionJankMonitor.begin(taskSurface, context,
+ CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
dragToDesktopTransitionHandler.startDragToDesktopTransition(
taskInfo.taskId,
dragToDesktopValueAnimator
@@ -1340,13 +1347,19 @@
fun onDragPositioningEndThroughStatusBar(
inputCoordinates: PointF,
taskInfo: RunningTaskInfo,
+ taskSurface: SurfaceControl,
): IndicatorType {
+ // End the drag_hold CUJ interaction.
+ interactionJankMonitor.end(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
val indicator = getVisualIndicator() ?: return IndicatorType.NO_INDICATOR
val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
when (indicatorType) {
IndicatorType.TO_DESKTOP_INDICATOR -> {
val displayLayout = displayController.getDisplayLayout(taskInfo.displayId)
?: return IndicatorType.NO_INDICATOR
+ // Start a new jank interaction for the drag release to desktop window animation.
+ interactionJankMonitor.begin(taskSurface, context,
+ CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE, "to_desktop")
if (Flags.enableWindowingDynamicInitialBounds()) {
finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo))
} else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index ddee8fa..9e79eddb0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -30,6 +30,9 @@
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import com.android.internal.protolog.ProtoLog
+import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
+import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
+import com.android.internal.jank.InteractionJankMonitor
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
@@ -57,17 +60,20 @@
private val context: Context,
private val transitions: Transitions,
private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
- private val transactionSupplier: Supplier<SurfaceControl.Transaction>
+ private val interactionJankMonitor: InteractionJankMonitor,
+ private val transactionSupplier: Supplier<SurfaceControl.Transaction>,
) : TransitionHandler {
constructor(
context: Context,
transitions: Transitions,
- rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+ rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ interactionJankMonitor: InteractionJankMonitor
) : this(
context,
transitions,
rootTaskDisplayAreaOrganizer,
+ interactionJankMonitor,
Supplier { SurfaceControl.Transaction() }
)
@@ -567,6 +573,8 @@
onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId)
startTransitionFinishCb.onTransitionFinished(null /* null */)
clearState()
+ interactionJankMonitor.end(
+ CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)
}
}
)
@@ -604,6 +612,10 @@
"DragToDesktop: onTransitionConsumed() start transition aborted"
)
state.startAborted = true
+ // Cancel CUJ interaction if the transition is aborted.
+ interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
+ } else if (state.cancelTransitionToken != transition) {
+ interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 9bb9d86..a52141c5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -1020,6 +1020,9 @@
mPipMenuController.attach(leash);
}
+ // Make sure we have the launcher shelf into destination bounds calculation
+ // before the animator starts.
+ mPipBoundsState.mayUseCachedLauncherShelfHeight();
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
final Rect currentBounds = pipChange.getStartAbsBounds();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 0cb7e17..7451d22 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -646,9 +646,9 @@
});
mTabletopModeController.registerOnTabletopModeChangedListener((isInTabletopMode) -> {
- final String tag = "tabletop-mode";
if (!isInTabletopMode) {
- mPipBoundsState.removeNamedUnrestrictedKeepClearArea(tag);
+ mPipBoundsState.setNamedUnrestrictedKeepClearArea(
+ PipBoundsState.NAMED_KCA_TABLETOP_MODE, null);
return;
}
@@ -657,14 +657,16 @@
if (mTabletopModeController.getPreferredHalfInTabletopMode()
== TabletopModeController.PREFERRED_TABLETOP_HALF_TOP) {
// Prefer top, avoid the bottom half of the display.
- mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect(
- displayBounds.left, displayBounds.centerY(),
- displayBounds.right, displayBounds.bottom));
+ mPipBoundsState.setNamedUnrestrictedKeepClearArea(
+ PipBoundsState.NAMED_KCA_TABLETOP_MODE, new Rect(
+ displayBounds.left, displayBounds.centerY(),
+ displayBounds.right, displayBounds.bottom));
} else {
// Prefer bottom, avoid the top half of the display.
- mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect(
- displayBounds.left, displayBounds.top,
- displayBounds.right, displayBounds.centerY()));
+ mPipBoundsState.setNamedUnrestrictedKeepClearArea(
+ PipBoundsState.NAMED_KCA_TABLETOP_MODE, new Rect(
+ displayBounds.left, displayBounds.top,
+ displayBounds.right, displayBounds.centerY()));
}
// Try to move the PiP window if we have entered PiP mode.
@@ -916,10 +918,12 @@
0, mPipBoundsState.getDisplayBounds().bottom - height,
mPipBoundsState.getDisplayBounds().right,
mPipBoundsState.getDisplayBounds().bottom);
- mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, rect);
+ mPipBoundsState.setNamedUnrestrictedKeepClearArea(
+ PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, rect);
updatePipPositionForKeepClearAreas();
} else {
- mPipBoundsState.removeNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG);
+ mPipBoundsState.setNamedUnrestrictedKeepClearArea(
+ PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, null);
// postpone moving in response to hide of Launcher in case there's another change
mMainExecutor.removeCallbacks(mMovePipInResponseToKeepClearAreasChangeCallback);
mMainExecutor.executeDelayed(
@@ -968,8 +972,8 @@
int launcherRotation, Rect hotseatKeepClearArea) {
// preemptively add the keep clear area for Hotseat, so that it is taken into account
// when calculating the entry destination bounds of PiP window
- mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG,
- hotseatKeepClearArea);
+ mPipBoundsState.setNamedUnrestrictedKeepClearArea(
+ PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, hotseatKeepClearArea);
onDisplayRotationChangedNotInPip(mContext, launcherRotation);
// cache current min/max size
Point minSize = mPipBoundsState.getMinSize();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index b3dab85..48d17ec6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -68,6 +68,7 @@
DismissSession mPendingDismiss = null;
EnterSession mPendingEnter = null;
TransitSession mPendingResize = null;
+ TransitSession mPendingRemotePassthrough = null;
private IBinder mAnimatingTransition = null;
private OneShotRemoteHandler mActiveRemoteHandler = null;
@@ -320,6 +321,11 @@
return mPendingResize != null && mPendingResize.mTransition == transition;
}
+ boolean isPendingPassThrough(IBinder transition) {
+ return mPendingRemotePassthrough != null &&
+ mPendingRemotePassthrough.mTransition == transition;
+ }
+
@Nullable
private TransitSession getPendingTransition(IBinder transition) {
if (isPendingEnter(transition)) {
@@ -331,6 +337,9 @@
} else if (isPendingResize(transition)) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "\tresolved resize transition");
return mPendingResize;
+ } else if (isPendingPassThrough(transition)) {
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "\tresolved passThrough transition");
+ return mPendingRemotePassthrough;
}
return null;
}
@@ -378,6 +387,19 @@
extraTransitType, resizeAnim);
}
+ /** Sets a transition to enter split. */
+ void setRemotePassThroughTransition(@NonNull IBinder transition,
+ @Nullable RemoteTransition remoteTransition) {
+ mPendingRemotePassthrough = new TransitSession(
+ transition, null, null,
+ remoteTransition, Transitions.TRANSIT_SPLIT_PASSTHROUGH);
+
+ ProtoLog.v(WM_SHELL_TRANSITIONS, " splitTransition "
+ + " deduced remote passthrough split screen");
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setRemotePassThrough: transitType=%d remote=%s",
+ Transitions.TRANSIT_SPLIT_PASSTHROUGH, remoteTransition);
+ }
+
/** Starts a transition to dismiss split. */
IBinder startDismissTransition(WindowContainerTransaction wct,
Transitions.TransitionHandler handler, @SplitScreen.StageType int dismissTop,
@@ -474,6 +496,12 @@
mPendingResize.onConsumed(aborted);
mPendingResize = null;
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionConsumed for resize transition");
+ } else if (isPendingPassThrough(transition)) {
+ mPendingRemotePassthrough.onConsumed(aborted);
+ mPendingRemotePassthrough.mRemoteHandler.onTransitionConsumed(transition, aborted,
+ finishT);
+ mPendingRemotePassthrough = null;
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionConsumed for passThrough transition");
}
// TODO: handle transition consumed for active remote handler
@@ -495,6 +523,10 @@
mPendingResize.onFinished(wct, mFinishTransaction);
mPendingResize = null;
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFinish for resize transition");
+ } else if (isPendingPassThrough(mAnimatingTransition)) {
+ mPendingRemotePassthrough.onFinished(wct, mFinishTransaction);
+ mPendingRemotePassthrough = null;
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFinish for passThrough transition");
}
mActiveRemoteHandler = null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index a4f32c4..d7ee563 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2710,7 +2710,7 @@
@Nullable TransitionRequestInfo request) {
final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
if (triggerTask == null) {
- if (isSplitScreenVisible()) {
+ if (isSplitActive()) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d display rotation",
request.getDebugId());
// Check if the display is rotating.
@@ -2720,6 +2720,10 @@
&& displayChange.getStartRotation() != displayChange.getEndRotation()) {
mSplitLayout.setFreezeDividerWindow(true);
}
+ if (request.getRemoteTransition() != null) {
+ mSplitTransitions.setRemotePassThroughTransition(transition,
+ request.getRemoteTransition());
+ }
// Still want to monitor everything while in split-screen, so return non-null.
return new WindowContainerTransaction();
} else {
@@ -3046,6 +3050,13 @@
notifySplitAnimationFinished();
return true;
}
+ } else if (mSplitTransitions.isPendingPassThrough(transition)) {
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
+ "startAnimation: passThrough transition=%d", info.getDebugId());
+ mSplitTransitions.mPendingRemotePassthrough.mRemoteHandler.startAnimation(transition,
+ info, startTransaction, finishTransaction, finishCallback);
+ notifySplitAnimationFinished();
+ return true;
}
return startPendingAnimation(transition, info, startTransaction, finishTransaction,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index fc8b1d2..874cca5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -190,6 +190,9 @@
// TRANSIT_FIRST_CUSTOM + 17
TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_DRAG_RESIZE;
+ /** Remote Transition that split accepts but ultimately needs to be animated by the remote. */
+ public static final int TRANSIT_SPLIT_PASSTHROUGH = TRANSIT_FIRST_CUSTOM + 18;
+
/** Transition type for desktop mode transitions. */
public static final int TRANSIT_DESKTOP_MODE_TYPES =
WindowManager.TRANSIT_FIRST_CUSTOM + 100;
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 8312aef..a77a76c 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
@@ -87,6 +87,7 @@
import com.android.wm.shell.R;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
@@ -162,6 +163,7 @@
private final DesktopModeKeyguardChangeListener mDesktopModeKeyguardChangeListener =
new DesktopModeKeyguardChangeListener();
private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+ private final AppToWebGenericLinksParser mGenericLinksParser;
private final DisplayInsetsController mDisplayInsetsController;
private final Region mExclusionRegion = Region.obtain();
private boolean mInImmersiveMode;
@@ -198,7 +200,8 @@
Transitions transitions,
Optional<DesktopTasksController> desktopTasksController,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
- InteractionJankMonitor interactionJankMonitor
+ InteractionJankMonitor interactionJankMonitor,
+ AppToWebGenericLinksParser genericLinksParser
) {
this(
context,
@@ -216,6 +219,7 @@
syncQueue,
transitions,
desktopTasksController,
+ genericLinksParser,
new DesktopModeWindowDecoration.Factory(),
new InputMonitorFactory(),
SurfaceControl.Transaction::new,
@@ -241,6 +245,7 @@
SyncTransactionQueue syncQueue,
Transitions transitions,
Optional<DesktopTasksController> desktopTasksController,
+ AppToWebGenericLinksParser genericLinksParser,
DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
InputMonitorFactory inputMonitorFactory,
Supplier<SurfaceControl.Transaction> transactionFactory,
@@ -266,6 +271,7 @@
mInputMonitorFactory = inputMonitorFactory;
mTransactionFactory = transactionFactory;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
+ mGenericLinksParser = genericLinksParser;
mInputManager = mContext.getSystemService(InputManager.class);
mWindowDecorByTaskId = windowDecorByTaskId;
mSysUIPackageName = mContext.getResources().getString(
@@ -669,12 +675,6 @@
View v, MotionEvent e) {
final int id = v.getId();
if (id == R.id.caption_handle) {
- if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
- // Caption handle is located within the status bar region, meaning the
- // DisplayPolicy will attempt to transfer this input to status bar if it's
- // a swipe down. Pilfer here to keep the gesture in handle alone.
- mInputManager.pilferPointers(v.getViewRootImpl().getInputToken());
- }
handleCaptionThroughStatusBar(e, decoration);
final boolean wasDragging = mIsDragging;
updateDragStatus(e.getActionMasked());
@@ -970,7 +970,9 @@
relevantDecor.updateHoverAndPressStatus(ev);
DesktopModeVisualIndicator.IndicatorType resultType =
mDesktopTasksController.onDragPositioningEndThroughStatusBar(
- new PointF(ev.getRawX(), ev.getRawY()), relevantDecor.mTaskInfo);
+ new PointF(ev.getRawX(), ev.getRawY()),
+ relevantDecor.mTaskInfo,
+ relevantDecor.mTaskSurface);
// If we are entering split select, handle will no longer be visible and
// should not be receiving any input.
if (resultType == TO_SPLIT_LEFT_INDICATOR
@@ -1010,7 +1012,7 @@
mContext, mDragToDesktopAnimationStartBounds,
relevantDecor.mTaskInfo, relevantDecor.mTaskSurface);
mDesktopTasksController.startDragToDesktop(relevantDecor.mTaskInfo,
- mMoveToDesktopAnimator);
+ mMoveToDesktopAnimator, relevantDecor.mTaskSurface);
}
}
if (mMoveToDesktopAnimator != null) {
@@ -1155,7 +1157,8 @@
mBgExecutor,
mMainChoreographer,
mSyncQueue,
- mRootTaskDisplayAreaOrganizer);
+ mRootTaskDisplayAreaOrganizer,
+ mGenericLinksParser);
mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
final DragPositioningCallback dragPositioningCallback;
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 529def7..a1cc650 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
@@ -30,6 +30,7 @@
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.WindowConfiguration.WindowingMode;
@@ -49,8 +50,8 @@
import android.net.Uri;
import android.os.Handler;
import android.os.Trace;
-import android.util.Log;
import android.util.Size;
+import android.util.Slog;
import android.view.Choreographer;
import android.view.MotionEvent;
import android.view.SurfaceControl;
@@ -68,6 +69,7 @@
import com.android.wm.shell.R;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
@@ -133,12 +135,15 @@
private CharSequence mAppName;
private CapturedLink mCapturedLink;
+ private Uri mGenericLink;
private OpenInBrowserClickListener mOpenInBrowserClickListener;
private ExclusionRegionListener mExclusionRegionListener;
private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private final MaximizeMenuFactory mMaximizeMenuFactory;
+ private final HandleMenuFactory mHandleMenuFactory;
+ private final AppToWebGenericLinksParser mGenericLinksParser;
// Hover state for the maximize menu and button. The menu will remain open as long as either of
// these is true. See {@link #onMaximizeHoverStateChanged()}.
@@ -161,13 +166,14 @@
@ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
- RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ AppToWebGenericLinksParser genericLinksParser) {
this (context, displayController, splitScreenController, taskOrganizer, taskInfo,
taskSurface, handler, bgExecutor, choreographer, syncQueue,
- rootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new,
+ rootTaskDisplayAreaOrganizer, genericLinksParser, SurfaceControl.Builder::new,
SurfaceControl.Transaction::new, WindowContainerTransaction::new,
SurfaceControl::new, new SurfaceControlViewHostFactory() {},
- DefaultMaximizeMenuFactory.INSTANCE);
+ DefaultMaximizeMenuFactory.INSTANCE, DefaultHandleMenuFactory.INSTANCE);
}
DesktopModeWindowDecoration(
@@ -182,12 +188,14 @@
Choreographer choreographer,
SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ AppToWebGenericLinksParser genericLinksParser,
Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
Supplier<SurfaceControl> surfaceControlSupplier,
SurfaceControlViewHostFactory surfaceControlViewHostFactory,
- MaximizeMenuFactory maximizeMenuFactory) {
+ MaximizeMenuFactory maximizeMenuFactory,
+ HandleMenuFactory handleMenuFactory) {
super(context, displayController, taskOrganizer, taskInfo, taskSurface,
surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
windowContainerTransactionSupplier, surfaceControlSupplier,
@@ -198,7 +206,9 @@
mChoreographer = choreographer;
mSyncQueue = syncQueue;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
+ mGenericLinksParser = genericLinksParser;
mMaximizeMenuFactory = maximizeMenuFactory;
+ mHandleMenuFactory = handleMenuFactory;
}
/**
@@ -425,11 +435,23 @@
}
void onOpenInBrowserClick() {
- if (mOpenInBrowserClickListener == null || mCapturedLink == null) return;
- mOpenInBrowserClickListener.onClick(this, mCapturedLink.mUri);
+ if (mOpenInBrowserClickListener == null || mHandleMenu == null) {
+ return;
+ }
+ mOpenInBrowserClickListener.onClick(this, mHandleMenu.getOpenInBrowserLink());
onCapturedLinkExpired();
}
+ @Nullable
+ private Uri getBrowserLink() {
+ // If the captured link is available and has not expired, return the captured link.
+ // Otherwise, return the generic link which is set to null if a generic link is unavailable.
+ if (mCapturedLink != null && !mCapturedLink.mExpired) {
+ return mCapturedLink.mUri;
+ }
+ return mGenericLink;
+ }
+
private void updateDragResizeListener(SurfaceControl oldDecorationSurface) {
if (!isDragResizable(mTaskInfo)) {
if (!mTaskInfo.positionInParent.equals(mPositionInParent)) {
@@ -520,11 +542,7 @@
return new AppHandleViewHolder(
mResult.mRootView,
mOnCaptionTouchListener,
- mOnCaptionButtonClickListener,
- (v, event) -> {
- updateHoverAndPressStatus(event);
- return true;
- }
+ mOnCaptionButtonClickListener
);
} else if (mRelayoutParams.mLayoutResId
== R.layout.desktop_mode_app_header) {
@@ -589,9 +607,11 @@
controlsElement.mWidthResId = R.dimen.desktop_mode_customizable_caption_margin_end;
controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END;
relayoutParams.mOccludingCaptionElements.add(controlsElement);
- } else if (isAppHandle) {
+ } else if (isAppHandle && !Flags.enableAdditionalWindowsAboveStatusBar()) {
// The focused decor (fullscreen/split) does not need to handle input because input in
// the App Handle is handled by the InputMonitor in DesktopModeWindowDecorViewModel.
+ // Note: This does not apply with the above flag enabled as the status bar input layer
+ // will forward events to the handle directly.
relayoutParams.mInputFeatures
|= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
}
@@ -700,7 +720,7 @@
}
final ComponentName baseActivity = mTaskInfo.baseActivity;
if (baseActivity == null) {
- Log.e(TAG, "Base activity component not found in task");
+ Slog.e(TAG, "Base activity component not found in task");
return;
}
final PackageManager pm = mContext.getApplicationContext().getPackageManager();
@@ -719,7 +739,7 @@
final ApplicationInfo applicationInfo = activityInfo.applicationInfo;
mAppName = pm.getApplicationLabel(applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Base activity's component name cannot be found on the system");
+ Slog.e(TAG, "Base activity's component name cannot be found on the system", e);
} finally {
Trace.endSection();
}
@@ -914,7 +934,8 @@
*/
void createHandleMenu(SplitScreenController splitScreenController) {
loadAppInfoIfNeeded();
- mHandleMenu = new HandleMenu(
+ updateGenericLink();
+ mHandleMenu = mHandleMenuFactory.create(
this,
mRelayoutParams.mLayoutResId,
mOnCaptionButtonClickListener,
@@ -924,7 +945,7 @@
mDisplayController,
splitScreenController,
DesktopModeStatus.canEnterDesktopMode(mContext),
- browserLinkAvailable(),
+ getBrowserLink(),
mResult.mCaptionWidth,
mResult.mCaptionHeight,
mResult.mCaptionX
@@ -933,9 +954,15 @@
mHandleMenu.show();
}
- @VisibleForTesting
- boolean browserLinkAvailable() {
- return mCapturedLink != null && !mCapturedLink.mExpired;
+ private void updateGenericLink() {
+ final ComponentName baseActivity = mTaskInfo.baseActivity;
+ if (baseActivity == null) {
+ return;
+ }
+
+ final String genericLink =
+ mGenericLinksParser.getGenericLink(baseActivity.getPackageName());
+ mGenericLink = genericLink == null ? null : Uri.parse(genericLink);
}
/**
@@ -1219,7 +1246,8 @@
@ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
- RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ AppToWebGenericLinksParser genericLinksParser) {
return new DesktopModeWindowDecoration(
context,
displayController,
@@ -1231,7 +1259,8 @@
bgExecutor,
choreographer,
syncQueue,
- rootTaskDisplayAreaOrganizer);
+ rootTaskDisplayAreaOrganizer,
+ genericLinksParser);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
index da26898..3fd3656 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
@@ -19,7 +19,11 @@
import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_HOVER_ENTER;
+import static android.view.MotionEvent.ACTION_HOVER_EXIT;
+import static android.view.MotionEvent.ACTION_HOVER_MOVE;
import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP;
import android.graphics.PointF;
@@ -43,7 +47,7 @@
private final PointF mInputDownPoint = new PointF();
private int mTouchSlop;
private boolean mIsDragEvent;
- private int mDragPointerId;
+ private int mDragPointerId = -1;
private boolean mResultOfDownAction;
@@ -67,7 +71,7 @@
*
* @return the result returned by {@link #mEventHandler}, or the result when
* {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
- */
+ */
boolean onMotionEvent(View v, MotionEvent ev) {
final boolean isTouchScreen =
(ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
@@ -86,10 +90,14 @@
return mResultOfDownAction;
}
case ACTION_MOVE: {
- if (ev.findPointerIndex(mDragPointerId) == -1) {
- mDragPointerId = ev.getPointerId(0);
+ if (mDragPointerId == -1) {
+ // The primary pointer was lifted, ignore the rest of the gesture.
+ return mResultOfDownAction;
}
final int dragPointerIndex = ev.findPointerIndex(mDragPointerId);
+ if (dragPointerIndex == -1) {
+ throw new IllegalStateException("Failed to find primary pointer!");
+ }
if (!mIsDragEvent) {
float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x;
float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y;
@@ -99,22 +107,52 @@
}
// The event handler should only be notified about 'move' events if a drag has been
// detected.
- if (mIsDragEvent) {
- return mEventHandler.handleMotionEvent(v, ev);
- } else {
+ if (!mIsDragEvent) {
return mResultOfDownAction;
}
+ return mEventHandler.handleMotionEvent(v,
+ getSinglePointerEvent(ev, mDragPointerId));
+ }
+ case ACTION_HOVER_ENTER:
+ case ACTION_HOVER_MOVE:
+ case ACTION_HOVER_EXIT: {
+ return mEventHandler.handleMotionEvent(v,
+ getSinglePointerEvent(ev, mDragPointerId));
+ }
+ case ACTION_POINTER_UP: {
+ if (mDragPointerId == -1) {
+ // The primary pointer was lifted, ignore the rest of the gesture.
+ return mResultOfDownAction;
+ }
+ if (mDragPointerId != ev.getPointerId(ev.getActionIndex())) {
+ // Ignore a secondary pointer being lifted.
+ return mResultOfDownAction;
+ }
+ // The primary pointer is being lifted.
+ final int dragPointerId = mDragPointerId;
+ mDragPointerId = -1;
+ return mEventHandler.handleMotionEvent(v, getSinglePointerEvent(ev, dragPointerId));
}
case ACTION_UP:
case ACTION_CANCEL: {
+ final int dragPointerId = mDragPointerId;
resetState();
- return mEventHandler.handleMotionEvent(v, ev);
+ if (dragPointerId == -1) {
+ // The primary pointer was lifted, ignore the rest of the gesture.
+ return mResultOfDownAction;
+ }
+ return mEventHandler.handleMotionEvent(v, getSinglePointerEvent(ev, dragPointerId));
}
default:
- return mEventHandler.handleMotionEvent(v, ev);
+ // Ignore other events.
+ return mResultOfDownAction;
}
}
+ private static MotionEvent getSinglePointerEvent(MotionEvent ev, int pointerId) {
+ return ev.getPointerCount() > 1 ? ev.split(1 << pointerId) : ev;
+ }
+
void setTouchSlop(int touchSlop) {
mTouchSlop = touchSlop;
}
@@ -129,4 +167,4 @@
interface MotionEventHandler {
boolean handleMotionEvent(@Nullable View v, MotionEvent ev);
}
-}
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index e174e83..32522c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -27,6 +27,7 @@
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
+import android.net.Uri
import android.view.MotionEvent
import android.view.SurfaceControl
import android.view.View
@@ -68,7 +69,7 @@
private val displayController: DisplayController,
private val splitScreenController: SplitScreenController,
private val shouldShowWindowingPill: Boolean,
- private val shouldShowBrowserPill: Boolean,
+ val openInBrowserLink: Uri?,
private val captionWidth: Int,
private val captionHeight: Int,
captionX: Int
@@ -106,6 +107,9 @@
// those as well.
private val globalMenuPosition: Point = Point()
+ private val shouldShowBrowserPill: Boolean
+ get() = openInBrowserLink != null
+
init {
updateHandleMenuPillPositions(captionX)
}
@@ -497,3 +501,57 @@
private const val SHOULD_SHOW_MORE_ACTIONS_PILL = false
}
}
+
+/** A factory interface to create a [HandleMenu]. */
+interface HandleMenuFactory {
+ fun create(
+ parentDecor: DesktopModeWindowDecoration,
+ layoutResId: Int,
+ onClickListener: View.OnClickListener?,
+ onTouchListener: View.OnTouchListener?,
+ appIconBitmap: Bitmap?,
+ appName: CharSequence?,
+ displayController: DisplayController,
+ splitScreenController: SplitScreenController,
+ shouldShowWindowingPill: Boolean,
+ openInBrowserLink: Uri?,
+ captionWidth: Int,
+ captionHeight: Int,
+ captionX: Int
+ ): HandleMenu
+}
+
+/** A [HandleMenuFactory] implementation that creates a [HandleMenu]. */
+object DefaultHandleMenuFactory : HandleMenuFactory {
+ override fun create(
+ parentDecor: DesktopModeWindowDecoration,
+ layoutResId: Int,
+ onClickListener: View.OnClickListener?,
+ onTouchListener: View.OnTouchListener?,
+ appIconBitmap: Bitmap?,
+ appName: CharSequence?,
+ displayController: DisplayController,
+ splitScreenController: SplitScreenController,
+ shouldShowWindowingPill: Boolean,
+ openInBrowserLink: Uri?,
+ captionWidth: Int,
+ captionHeight: Int,
+ captionX: Int
+ ): HandleMenu {
+ return HandleMenu(
+ parentDecor,
+ layoutResId,
+ onClickListener,
+ onTouchListener,
+ appIconBitmap,
+ appName,
+ displayController,
+ splitScreenController,
+ shouldShowWindowingPill,
+ openInBrowserLink,
+ captionWidth,
+ captionHeight,
+ captionX
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index 76dfe37..57d8cac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -21,10 +21,11 @@
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Point
+import android.hardware.input.InputManager
+import android.view.MotionEvent.ACTION_DOWN
import android.view.SurfaceControl
import android.view.View
import android.view.View.OnClickListener
-import android.view.View.OnHoverListener
import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
import android.view.WindowManager
import android.widget.ImageButton
@@ -39,9 +40,8 @@
*/
internal class AppHandleViewHolder(
rootView: View,
- private val onCaptionTouchListener: View.OnTouchListener,
- private val onCaptionButtonClickListener: OnClickListener,
- private val onCaptionHoverListener: OnHoverListener,
+ onCaptionTouchListener: View.OnTouchListener,
+ onCaptionButtonClickListener: OnClickListener
) : WindowDecorationViewHolder(rootView) {
companion object {
@@ -51,6 +51,7 @@
private val windowManager = context.getSystemService(WindowManager::class.java)
private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle)
+ private val inputManager = context.getSystemService(InputManager::class.java)
// An invisible View that takes up the same coordinates as captionHandle but is layered
// above the status bar. The purpose of this View is to receive input intended for
@@ -61,7 +62,6 @@
captionView.setOnTouchListener(onCaptionTouchListener)
captionHandle.setOnTouchListener(onCaptionTouchListener)
captionHandle.setOnClickListener(onCaptionButtonClickListener)
- captionHandle.setOnHoverListener(onCaptionHoverListener)
}
override fun bindData(
@@ -106,10 +106,19 @@
// gesture listener that receives events before window. This is to prevent notification
// shade gesture when we swipe down to enter desktop.
lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
- view.id = R.id.caption_handle
- view.setOnClickListener(onCaptionButtonClickListener)
- view.setOnTouchListener(onCaptionTouchListener)
- view.setOnHoverListener(onCaptionHoverListener)
+ view.setOnHoverListener { _, event ->
+ captionHandle.onHoverEvent(event)
+ }
+ // Caption handle is located within the status bar region, meaning the
+ // DisplayPolicy will attempt to transfer this input to status bar if it's
+ // a swipe down. Pilfer here to keep the gesture in handle alone.
+ view.setOnTouchListener { v, event ->
+ if (event.actionMasked == ACTION_DOWN) {
+ inputManager.pilferPointers(v.viewRootImpl.inputToken)
+ }
+ captionHandle.dispatchTouchEvent(event)
+ true
+ }
windowManager.updateViewLayout(view, lp)
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParserTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParserTests.kt
new file mode 100644
index 0000000..053027f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParserTests.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.apptoweb
+
+import android.provider.DeviceConfig
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableResources
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser.Companion.FLAG_GENERIC_LINKS
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.quality.Strictness
+
+/**
+ * Tests for [AppToWebGenericLinksParser].
+ *
+ * Build/Install/Run: atest WMShellUnitTests:AppToWebGenericLinksParserTests
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class AppToWebGenericLinksParserTests : ShellTestCase() {
+ @Mock private lateinit var mockExecutor: ShellExecutor
+
+ private lateinit var genericLinksParser: AppToWebGenericLinksParser
+ private lateinit var mockitoSession: StaticMockitoSession
+ private lateinit var resources: TestableResources
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ mockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .spyStatic(DesktopModeStatus::class.java)
+ .startMocking()
+ resources = mContext.getOrCreateTestableResources()
+ resources.addOverride(R.string.generic_links_list, BUILD_TIME_LIST)
+ DeviceConfig.setProperty(
+ NAMESPACE,
+ FLAG_GENERIC_LINKS,
+ SERVER_SIDE_LIST,
+ false /* makeDefault */
+ )
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ fun init_usingBuildTimeList() {
+ doReturn(true).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() }
+ genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor)
+ // Assert build-time list correctly parsed
+ assertEquals(URL_B, genericLinksParser.getGenericLink(PACKAGE_NAME_1))
+ }
+
+ @Test
+ fun init_usingServerSideList() {
+ doReturn(false).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() }
+ genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor)
+ // Assert server side list correctly parsed
+ assertEquals(URL_S, genericLinksParser.getGenericLink(PACKAGE_NAME_1))
+ }
+
+ @Test
+ fun init_ignoresMalformedPair() {
+ doReturn(true).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() }
+ val packageName2 = "com.google.android.slides"
+ val url2 = "https://docs.google.com"
+ resources.addOverride(R.string.generic_links_list,
+ "$PACKAGE_NAME_1:$URL_B error $packageName2:$url2")
+ genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor)
+ // Assert generics links list correctly parsed
+ assertEquals(URL_B, genericLinksParser.getGenericLink(PACKAGE_NAME_1))
+ assertEquals(url2, genericLinksParser.getGenericLink(packageName2))
+ }
+
+
+ @Test
+ fun onlySavesValidPackageToUrlMaps() {
+ doReturn(true).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() }
+ resources.addOverride(R.string.generic_links_list, "$PACKAGE_NAME_1:www.yout")
+ genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor)
+ // Verify map with invalid url not saved
+ assertNull(genericLinksParser.getGenericLink(PACKAGE_NAME_1))
+ }
+
+ companion object {
+ private const val PACKAGE_NAME_1 = "com.google.android.youtube"
+
+ private const val URL_B = "http://www.youtube.com"
+ private const val URL_S = "http://www.google.com"
+
+ private const val SERVER_SIDE_LIST = "$PACKAGE_NAME_1:$URL_S"
+ private const val BUILD_TIME_LIST = "$PACKAGE_NAME_1:$URL_B"
+
+ private const val NAMESPACE = DeviceConfig.NAMESPACE_APP_COMPAT_OVERRIDES
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 8421365..37510ef4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -66,6 +66,7 @@
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.dx.mockito.inline.extended.ExtendedMockito.never
import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.internal.jank.InteractionJankMonitor
import com.android.window.flags.Flags
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
import com.android.wm.shell.MockToken
@@ -166,6 +167,9 @@
@Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver
@Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator
@Mock lateinit var recentTasksController: RecentTasksController
+ @Mock
+ private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
+ @Mock private lateinit var mockSurface: SurfaceControl
private lateinit var mockitoSession: StaticMockitoSession
private lateinit var controller: DesktopTasksController
@@ -248,7 +252,8 @@
multiInstanceHelper,
shellExecutor,
Optional.of(desktopTasksLimiter),
- recentTasksController)
+ recentTasksController,
+ mockInteractionJankMonitor)
}
@After
@@ -2016,7 +2021,7 @@
val task = setUpFullscreenTask()
setUpLandscapeDisplay()
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
val wct = getLatestDragToDesktopWct()
assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
}
@@ -2032,7 +2037,7 @@
val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
setUpLandscapeDisplay()
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
val wct = getLatestDragToDesktopWct()
assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
}
@@ -2049,7 +2054,7 @@
setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
setUpLandscapeDisplay()
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
val wct = getLatestDragToDesktopWct()
assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
}
@@ -2066,7 +2071,7 @@
setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
setUpLandscapeDisplay()
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
val wct = getLatestDragToDesktopWct()
assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
}
@@ -2086,7 +2091,7 @@
shouldLetterbox = true)
setUpLandscapeDisplay()
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
val wct = getLatestDragToDesktopWct()
assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
}
@@ -2102,7 +2107,7 @@
val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
setUpPortraitDisplay()
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
val wct = getLatestDragToDesktopWct()
assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
}
@@ -2121,7 +2126,7 @@
screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
setUpPortraitDisplay()
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
val wct = getLatestDragToDesktopWct()
assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
}
@@ -2141,7 +2146,7 @@
shouldLetterbox = true)
setUpPortraitDisplay()
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
val wct = getLatestDragToDesktopWct()
assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
}
@@ -2161,7 +2166,7 @@
screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
setUpPortraitDisplay()
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface)
val wct = getLatestDragToDesktopWct()
assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
}
@@ -2182,7 +2187,7 @@
shouldLetterbox = true)
setUpPortraitDisplay()
- spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task)
+ spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task, mockSurface)
val wct = getLatestDragToDesktopWct()
assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index bbf523b..e4e2bd2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -15,6 +15,7 @@
import android.window.TransitionInfo.FLAG_IS_WALLPAPER
import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
+import com.android.internal.jank.InteractionJankMonitor
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestRunningTaskInfoBuilder
@@ -51,6 +52,8 @@
@Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock private lateinit var splitScreenController: SplitScreenController
@Mock private lateinit var dragAnimator: MoveToDesktopAnimator
+ @Mock
+ private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() }
@@ -63,7 +66,8 @@
context,
transitions,
taskDisplayAreaOrganizer,
- transactionSupplier
+ mockInteractionJankMonitor,
+ transactionSupplier,
)
.apply { setSplitScreenController(splitScreenController) }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt
index b1d62f4..dd19d76 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt
@@ -184,108 +184,6 @@
}
@Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
- @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- fun isEnabled_noSystemProperty_overrideOn_featureFlagOff_returnsTrueAndStoresPropertyOn() {
- System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)
- setOverride(OVERRIDE_ON.setting)
-
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue()
- // Store System Property if not present
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(OVERRIDE_ON.setting.toString())
- }
-
- @Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- fun isEnabled_noSystemProperty_overrideUnset_featureFlagOn_returnsTrueAndStoresPropertyUnset() {
- System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)
- setOverride(OVERRIDE_UNSET.setting)
-
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue()
- // Store System Property if not present
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(OVERRIDE_UNSET.setting.toString())
- }
-
- @Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
- @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- fun isEnabled_noSystemProperty_overrideUnset_featureFlagOff_returnsFalseAndStoresPropertyUnset() {
- System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)
- setOverride(OVERRIDE_UNSET.setting)
-
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse()
- // Store System Property if not present
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(OVERRIDE_UNSET.setting.toString())
- }
-
- @Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- @Suppress("ktlint:standard:max-line-length")
- fun isEnabled_systemPropertyNotInteger_overrideOff_featureFlagOn_returnsFalseAndStoresPropertyOff() {
- System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "abc")
- setOverride(OVERRIDE_OFF.setting)
-
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse()
- // Store System Property if currently invalid
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(OVERRIDE_OFF.setting.toString())
- }
-
- @Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- @Suppress("ktlint:standard:max-line-length")
- fun isEnabled_systemPropertyInvalidInteger_overrideOff_featureFlagOn_returnsFalseAndStoresPropertyOff() {
- System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "-2")
- setOverride(OVERRIDE_OFF.setting)
-
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse()
- // Store System Property if currently invalid
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(OVERRIDE_OFF.setting.toString())
- }
-
- @Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- fun isEnabled_systemPropertyOff_overrideOn_featureFlagOn_returnsFalseAndDoesNotUpdateProperty() {
- System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, OVERRIDE_OFF.setting.toString())
- setOverride(OVERRIDE_ON.setting)
-
- // Have a consistent override until reboot
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse()
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(OVERRIDE_OFF.setting.toString())
- }
-
- @Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
- @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- fun isEnabled_systemPropertyOn_overrideOff_featureFlagOff_returnsTrueAndDoesNotUpdateProperty() {
- System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, OVERRIDE_ON.setting.toString())
- setOverride(OVERRIDE_OFF.setting)
-
- // Have a consistent override until reboot
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue()
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(OVERRIDE_ON.setting.toString())
- }
-
- @Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- @Suppress("ktlint:standard:max-line-length")
- fun isEnabled_systemPropertyUnset_overrideOff_featureFlagOn_returnsTrueAndDoesNotUpdateProperty() {
- System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, OVERRIDE_UNSET.setting.toString())
- setOverride(OVERRIDE_OFF.setting)
-
- // Have a consistent override until reboot
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue()
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(OVERRIDE_UNSET.setting.toString())
- }
-
- @Test
@EnableFlags(
FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
@@ -445,12 +343,5 @@
DesktopModeFlags::class.java.getDeclaredField("cachedToggleOverride")
cachedToggleOverride.isAccessible = true
cachedToggleOverride.set(null, null)
-
- // Clear override cache stored in System property
- System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)
- }
-
- private companion object {
- const val SYSTEM_PROPERTY_OVERRIDE_KEY = "sys.wmshell.desktopmode.dev_toggle_override"
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 37ef788..22b408c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -51,8 +51,10 @@
import android.app.ActivityManager;
import android.os.Handler;
import android.os.IBinder;
+import android.os.RemoteException;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
+import android.window.IRemoteTransition;
import android.window.RemoteTransition;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
@@ -328,6 +330,32 @@
@Test
@UiThreadTest
+ public void testRemotePassThroughInvoked() throws RemoteException {
+ RemoteTransition remoteWrapper = mock(RemoteTransition.class);
+ IRemoteTransition remoteTransition = mock(IRemoteTransition.class);
+ IBinder remoteBinder = mock(IBinder.class);
+ doReturn(remoteBinder).when(remoteTransition).asBinder();
+ doReturn(remoteTransition).when(remoteWrapper).getRemoteTransition();
+
+ TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_CHANGE, null,
+ remoteWrapper);
+ IBinder transition = mock(IBinder.class);
+ mMainStage.activate(new WindowContainerTransaction(), false);
+ mStageCoordinator.handleRequest(transition, request);
+ TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CHANGE, 0)
+ .build();
+ boolean accepted = mStageCoordinator.startAnimation(transition, info,
+ mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class),
+ mock(Transitions.TransitionFinishCallback.class));
+ assertTrue(accepted);
+
+ verify(remoteTransition, times(1)).startAnimation(any(),
+ any(), any(), any());
+ }
+
+ @Test
+ @UiThreadTest
public void testEnterRecentsAndRestore() {
enterSplit();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index aeae0be..01c4f3a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -64,6 +64,7 @@
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestRunningTaskInfoBuilder
import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayInsetsController
import com.android.wm.shell.common.DisplayLayout
@@ -140,6 +141,7 @@
@Mock private lateinit var mockShellCommandHandler: ShellCommandHandler
@Mock private lateinit var mockWindowManager: IWindowManager
@Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
+ @Mock private lateinit var mockGenericLinksParser: AppToWebGenericLinksParser
private val bgExecutor = TestShellExecutor()
private val transactionFactory = Supplier<SurfaceControl.Transaction> {
@@ -171,11 +173,13 @@
mockSyncQueue,
mockTransitions,
Optional.of(mockDesktopTasksController),
+ mockGenericLinksParser,
mockDesktopModeWindowDecorFactory,
mockInputMonitorFactory,
transactionFactory,
mockRootTaskDisplayAreaOrganizer,
- windowDecorByTaskIdSpy, mockInteractionJankMonitor
+ windowDecorByTaskIdSpy,
+ mockInteractionJankMonitor
)
desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -218,7 +222,8 @@
bgExecutor,
mockMainChoreographer,
mockSyncQueue,
- mockRootTaskDisplayAreaOrganizer
+ mockRootTaskDisplayAreaOrganizer,
+ mockGenericLinksParser
)
verify(decoration).close()
}
@@ -244,7 +249,8 @@
bgExecutor,
mockMainChoreographer,
mockSyncQueue,
- mockRootTaskDisplayAreaOrganizer
+ mockRootTaskDisplayAreaOrganizer,
+ mockGenericLinksParser
)
task.setWindowingMode(WINDOWING_MODE_FREEFORM)
@@ -261,7 +267,8 @@
bgExecutor,
mockMainChoreographer,
mockSyncQueue,
- mockRootTaskDisplayAreaOrganizer
+ mockRootTaskDisplayAreaOrganizer,
+ mockGenericLinksParser
)
}
@@ -359,7 +366,7 @@
verify(mockDesktopModeWindowDecorFactory, never())
.create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
- any())
+ any(), any())
}
@Test
@@ -381,7 +388,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
.create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
- any(), any())
+ any(), any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -399,7 +406,7 @@
verify(mockDesktopModeWindowDecorFactory, never())
.create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
- any())
+ any(), any())
}
@Test
@@ -416,7 +423,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
+ .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
any(), any())
}
@@ -515,7 +522,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory, never())
.create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
- any())
+ any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -540,7 +547,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
.create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
- any())
+ any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -564,7 +571,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
.create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
- any())
+ any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -702,8 +709,8 @@
private fun setUpMockDecorationForTask(task: RunningTaskInfo): DesktopModeWindowDecoration {
val decoration = mock(DesktopModeWindowDecoration::class.java)
whenever(
- mockDesktopModeWindowDecorFactory.create(
- any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
+ mockDesktopModeWindowDecorFactory.create(any(), any(), any(), any(), eq(task), any(),
+ any(), any(), any(), any(), any(), any())
).thenReturn(decoration)
decoration.mTaskInfo = task
whenever(decoration.isFocused).thenReturn(task.isFocused)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 412fef3..4b069f9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -31,6 +31,7 @@
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
@@ -59,6 +60,7 @@
import android.platform.test.flag.junit.SetFlagsRule;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
+import android.testing.TestableLooper;
import android.view.AttachedSurfaceControl;
import android.view.Choreographer;
import android.view.Display;
@@ -83,6 +85,7 @@
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -114,6 +117,7 @@
* atest WMShellUnitTests:DesktopModeWindowDecorationTests
*/
@SmallTest
+@TestableLooper.RunWithLooper
@RunWith(AndroidTestingRunner.class)
public class DesktopModeWindowDecorationTests extends ShellTestCase {
private static final String USE_WINDOW_SHADOWS_SYSPROP_KEY =
@@ -123,7 +127,8 @@
private static final String USE_ROUNDED_CORNERS_SYSPROP_KEY =
"persist.wm.debug.desktop_use_rounded_corners";
- private static final Uri TEST_URI = Uri.parse("www.google.com");
+ private static final Uri TEST_URI1 = Uri.parse("https://www.google.com/");
+ private static final Uri TEST_URI2 = Uri.parse("https://docs.google.com/");
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
@@ -161,6 +166,12 @@
private Handler mMockHandler;
@Mock
private DesktopModeWindowDecoration.OpenInBrowserClickListener mMockOpenInBrowserClickListener;
+ @Mock
+ private AppToWebGenericLinksParser mMockGenericLinksParser;
+ @Mock
+ private HandleMenu mMockHandleMenu;
+ @Mock
+ private HandleMenuFactory mMockHandleMenuFactory;
@Captor
private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener;
@Captor
@@ -204,6 +215,8 @@
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY);
doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
+ doReturn(mMockHandleMenu).when(mMockHandleMenuFactory).create(any(), anyInt(), any(), any(),
+ any(), any(), any(), any(), anyBoolean(), any(), anyInt(), anyInt(), anyInt());
}
@After
@@ -572,65 +585,123 @@
verify(mMockHandler).removeCallbacks(any());
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+ public void capturedLink_handleMenuBrowserLinkSetToCapturedLinkIfValid() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+ final DesktopModeWindowDecoration decor = createWindowDecoration(
+ taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* generic link */);
+
+ // Verify handle menu's browser link set as captured link
+ decor.createHandleMenu(mMockSplitScreenController);
+ verifyHandleMenuCreated(TEST_URI1);
+ }
+
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
public void capturedLink_postsOnCapturedLinkExpiredRunnable() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+ final DesktopModeWindowDecoration decor = createWindowDecoration(
+ taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
- final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo);
- decor.relayout(taskInfo);
- // Assert captured link is set
- assertTrue(decor.browserLinkAvailable());
- // Asset runnable posted to set captured link to expired
+ // Run runnable to set captured link to expired
verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong());
runnableArgument.getValue().run();
- assertFalse(decor.browserLinkAvailable());
+
+ // Verify captured link is no longer valid by verifying link is not set as handle menu
+ // browser link.
+ decor.createHandleMenu(mMockSplitScreenController);
+ verifyHandleMenuCreated(null /* uri */);
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
public void capturedLink_capturedLinkNotResetToSameLink() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
- final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo);
+ final DesktopModeWindowDecoration decor = createWindowDecoration(
+ taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
- // Set captured link and run on captured link expired runnable
- decor.relayout(taskInfo);
+ // Run runnable to set captured link to expired
verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong());
runnableArgument.getValue().run();
+ // Relayout decor with same captured link
decor.relayout(taskInfo);
- // Assert captured link not set to same value twice
- assertFalse(decor.browserLinkAvailable());
+
+ // Verify handle menu's browser link not set to captured link since link is expired
+ decor.createHandleMenu(mMockSplitScreenController);
+ verifyHandleMenuCreated(null /* uri */);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+ public void capturedLink_capturedLinkStillUsedIfExpiredAfterHandleMenuCreation() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+ final DesktopModeWindowDecoration decor = createWindowDecoration(
+ taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
+ final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
+
+ // Create handle menu before link expires
+ decor.createHandleMenu(mMockSplitScreenController);
+
+ // Run runnable to set captured link to expired
+ verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong());
+ runnableArgument.getValue().run();
+
+ // Verify handle menu's browser link is set to captured link since menu was opened before
+ // captured link expired
+ decor.createHandleMenu(mMockSplitScreenController);
+ verifyHandleMenuCreated(TEST_URI1);
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
public void capturedLink_capturedLinkExpiresAfterClick() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
- final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo);
-
- decor.relayout(taskInfo);
- // Assert captured link is set
- assertTrue(decor.browserLinkAvailable());
+ final DesktopModeWindowDecoration decor = createWindowDecoration(
+ taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
+ // Simulate menu opening and clicking open in browser button
+ decor.createHandleMenu(mMockSplitScreenController);
decor.onOpenInBrowserClick();
- //Assert Captured link expires after button is clicked
- assertFalse(decor.browserLinkAvailable());
+
+ // Verify handle menu's browser link not set to captured link since link not valid after
+ // open in browser clicked
+ decor.createHandleMenu(mMockSplitScreenController);
+ verifyHandleMenuCreated(null /* uri */);
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
public void capturedLink_openInBrowserListenerCalledOnClick() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
- final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo);
-
- decor.relayout(taskInfo);
+ final DesktopModeWindowDecoration decor = createWindowDecoration(
+ taskInfo, TEST_URI1 /* captured link */, null /* generic link */);
+ decor.createHandleMenu(mMockSplitScreenController);
decor.onOpenInBrowserClick();
verify(mMockOpenInBrowserClickListener).onClick(any(), any());
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+ public void genericLink_genericLinkUsedWhenCapturedLinkUnavailable() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+ final DesktopModeWindowDecoration decor = createWindowDecoration(
+ taskInfo, null /* captured link */, TEST_URI2 /* generic link */);
+
+ // Verify handle menu's browser link set as generic link no captured link is available
+ decor.createHandleMenu(mMockSplitScreenController);
+ verifyHandleMenuCreated(TEST_URI2);
+ }
+
+ private void verifyHandleMenuCreated(@Nullable Uri uri) {
+ verify(mMockHandleMenuFactory).create(any(), anyInt(), any(), any(), any(), any(), any(),
+ any(), anyBoolean(), eq(uri), anyInt(), anyInt(), anyInt());
+ }
+
private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) {
final OnTaskActionClickListener l = (taskId, tag) -> {};
decoration.setOnMaximizeOrRestoreClickListener(l);
@@ -657,6 +728,18 @@
R.dimen.rounded_corner_radius_bottom, fillValue);
}
+ private DesktopModeWindowDecoration createWindowDecoration(
+ ActivityManager.RunningTaskInfo taskInfo, @Nullable Uri capturedLink,
+ @Nullable Uri genericLink) {
+ taskInfo.capturedLink = capturedLink;
+ taskInfo.capturedLinkTimestamp = System.currentTimeMillis();
+ final String genericLinkString = genericLink == null ? null : genericLink.toString();
+ doReturn(genericLinkString).when(mMockGenericLinksParser).getGenericLink(any());
+ final DesktopModeWindowDecoration decor = createWindowDecoration(taskInfo);
+ // Relayout to set captured link
+ decor.relayout(taskInfo);
+ return decor;
+ }
private DesktopModeWindowDecoration createWindowDecoration(
ActivityManager.RunningTaskInfo taskInfo) {
@@ -669,14 +752,15 @@
final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
mMockDisplayController, mMockSplitScreenController, mMockShellTaskOrganizer,
taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor, mMockChoreographer,
- mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
+ mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, mMockGenericLinksParser,
SurfaceControl.Builder::new, mMockTransactionSupplier,
WindowContainerTransaction::new, SurfaceControl::new,
- mMockSurfaceControlViewHostFactory, maximizeMenuFactory);
+ mMockSurfaceControlViewHostFactory, maximizeMenuFactory, mMockHandleMenuFactory);
windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener,
mMockTouchEventListener, mMockTouchEventListener);
windowDecor.setExclusionRegionListener(mMockExclusionRegionListener);
windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener);
+ windowDecor.mDecorWindowContext = mContext;
return windowDecor;
}
@@ -692,8 +776,6 @@
"DesktopModeWindowDecorationTests");
taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor",
"DesktopModeWindowDecorationTests");
- taskInfo.capturedLink = TEST_URI;
- taskInfo.capturedLinkTimestamp = System.currentTimeMillis();
return taskInfo;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt
index 3fbab0f..56224b4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt
@@ -85,6 +85,23 @@
}
@Test
+ fun testNoMove_mouse_passesDownAndUp() {
+ assertTrue(dragDetector.onMotionEvent(
+ createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false)))
+ verify(eventHandler).handleMotionEvent(any(), argThat {
+ return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
+ it.source == InputDevice.SOURCE_MOUSE
+ })
+
+ assertTrue(dragDetector.onMotionEvent(
+ createMotionEvent(MotionEvent.ACTION_UP, isTouch = false)))
+ verify(eventHandler).handleMotionEvent(any(), argThat {
+ return@argThat it.action == MotionEvent.ACTION_UP && it.x == X && it.y == Y &&
+ it.source == InputDevice.SOURCE_MOUSE
+ })
+ }
+
+ @Test
fun testMoveInSlop_touch_passesDownAndUp() {
`when`(eventHandler.handleMotionEvent(any(), argThat {
return@argThat it.action == MotionEvent.ACTION_DOWN
@@ -166,6 +183,52 @@
}
@Test
+ fun testDownMoveDown_shouldIgnoreTheSecondDownMotion() {
+ assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
+ verify(eventHandler).handleMotionEvent(any(), argThat {
+ return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
+ it.source == InputDevice.SOURCE_TOUCHSCREEN
+ })
+
+ val newX = X + SLOP + 1
+ assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y)))
+ verify(eventHandler).handleMotionEvent(any(), argThat {
+ return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y &&
+ it.source == InputDevice.SOURCE_TOUCHSCREEN
+ })
+
+ assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
+ verify(eventHandler).handleMotionEvent(any(), argThat {
+ return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y &&
+ it.source == InputDevice.SOURCE_TOUCHSCREEN
+ })
+ }
+
+ @Test
+ fun testDownMouseMoveDownTouch_shouldIgnoreTheTouchDownMotion() {
+ assertTrue(dragDetector.onMotionEvent(
+ createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false)))
+ verify(eventHandler).handleMotionEvent(any(), argThat {
+ return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
+ it.source == InputDevice.SOURCE_MOUSE
+ })
+
+ val newX = X + SLOP + 1
+ assertTrue(dragDetector.onMotionEvent(
+ createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y, isTouch = false)))
+ verify(eventHandler).handleMotionEvent(any(), argThat {
+ return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y &&
+ it.source == InputDevice.SOURCE_MOUSE
+ })
+
+ assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
+ verify(eventHandler).handleMotionEvent(any(), argThat {
+ return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y &&
+ it.source == InputDevice.SOURCE_MOUSE
+ })
+ }
+
+ @Test
fun testPassesHoverEnter() {
`when`(eventHandler.handleMotionEvent(any(), argThat {
it.action == MotionEvent.ACTION_HOVER_ENTER
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index e548f8f..ed43aa3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -229,7 +229,7 @@
val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId,
onClickListener, onTouchListener, appIcon, appName, displayController,
splitScreenController, shouldShowWindowingPill = true,
- shouldShowBrowserPill = true, captionWidth = HANDLE_WIDTH, captionHeight = 50,
+ null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50,
captionX = captionX
)
handleMenu.show()
diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp
index ad963dd..93118aea 100644
--- a/libs/hwui/tests/common/TestUtils.cpp
+++ b/libs/hwui/tests/common/TestUtils.cpp
@@ -40,6 +40,7 @@
namespace android {
namespace uirenderer {
+std::mutex TestUtils::sMutex;
std::unordered_map<int, TestUtils::CallCounts> TestUtils::sMockFunctorCounts{};
SkColor TestUtils::interpolateColor(float fraction, SkColor start, SkColor end) {
diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h
index 0ede902..8ab2b16 100644
--- a/libs/hwui/tests/common/TestUtils.h
+++ b/libs/hwui/tests/common/TestUtils.h
@@ -305,22 +305,26 @@
.onSync =
[](int functor, void* client_data, const WebViewSyncData& data) {
expectOnRenderThread("onSync");
+ std::scoped_lock lock(sMutex);
sMockFunctorCounts[functor].sync++;
},
.onContextDestroyed =
[](int functor, void* client_data) {
expectOnRenderThread("onContextDestroyed");
+ std::scoped_lock lock(sMutex);
sMockFunctorCounts[functor].contextDestroyed++;
},
.onDestroyed =
[](int functor, void* client_data) {
expectOnRenderThread("onDestroyed");
+ std::scoped_lock lock(sMutex);
sMockFunctorCounts[functor].destroyed++;
},
.removeOverlays =
[](int functor, void* data,
void (*mergeTransaction)(ASurfaceTransaction*)) {
expectOnRenderThread("removeOverlays");
+ std::scoped_lock lock(sMutex);
sMockFunctorCounts[functor].removeOverlays++;
},
};
@@ -329,6 +333,7 @@
callbacks.gles.draw = [](int functor, void* client_data, const DrawGlInfo& params,
const WebViewOverlayData& overlay_params) {
expectOnRenderThread("draw");
+ std::scoped_lock lock(sMutex);
sMockFunctorCounts[functor].glesDraw++;
};
break;
@@ -336,15 +341,18 @@
callbacks.vk.initialize = [](int functor, void* data,
const VkFunctorInitParams& params) {
expectOnRenderThread("initialize");
+ std::scoped_lock lock(sMutex);
sMockFunctorCounts[functor].vkInitialize++;
};
callbacks.vk.draw = [](int functor, void* data, const VkFunctorDrawParams& params,
const WebViewOverlayData& overlayParams) {
expectOnRenderThread("draw");
+ std::scoped_lock lock(sMutex);
sMockFunctorCounts[functor].vkDraw++;
};
callbacks.vk.postDraw = [](int functor, void* data) {
expectOnRenderThread("postDraw");
+ std::scoped_lock lock(sMutex);
sMockFunctorCounts[functor].vkPostDraw++;
};
break;
@@ -352,11 +360,16 @@
return callbacks;
}
- static CallCounts& countsForFunctor(int functor) { return sMockFunctorCounts[functor]; }
+ static CallCounts copyCountsForFunctor(int functor) {
+ std::scoped_lock lock(sMutex);
+ return sMockFunctorCounts[functor];
+ }
static SkFont defaultFont();
private:
+ // guards sMockFunctorCounts
+ static std::mutex sMutex;
static std::unordered_map<int, CallCounts> sMockFunctorCounts;
static void syncHierarchyPropertiesAndDisplayListImpl(RenderNode* node) {
diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp
index e727ea8..690a60a4 100644
--- a/libs/hwui/tests/unit/RenderNodeTests.cpp
+++ b/libs/hwui/tests/unit/RenderNodeTests.cpp
@@ -239,19 +239,21 @@
TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) {
TestUtils::syncHierarchyPropertiesAndDisplayList(node);
});
- auto& counts = TestUtils::countsForFunctor(functor);
+ auto counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(1, counts.sync);
EXPECT_EQ(0, counts.destroyed);
TestUtils::recordNode(*node, [&](Canvas& canvas) {
canvas.drawWebViewFunctor(functor);
});
+ counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(1, counts.sync);
EXPECT_EQ(0, counts.destroyed);
TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) {
TestUtils::syncHierarchyPropertiesAndDisplayList(node);
});
+ counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(2, counts.sync);
EXPECT_EQ(0, counts.destroyed);
@@ -265,6 +267,7 @@
});
// Fence on any remaining post'd work
TestUtils::runOnRenderThreadUnmanaged([] (RenderThread&) {});
+ counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(2, counts.sync);
EXPECT_EQ(1, counts.destroyed);
}
diff --git a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
index 064d42e..26b4729 100644
--- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
+++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
@@ -101,7 +101,7 @@
SkCanvas dummyCanvas;
int functor1 = TestUtils::createMockFunctor();
- auto& counts = TestUtils::countsForFunctor(functor1);
+ auto counts = TestUtils::copyCountsForFunctor(functor1);
skiaDL.mChildFunctors.push_back(
skiaDL.allocateDrawable<GLFunctorDrawable>(functor1, &dummyCanvas));
WebViewFunctor_release(functor1);
@@ -118,6 +118,7 @@
});
});
+ counts = TestUtils::copyCountsForFunctor(functor1);
EXPECT_EQ(counts.sync, 1);
EXPECT_EQ(counts.destroyed, 0);
EXPECT_EQ(vectorDrawable.mutateProperties()->getBounds(), bounds);
@@ -126,6 +127,7 @@
TestUtils::runOnRenderThread([](auto&) {
// Fence
});
+ counts = TestUtils::copyCountsForFunctor(functor1);
EXPECT_EQ(counts.destroyed, 1);
}
diff --git a/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp b/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp
index 5e8f13d..09ce98a 100644
--- a/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp
+++ b/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp
@@ -40,7 +40,7 @@
TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) {
// Empty, don't care
});
- auto& counts = TestUtils::countsForFunctor(functor);
+ auto counts = TestUtils::copyCountsForFunctor(functor);
// We never initialized, so contextDestroyed == 0
EXPECT_EQ(0, counts.contextDestroyed);
EXPECT_EQ(1, counts.destroyed);
@@ -59,7 +59,7 @@
TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) {
// fence
});
- auto& counts = TestUtils::countsForFunctor(functor);
+ auto counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(0, counts.sync);
EXPECT_EQ(0, counts.contextDestroyed);
EXPECT_EQ(0, counts.destroyed);
@@ -69,6 +69,7 @@
handle->sync(syncData);
});
+ counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(1, counts.sync);
TestUtils::runOnRenderThreadUnmanaged([&](auto&) {
@@ -76,6 +77,7 @@
handle->sync(syncData);
});
+ counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(2, counts.sync);
handle.clear();
@@ -84,6 +86,7 @@
// fence
});
+ counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(2, counts.sync);
EXPECT_EQ(0, counts.contextDestroyed);
EXPECT_EQ(1, counts.destroyed);
@@ -98,7 +101,6 @@
auto handle = WebViewFunctorManager::instance().handleFor(functor);
ASSERT_TRUE(handle);
WebViewFunctor_release(functor);
- auto& counts = TestUtils::countsForFunctor(functor);
for (int i = 0; i < 5; i++) {
TestUtils::runOnRenderThreadUnmanaged([&](auto&) {
WebViewSyncData syncData;
@@ -112,6 +114,7 @@
TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) {
// fence
});
+ auto counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(5, counts.sync);
EXPECT_EQ(10, counts.glesDraw);
EXPECT_EQ(1, counts.contextDestroyed);
@@ -127,13 +130,13 @@
auto handle = WebViewFunctorManager::instance().handleFor(functor);
ASSERT_TRUE(handle);
WebViewFunctor_release(functor);
- auto& counts = TestUtils::countsForFunctor(functor);
TestUtils::runOnRenderThreadUnmanaged([&](auto&) {
WebViewSyncData syncData;
handle->sync(syncData);
DrawGlInfo drawInfo;
handle->drawGl(drawInfo);
});
+ auto counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(1, counts.sync);
EXPECT_EQ(1, counts.glesDraw);
EXPECT_EQ(0, counts.contextDestroyed);
@@ -141,6 +144,7 @@
TestUtils::runOnRenderThreadUnmanaged([](auto& rt) {
rt.destroyRenderingContext();
});
+ counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(1, counts.sync);
EXPECT_EQ(1, counts.glesDraw);
EXPECT_EQ(1, counts.contextDestroyed);
@@ -151,6 +155,7 @@
DrawGlInfo drawInfo;
handle->drawGl(drawInfo);
});
+ counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(2, counts.sync);
EXPECT_EQ(2, counts.glesDraw);
EXPECT_EQ(1, counts.contextDestroyed);
@@ -159,6 +164,7 @@
TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) {
// fence
});
+ counts = TestUtils::copyCountsForFunctor(functor);
EXPECT_EQ(2, counts.sync);
EXPECT_EQ(2, counts.glesDraw);
EXPECT_EQ(2, counts.contextDestroyed);
diff --git a/packages/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING
index 76d7ab1..b3fb1e7 100644
--- a/packages/PackageInstaller/TEST_MAPPING
+++ b/packages/PackageInstaller/TEST_MAPPING
@@ -9,6 +9,9 @@
]
},
{
+ "name": "CtsUpdateOwnershipEnforcementTestCases"
+ },
+ {
"name": "CtsNoPermissionTestCases"
},
{
diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts
index d6345ce..f36344a 100644
--- a/packages/SettingsLib/Spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/build.gradle.kts
@@ -29,7 +29,7 @@
allprojects {
extra["androidTop"] = androidTop
- extra["jetpackComposeVersion"] = "1.7.0-beta02"
+ extra["jetpackComposeVersion"] = "1.7.0-beta05"
}
subprojects {
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
index a842009..1cca73a 100644
--- a/packages/SettingsLib/Spa/gradle/libs.versions.toml
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -15,7 +15,7 @@
#
[versions]
-agp = "8.5.0"
+agp = "8.5.1"
compose-compiler = "1.5.11"
dexmaker-mockito = "2.28.3"
jvm = "17"
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.8-bin.zip b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.8-bin.zip
deleted file mode 100644
index 77e6ad3..0000000
--- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.8-bin.zip
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.9-bin.zip b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.9-bin.zip
new file mode 100644
index 0000000..9a97e46
--- /dev/null
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.9-bin.zip
Binary files differ
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar
index e644113..2c35211 100644
--- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
index 91d2a3a..9f29c77 100644
--- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
@@ -16,6 +16,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=gradle-8.8-bin.zip
+distributionUrl=gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/packages/SettingsLib/Spa/gradlew b/packages/SettingsLib/Spa/gradlew
index b740cf1..f5feea6 100755
--- a/packages/SettingsLib/Spa/gradlew
+++ b/packages/SettingsLib/Spa/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -84,7 +86,8 @@
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts
index 492d7c0..ce3d96e 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/spa/build.gradle.kts
@@ -53,14 +53,14 @@
dependencies {
api(project(":SettingsLibColor"))
- api("androidx.appcompat:appcompat:1.7.0-rc01")
- api("androidx.compose.material3:material3:1.3.0-beta02")
+ api("androidx.appcompat:appcompat:1.7.0")
+ api("androidx.compose.material3:material3:1.3.0-beta04")
api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion")
api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion")
api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion")
api("androidx.lifecycle:lifecycle-livedata-ktx")
api("androidx.lifecycle:lifecycle-runtime-compose")
- api("androidx.navigation:navigation-compose:2.8.0-beta02")
+ api("androidx.navigation:navigation-compose:2.8.0-beta05")
api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha")
api("com.google.android.material:material:1.11.0")
debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion")
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index cfd74d4..ce997bf 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1010,10 +1010,8 @@
<!-- UI debug setting: force allow on external summary [CHAR LIMIT=150] -->
<string name="force_resizable_activities_summary">Make all activities resizable for multi-window, regardless of manifest values.</string>
- <!-- UI debug setting: enable legacy freeform window support [CHAR LIMIT=50] -->
- <string name="enable_freeform_support">Enable freeform windows (legacy)</string>
- <!-- UI debug setting: enable legacy freeform window support summary [CHAR LIMIT=150] -->
- <string name="enable_freeform_support_summary">Enable support for experimental legacy freeform windows.</string>
+ <!-- Title for a toggle that enables support for windows to be in freeform (apps run in resizable windows). [CHAR LIMIT=50] -->
+ <string name="enable_freeform_support">Enable freeform window support</string>
<!-- Local (desktop) backup password menu title [CHAR LIMIT=25] -->
<string name="local_backup_password_title">Desktop backup password</string>
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 5f23651..2b8b23e 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -282,5 +282,6 @@
Settings.Secure.ON_DEVICE_INFERENCE_UNBIND_TIMEOUT_MS,
Settings.Secure.ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS,
Settings.Secure.MANDATORY_BIOMETRICS,
+ Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED,
};
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index c8da8af..cc5302b 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -441,5 +441,7 @@
VALIDATORS.put(Secure.ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS, NONE_NEGATIVE_LONG_VALIDATOR);
VALIDATORS.put(Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.MANDATORY_BIOMETRICS, new InclusiveIntegerRangeValidator(0, 1));
+ VALIDATORS.put(Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED,
+ new InclusiveIntegerRangeValidator(0, 1));
}
}
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 1c02d3f..68e968f 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
@@ -1004,6 +1004,7 @@
}
.thenIf(viewModel.isEditMode) {
Modifier.semantics {
+ onClick(clickActionLabel, null)
contentDescription = accessibilityLabel
val deleteAction =
CustomAccessibilityAction(removeWidgetActionLabel) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
index c5dab33..38a3474 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
@@ -58,7 +58,7 @@
communalContent: List<CommunalContentModel>,
private val onAddWidget:
(componentName: ComponentName, user: UserHandle, priority: Int) -> Unit,
- private val onDeleteWidget: (id: Int) -> Unit,
+ private val onDeleteWidget: (id: Int, componentName: ComponentName, priority: Int) -> Unit,
private val onReorderWidgets: (widgetIdToPriorityMap: Map<Int, Int>) -> Unit,
) {
var list = communalContent.toMutableStateList()
@@ -74,7 +74,7 @@
if (list[indexToRemove].isWidgetContent()) {
val widget = list[indexToRemove] as CommunalContentModel.WidgetContent
list.apply { removeAt(indexToRemove) }
- onDeleteWidget(widget.appWidgetId)
+ onDeleteWidget(widget.appWidgetId, widget.componentName, widget.priority)
}
}
@@ -110,7 +110,7 @@
// reorder and then add the new widget
onReorderWidgets(widgetIdToPriorityMap)
if (newItemComponentName != null && newItemUser != null && newItemIndex != null) {
- onAddWidget(newItemComponentName, newItemUser, /*priority=*/ list.size - newItemIndex)
+ onAddWidget(newItemComponentName, newItemUser, /* priority= */ list.size - newItemIndex)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt
index 620892a..b4c1a2e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt
@@ -50,7 +50,6 @@
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
@@ -102,8 +101,6 @@
val interactionSource = remember { MutableInteractionSource() }
val focusRequester = remember { FocusRequester() }
- val context = LocalContext.current
-
LaunchedEffect(Unit) {
// Adding a delay to ensure the animation completes before requesting focus
delay(250)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 665be53..5b328b8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -52,11 +52,19 @@
* and [onStop] methods.
*/
internal interface DragController {
- /** Drag the current scene by [delta] pixels. */
- fun onDrag(delta: Float)
+ /**
+ * Drag the current scene by [delta] pixels.
+ *
+ * @return the consumed [delta]
+ */
+ fun onDrag(delta: Float): Float
- /** Starts a transition to a target scene. */
- fun onStop(velocity: Float, canChangeScene: Boolean)
+ /**
+ * Starts a transition to a target scene.
+ *
+ * @return the consumed [velocity]
+ */
+ fun onStop(velocity: Float, canChangeScene: Boolean): Float
}
internal class DraggableHandlerImpl(
@@ -272,8 +280,10 @@
*
* @return the consumed delta
*/
- override fun onDrag(delta: Float) {
- if (delta == 0f || !isDrivingTransition || swipeTransition.isFinishing) return
+ override fun onDrag(delta: Float): Float {
+ if (delta == 0f || !isDrivingTransition || swipeTransition.isFinishing) {
+ return 0f
+ }
swipeTransition.dragOffset += delta
val (fromScene, acceleratedOffset) =
@@ -289,7 +299,7 @@
if (result == null) {
onStop(velocity = delta, canChangeScene = true)
- return
+ return 0f
}
if (
@@ -314,6 +324,8 @@
updateTransition(swipeTransition)
}
+
+ return delta
}
/**
@@ -351,10 +363,10 @@
}
}
- override fun onStop(velocity: Float, canChangeScene: Boolean) {
+ override fun onStop(velocity: Float, canChangeScene: Boolean): Float {
// The state was changed since the drag started; don't do anything.
if (!isDrivingTransition || swipeTransition.isFinishing) {
- return
+ return 0f
}
// Important: Make sure that all the code here references the current transition when
@@ -440,7 +452,7 @@
if (result == null) {
// We will not animate
swipeTransition.snapToScene(fromScene.key)
- return
+ return 0f
}
val newSwipeTransition =
@@ -462,6 +474,9 @@
animateTo(targetScene = fromScene, targetOffset = 0f)
}
}
+
+ // The onStop animation consumes any remaining velocity.
+ return velocity
}
/**
@@ -1081,17 +1096,13 @@
// TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is
// initiated in a nested child.
controller.onDrag(delta = offsetAvailable)
-
- offsetAvailable
},
onStop = { velocityAvailable ->
val controller = dragController ?: error("Should be called after onStart")
- controller.onStop(velocity = velocityAvailable, canChangeScene = canChangeScene)
-
- dragController = null
- // The onDragStopped animation consumes any remaining velocity.
- velocityAvailable
+ controller
+ .onStop(velocity = velocityAvailable, canChangeScene = canChangeScene)
+ .also { dragController = null }
},
)
}
@@ -1106,7 +1117,7 @@
internal const val OffsetVisibilityThreshold = 0.5f
private object NoOpDragController : DragController {
- override fun onDrag(delta: Float) {}
+ override fun onDrag(delta: Float) = 0f
- override fun onStop(velocity: Float, canChangeScene: Boolean) {}
+ override fun onStop(velocity: Float, canChangeScene: Boolean) = 0f
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 7a5a84e..c8bbb14 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -212,7 +212,8 @@
draggableHandler: DraggableHandler,
startedPosition: Offset = Offset.Zero,
overSlop: Float = 0f,
- pointersDown: Int = 1
+ pointersDown: Int = 1,
+ expectedConsumed: Boolean = true,
): DragController {
val dragController =
draggableHandler.onDragStarted(
@@ -222,17 +223,23 @@
)
// MultiPointerDraggable will always call onDelta with the initial overSlop right after
- dragController.onDragDelta(pixels = overSlop)
+ dragController.onDragDelta(pixels = overSlop, expectedConsumed = expectedConsumed)
return dragController
}
- fun DragController.onDragDelta(pixels: Float) {
- onDrag(delta = pixels)
+ fun DragController.onDragDelta(pixels: Float, expectedConsumed: Boolean = true) {
+ val consumed = onDrag(delta = pixels)
+ assertThat(consumed).isEqualTo(if (expectedConsumed) pixels else 0f)
}
- fun DragController.onDragStopped(velocity: Float, canChangeScene: Boolean = true) {
- onStop(velocity, canChangeScene)
+ fun DragController.onDragStopped(
+ velocity: Float,
+ canChangeScene: Boolean = true,
+ expectedConsumed: Boolean = true
+ ) {
+ val consumed = onStop(velocity, canChangeScene)
+ assertThat(consumed).isEqualTo(if (expectedConsumed) velocity else 0f)
}
fun NestedScrollConnection.scroll(
@@ -360,10 +367,18 @@
@Test
fun onDragStartedWithoutActionsInBothDirections_stayIdle() = runGestureTest {
- onDragStarted(horizontalDraggableHandler, overSlop = up(fractionOfScreen = 0.3f))
+ onDragStarted(
+ horizontalDraggableHandler,
+ overSlop = up(fractionOfScreen = 0.3f),
+ expectedConsumed = false,
+ )
assertIdle(currentScene = SceneA)
- onDragStarted(horizontalDraggableHandler, overSlop = down(fractionOfScreen = 0.3f))
+ onDragStarted(
+ horizontalDraggableHandler,
+ overSlop = down(fractionOfScreen = 0.3f),
+ expectedConsumed = false,
+ )
assertIdle(currentScene = SceneA)
}
@@ -489,19 +504,19 @@
// start accelaratedScroll and scroll over to B -> null
val dragController2 = onDragStartedImmediately()
- dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f))
- dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f))
+ dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false)
+ dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false)
// here onDragStopped is already triggered, but subsequent onDelta/onDragStopped calls may
// still be called. Make sure that they don't crash or change the scene
- dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f))
+ dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false)
dragController2.onDragStopped(velocity = 0f)
advanceUntilIdle()
assertIdle(SceneB)
// These events can still come in after the animation has settled
- dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f))
+ dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false)
dragController2.onDragStopped(velocity = 0f)
assertIdle(SceneB)
}
@@ -845,7 +860,7 @@
assertThat(progress).isEqualTo(0.2f)
// this should be ignored, we are scrolling now!
- dragController.onDragStopped(-velocityThreshold)
+ dragController.onDragStopped(-velocityThreshold, expectedConsumed = false)
assertTransition(currentScene = SceneA)
nestedScroll.scroll(available = -offsetY10)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
index ecafb17..b98400a 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
@@ -49,6 +49,21 @@
class MultiPointerDraggableTest {
@get:Rule val rule = createComposeRule()
+ private class SimpleDragController(
+ val onDrag: () -> Unit,
+ val onStop: () -> Unit,
+ ) : DragController {
+ override fun onDrag(delta: Float): Float {
+ onDrag()
+ return delta
+ }
+
+ override fun onStop(velocity: Float, canChangeScene: Boolean): Float {
+ onStop()
+ return velocity
+ }
+ }
+
@Test
fun cancellingPointerCallsOnDragStopped() {
val size = 200f
@@ -70,15 +85,10 @@
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
started = true
- object : DragController {
- override fun onDrag(delta: Float) {
- dragged = true
- }
-
- override fun onStop(velocity: Float, canChangeScene: Boolean) {
- stopped = true
- }
- }
+ SimpleDragController(
+ onDrag = { dragged = true },
+ onStop = { stopped = true },
+ )
},
)
)
@@ -142,15 +152,10 @@
startDragImmediately = { true },
onDragStarted = { _, _, _ ->
started = true
- object : DragController {
- override fun onDrag(delta: Float) {
- dragged = true
- }
-
- override fun onStop(velocity: Float, canChangeScene: Boolean) {
- stopped = true
- }
- }
+ SimpleDragController(
+ onDrag = { dragged = true },
+ onStop = { stopped = true },
+ )
},
)
.pointerInput(Unit) {
@@ -218,15 +223,10 @@
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
started = true
- object : DragController {
- override fun onDrag(delta: Float) {
- dragged = true
- }
-
- override fun onStop(velocity: Float, canChangeScene: Boolean) {
- stopped = true
- }
- }
+ SimpleDragController(
+ onDrag = { dragged = true },
+ onStop = { stopped = true },
+ )
},
)
) {
@@ -341,15 +341,10 @@
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
started = true
- object : DragController {
- override fun onDrag(delta: Float) {
- dragged = true
- }
-
- override fun onStop(velocity: Float, canChangeScene: Boolean) {
- stopped = true
- }
- }
+ SimpleDragController(
+ onDrag = { dragged = true },
+ onStop = { stopped = true },
+ )
},
)
) {
@@ -447,15 +442,10 @@
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
verticalStarted = true
- object : DragController {
- override fun onDrag(delta: Float) {
- verticalDragged = true
- }
-
- override fun onStop(velocity: Float, canChangeScene: Boolean) {
- verticalStopped = true
- }
- }
+ SimpleDragController(
+ onDrag = { verticalDragged = true },
+ onStop = { verticalStopped = true },
+ )
},
)
.multiPointerDraggable(
@@ -464,15 +454,10 @@
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
horizontalStarted = true
- object : DragController {
- override fun onDrag(delta: Float) {
- horizontalDragged = true
- }
-
- override fun onStop(velocity: Float, canChangeScene: Boolean) {
- horizontalStopped = true
- }
- }
+ SimpleDragController(
+ onDrag = { horizontalDragged = true },
+ onStop = { horizontalStopped = true },
+ )
},
)
)
@@ -567,11 +552,10 @@
},
onDragStarted = { _, _, _ ->
started = true
- object : DragController {
- override fun onDrag(delta: Float) {}
-
- override fun onStop(velocity: Float, canChangeScene: Boolean) {}
- }
+ SimpleDragController(
+ onDrag = { /* do nothing */ },
+ onStop = { /* do nothing */ },
+ )
},
)
) {}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index 4ad020f..bbd2f6b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -16,11 +16,13 @@
package com.android.systemui.communal
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
+import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
@@ -103,6 +105,7 @@
}
@Test
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun keyguardGoesAway_whenLaunchingWidget_doNotForceBlankScene() =
with(kosmos) {
testScope.runTest {
@@ -123,6 +126,7 @@
}
@Test
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun keyguardGoesAway_whenNotLaunchingWidget_forceBlankScene() =
with(kosmos) {
testScope.runTest {
@@ -143,6 +147,7 @@
}
@Test
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun keyguardGoesAway_whenInEditMode_doesNotChangeScene() =
with(kosmos) {
testScope.runTest {
@@ -180,6 +185,7 @@
}
@Test
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun occluded_forceBlankScene() =
with(kosmos) {
testScope.runTest {
@@ -199,6 +205,7 @@
}
@Test
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun occluded_doesNotForceBlankSceneIfLaunchingActivityOverLockscreen() =
with(kosmos) {
testScope.runTest {
@@ -218,6 +225,7 @@
}
@Test
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun deviceDocked_doesNotForceCommunalIfTransitioningFromCommunal() =
with(kosmos) {
testScope.runTest {
@@ -235,6 +243,7 @@
}
@Test
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun deviceAsleep_forceBlankSceneAfterTimeout() =
with(kosmos) {
testScope.runTest {
@@ -256,6 +265,7 @@
}
@Test
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun deviceAsleep_wakesUpBeforeTimeout_noChangeInScene() =
with(kosmos) {
testScope.runTest {
@@ -483,6 +493,7 @@
}
@Test
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun transitionFromDozingToGlanceableHub_forcesCommunal() =
with(kosmos) {
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index c707ebf..ca81838 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -696,7 +696,7 @@
CommunalWidgetContentModel.Pending(
appWidgetId = 2,
priority = 2,
- packageName = "pk_2",
+ componentName = ComponentName("pk_2", "cls_2"),
icon = fakeIcon,
user = mainUser,
),
@@ -731,7 +731,7 @@
CommunalWidgetContentModel.Pending(
appWidgetId = 1,
priority = 1,
- packageName = "pk_1",
+ componentName = ComponentName("pk_1", "cls_1"),
icon = fakeIcon,
user = mainUser,
),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 9539c04..0242c2d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -19,10 +19,8 @@
import android.app.admin.DevicePolicyManager
import android.app.admin.devicePolicyManager
-import android.appwidget.AppWidgetProviderInfo
import android.content.Intent
import android.content.pm.UserInfo
-import android.graphics.Bitmap
import android.os.UserHandle
import android.os.UserManager
import android.os.userManager
@@ -52,7 +50,6 @@
import com.android.systemui.communal.domain.model.CommunalTransitionProgressModel
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.shared.model.EditModeState
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
import com.android.systemui.coroutines.collectLastValue
@@ -251,18 +248,16 @@
runCurrent()
// Widgets available.
- val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
- val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
- val widgets = listOf(widget1, widget2, widget3)
- widgetRepository.setCommunalWidgets(widgets)
+ widgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id)
+ widgetRepository.addWidget(appWidgetId = 2, userId = MAIN_USER_INFO.id)
+ widgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id)
val widgetContent by collectLastValue(underTest.widgetContent)
- assertThat(widgetContent!!).isNotEmpty()
- widgetContent!!.forEachIndexed { index, model ->
- assertThat(model.appWidgetId).isEqualTo(widgets[index].appWidgetId)
- }
+ assertThat(checkNotNull(widgetContent)).isNotEmpty()
+ assertThat(widgetContent!![0].appWidgetId).isEqualTo(1)
+ assertThat(widgetContent!![1].appWidgetId).isEqualTo(2)
+ assertThat(widgetContent!![2].appWidgetId).isEqualTo(3)
}
@Test
@@ -839,11 +834,9 @@
val widgetContent by collectLastValue(underTest.widgetContent)
// Given three widgets, and one of them is associated with pre-existing work profile.
- val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
- val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
- val widgets = listOf(widget1, widget2, widget3)
- widgetRepository.setCommunalWidgets(widgets)
+ widgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id)
+ widgetRepository.addWidget(appWidgetId = 2, userId = MAIN_USER_INFO.id)
+ widgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id)
// One widget is filtered out and the remaining two link to main user id.
assertThat(checkNotNull(widgetContent).size).isEqualTo(2)
@@ -882,11 +875,9 @@
whenever(userManager.isManagedProfile(eq(USER_INFO_WORK.id))).thenReturn(true)
val widgetContent by collectLastValue(underTest.widgetContent)
- val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
- val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
- val widgets = listOf(widget1, widget2, widget3)
- widgetRepository.setCommunalWidgets(widgets)
+ widgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id)
+ widgetRepository.addWidget(appWidgetId = 2, userId = MAIN_USER_INFO.id)
+ widgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id)
// The work profile widget is in quiet mode, while other widgets are not.
assertThat(widgetContent).hasSize(3)
@@ -927,11 +918,9 @@
val widgetContent by collectLastValue(underTest.widgetContent)
// One available work widget, one pending work widget, and one regular available widget.
- val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id)
- val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
- val widgets = listOf(widget1, widget2, widget3)
- widgetRepository.setCommunalWidgets(widgets)
+ widgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id)
+ widgetRepository.addPendingWidget(appWidgetId = 2, userId = USER_INFO_WORK.id)
+ widgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id)
setKeyguardFeaturesDisabled(
USER_INFO_WORK,
@@ -962,11 +951,9 @@
val widgetContent by collectLastValue(underTest.widgetContent)
// Given three widgets, and one of them is associated with work profile.
- val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id)
- val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
- val widgets = listOf(widget1, widget2, widget3)
- widgetRepository.setCommunalWidgets(widgets)
+ widgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id)
+ widgetRepository.addPendingWidget(appWidgetId = 2, userId = USER_INFO_WORK.id)
+ widgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id)
setKeyguardFeaturesDisabled(
USER_INFO_WORK,
@@ -1088,47 +1075,6 @@
)
}
- private fun createWidgetForUser(
- appWidgetId: Int,
- userId: Int
- ): CommunalWidgetContentModel.Available =
- mock<CommunalWidgetContentModel.Available> {
- whenever(this.appWidgetId).thenReturn(appWidgetId)
- val providerInfo =
- mock<AppWidgetProviderInfo>().apply {
- widgetCategory = AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
- }
- whenever(providerInfo.profile).thenReturn(UserHandle(userId))
- whenever(this.providerInfo).thenReturn(providerInfo)
- }
-
- private fun createPendingWidgetForUser(
- appWidgetId: Int,
- priority: Int = 0,
- packageName: String = "",
- icon: Bitmap? = null,
- userId: Int = 0,
- ): CommunalWidgetContentModel.Pending {
- return CommunalWidgetContentModel.Pending(
- appWidgetId = appWidgetId,
- priority = priority,
- packageName = packageName,
- icon = icon,
- user = UserHandle(userId),
- )
- }
-
- private fun createWidgetWithCategory(
- appWidgetId: Int,
- category: Int
- ): CommunalWidgetContentModel =
- mock<CommunalWidgetContentModel.Available> {
- whenever(this.appWidgetId).thenReturn(appWidgetId)
- val providerInfo = mock<AppWidgetProviderInfo>().apply { widgetCategory = category }
- whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))
- whenever(this.providerInfo).thenReturn(providerInfo)
- }
-
private companion object {
val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
val USER_INFO_WORK =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
index f7f70c1..ad73853 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
@@ -34,8 +34,11 @@
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.realKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
import com.android.systemui.keyguard.shared.model.TransitionInfo
@@ -46,6 +49,8 @@
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -53,6 +58,7 @@
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -211,8 +217,15 @@
@Test
fun transition_from_hub_end_in_dream() =
testScope.runTest {
+ // Device is dreaming and not dozing.
+ kosmos.powerInteractor.setAwakeForTest()
+ kosmos.fakeKeyguardRepository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ kosmos.fakeKeyguardRepository.setKeyguardOccluded(true)
kosmos.fakeKeyguardRepository.setDreaming(true)
- runCurrent()
+ kosmos.fakeKeyguardRepository.setDreamingWithOverlay(true)
+ advanceTimeBy(100L)
sceneTransitions.value = hubToBlank
@@ -254,6 +267,100 @@
)
}
+ /** Transition from hub to occluded. */
+ @Test
+ fun transition_from_hub_end_in_occluded() =
+ testScope.runTest {
+ kosmos.fakeKeyguardRepository.setKeyguardOccluded(true)
+ runCurrent()
+
+ sceneTransitions.value = hubToBlank
+
+ val currentStep by collectLastValue(keyguardTransitionRepository.transitions)
+
+ assertThat(currentStep)
+ .isEqualTo(
+ TransitionStep(
+ from = GLANCEABLE_HUB,
+ to = OCCLUDED,
+ transitionState = STARTED,
+ value = 0f,
+ ownerName = ownerName,
+ )
+ )
+
+ progress.emit(0.4f)
+ assertThat(currentStep)
+ .isEqualTo(
+ TransitionStep(
+ from = GLANCEABLE_HUB,
+ to = OCCLUDED,
+ transitionState = RUNNING,
+ value = 0.4f,
+ ownerName = ownerName,
+ )
+ )
+
+ sceneTransitions.value = Idle(CommunalScenes.Blank)
+ assertThat(currentStep)
+ .isEqualTo(
+ TransitionStep(
+ from = GLANCEABLE_HUB,
+ to = OCCLUDED,
+ transitionState = FINISHED,
+ value = 1f,
+ ownerName = ownerName,
+ )
+ )
+ }
+
+ /** Transition from hub to gone. */
+ @Test
+ fun transition_from_hub_end_in_gone() =
+ testScope.runTest {
+ kosmos.fakeKeyguardRepository.setKeyguardGoingAway(true)
+ runCurrent()
+
+ sceneTransitions.value = hubToBlank
+
+ val currentStep by collectLastValue(keyguardTransitionRepository.transitions)
+
+ assertThat(currentStep)
+ .isEqualTo(
+ TransitionStep(
+ from = GLANCEABLE_HUB,
+ to = GONE,
+ transitionState = STARTED,
+ value = 0f,
+ ownerName = ownerName,
+ )
+ )
+
+ progress.emit(0.4f)
+ assertThat(currentStep)
+ .isEqualTo(
+ TransitionStep(
+ from = GLANCEABLE_HUB,
+ to = GONE,
+ transitionState = RUNNING,
+ value = 0.4f,
+ ownerName = ownerName,
+ )
+ )
+
+ sceneTransitions.value = Idle(CommunalScenes.Blank)
+ assertThat(currentStep)
+ .isEqualTo(
+ TransitionStep(
+ from = GLANCEABLE_HUB,
+ to = GONE,
+ transitionState = FINISHED,
+ value = 1f,
+ ownerName = ownerName,
+ )
+ )
+ }
+
/** Transition from blank to hub, then settle back in blank. */
@Test
fun transition_from_blank_end_in_blank() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalMetricsLoggerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalMetricsLoggerTest.kt
new file mode 100644
index 0000000..35df641
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalMetricsLoggerTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.communal.log
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.shared.log.CommunalMetricsLogger
+import com.android.systemui.shared.system.SysUiStatsLog
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalMetricsLoggerTest : SysuiTestCase() {
+ private val statsLogProxy = mock<CommunalMetricsLogger.StatsLogProxy>()
+
+ private val loggablePrefixes = listOf("com.blue.", "com.red.")
+ private lateinit var underTest: CommunalMetricsLogger
+
+ @Before
+ fun setUp() {
+ underTest = CommunalMetricsLogger(loggablePrefixes, statsLogProxy)
+ }
+
+ @Test
+ fun logAddWidget_componentNotLoggable_doNotLog() {
+ underTest.logAddWidget(
+ componentName = "com.green.package/my_test_widget",
+ rank = 1,
+ )
+ verify(statsLogProxy, never())
+ .writeCommunalHubWidgetEventReported(anyInt(), any(), anyInt())
+ }
+
+ @Test
+ fun logAddWidget_componentLoggable_logAddEvent() {
+ underTest.logAddWidget(
+ componentName = "com.blue.package/my_test_widget",
+ rank = 1,
+ )
+ verify(statsLogProxy)
+ .writeCommunalHubWidgetEventReported(
+ SysUiStatsLog.COMMUNAL_HUB_WIDGET_EVENT_REPORTED__ACTION__ADD,
+ "com.blue.package/my_test_widget",
+ 1,
+ )
+ }
+
+ @Test
+ fun logRemoveWidget_componentNotLoggable_doNotLog() {
+ underTest.logRemoveWidget(
+ componentName = "com.yellow.package/my_test_widget",
+ rank = 2,
+ )
+ verify(statsLogProxy, never())
+ .writeCommunalHubWidgetEventReported(anyInt(), any(), anyInt())
+ }
+
+ @Test
+ fun logRemoveWidget_componentLoggable_logRemoveEvent() {
+ underTest.logRemoveWidget(
+ componentName = "com.red.package/my_test_widget",
+ rank = 2,
+ )
+ verify(statsLogProxy)
+ .writeCommunalHubWidgetEventReported(
+ SysUiStatsLog.COMMUNAL_HUB_WIDGET_EVENT_REPORTED__ACTION__REMOVE,
+ "com.red.package/my_test_widget",
+ 2,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index f8906ad..61487b0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -16,14 +16,13 @@
package com.android.systemui.communal.view.viewmodel
-import android.appwidget.AppWidgetProviderInfo
import android.content.ActivityNotFoundException
+import android.content.ComponentName
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.content.pm.UserInfo
-import android.os.UserHandle
import android.provider.Settings
import android.widget.RemoteViews
import androidx.activity.result.ActivityResultLauncher
@@ -47,8 +46,8 @@
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.shared.log.CommunalMetricsLogger
import com.android.systemui.communal.shared.log.CommunalUiEvent
-import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.shared.model.EditModeState
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.coroutines.collectLastValue
@@ -86,9 +85,9 @@
class CommunalEditModeViewModelTest : SysuiTestCase() {
@Mock private lateinit var mediaHost: MediaHost
@Mock private lateinit var uiEventLogger: UiEventLogger
- @Mock private lateinit var providerInfo: AppWidgetProviderInfo
@Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
+ @Mock private lateinit var metricsLogger: CommunalMetricsLogger
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
@@ -120,7 +119,6 @@
selectedUserIndex = 0,
)
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
- whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))
underTest =
CommunalEditModeViewModel(
@@ -133,6 +131,7 @@
logcatLogBuffer("CommunalEditModeViewModelTest"),
kosmos.testDispatcher,
kosmos.communalPrefsInteractor,
+ metricsLogger,
)
}
@@ -142,20 +141,8 @@
tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
// Widgets available.
- val widgets =
- listOf(
- CommunalWidgetContentModel.Available(
- appWidgetId = 0,
- priority = 30,
- providerInfo = providerInfo,
- ),
- CommunalWidgetContentModel.Available(
- appWidgetId = 1,
- priority = 20,
- providerInfo = providerInfo,
- ),
- )
- widgetRepository.setCommunalWidgets(widgets)
+ widgetRepository.addWidget(appWidgetId = 0, priority = 30)
+ widgetRepository.addWidget(appWidgetId = 1, priority = 20)
// Smartspace available.
smartspaceRepository.setTimers(
@@ -216,20 +203,8 @@
tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
// Widgets available.
- val widgets =
- listOf(
- CommunalWidgetContentModel.Available(
- appWidgetId = 0,
- priority = 30,
- providerInfo = providerInfo,
- ),
- CommunalWidgetContentModel.Available(
- appWidgetId = 1,
- priority = 20,
- providerInfo = providerInfo,
- ),
- )
- widgetRepository.setCommunalWidgets(widgets)
+ widgetRepository.addWidget(appWidgetId = 0, priority = 30)
+ widgetRepository.addWidget(appWidgetId = 1, priority = 20)
val communalContent by collectLastValue(underTest.communalContent)
@@ -240,14 +215,18 @@
assertThat(communalContent?.get(1))
.isInstanceOf(CommunalContentModel.WidgetContent::class.java)
- underTest.onDeleteWidget(widgets.get(0).appWidgetId)
+ underTest.onDeleteWidget(
+ id = 0,
+ componentName = ComponentName("test_package", "test_class"),
+ priority = 30,
+ )
// Only one widget and CTA tile remain.
assertThat(communalContent?.size).isEqualTo(1)
val item = communalContent?.get(0)
val appWidgetId =
if (item is CommunalContentModel.WidgetContent) item.appWidgetId else null
- assertThat(appWidgetId).isEqualTo(widgets.get(1).appWidgetId)
+ assertThat(appWidgetId).isEqualTo(1)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index c480aa8..d862a21 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -16,9 +16,7 @@
package com.android.systemui.communal.view.viewmodel
-import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
-import android.os.UserHandle
import android.platform.test.flag.junit.FlagsParameterization
import android.provider.Settings
import android.widget.RemoteViews
@@ -44,7 +42,6 @@
import com.android.systemui.communal.domain.interactor.communalTutorialInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS
import com.android.systemui.communal.ui.viewmodel.PopupType
@@ -110,7 +107,6 @@
@RunWith(ParameterizedAndroidJunit4::class)
class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@Mock private lateinit var mediaHost: MediaHost
- @Mock private lateinit var providerInfo: AppWidgetProviderInfo
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
@@ -153,7 +149,6 @@
userInfos = listOf(MAIN_USER_INFO),
selectedUserIndex = 0,
)
- whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))
whenever(mediaHost.visible).thenReturn(true)
kosmos.powerInteractor.setAwakeForTest()
@@ -212,20 +207,8 @@
tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
// Widgets available.
- val widgets =
- listOf(
- CommunalWidgetContentModel.Available(
- appWidgetId = 0,
- priority = 30,
- providerInfo = providerInfo,
- ),
- CommunalWidgetContentModel.Available(
- appWidgetId = 1,
- priority = 20,
- providerInfo = providerInfo,
- ),
- )
- widgetRepository.setCommunalWidgets(widgets)
+ widgetRepository.addWidget(appWidgetId = 0, priority = 30)
+ widgetRepository.addWidget(appWidgetId = 1, priority = 20)
// Smartspace available.
smartspaceRepository.setTimers(
@@ -314,15 +297,7 @@
testScope.runTest {
tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
- widgetRepository.setCommunalWidgets(
- listOf(
- CommunalWidgetContentModel.Available(
- appWidgetId = 1,
- priority = 1,
- providerInfo = providerInfo,
- )
- ),
- )
+ widgetRepository.addWidget(appWidgetId = 1, priority = 1)
mediaRepository.mediaInactive()
smartspaceRepository.setTimers(emptyList())
@@ -676,20 +651,8 @@
)
// Widgets available
- val widgets =
- listOf(
- CommunalWidgetContentModel.Available(
- appWidgetId = 0,
- priority = 30,
- providerInfo = providerInfo,
- ),
- CommunalWidgetContentModel.Available(
- appWidgetId = 1,
- priority = 20,
- providerInfo = providerInfo,
- ),
- )
- widgetRepository.setCommunalWidgets(widgets)
+ widgetRepository.addWidget(appWidgetId = 0, priority = 30)
+ widgetRepository.addWidget(appWidgetId = 1, priority = 20)
// Then hub shows widgets and the CTA tile
assertThat(communalContent).hasSize(3)
@@ -743,20 +706,8 @@
)
// And widgets available
- val widgets =
- listOf(
- CommunalWidgetContentModel.Available(
- appWidgetId = 0,
- priority = 30,
- providerInfo = providerInfo,
- ),
- CommunalWidgetContentModel.Available(
- appWidgetId = 1,
- priority = 20,
- providerInfo = providerInfo,
- ),
- )
- widgetRepository.setCommunalWidgets(widgets)
+ widgetRepository.addWidget(appWidgetId = 0, priority = 30)
+ widgetRepository.addWidget(appWidgetId = 1, priority = 20)
// Then emits widgets and the CTA tile
assertThat(communalContent).hasSize(3)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index 3d2eabf..c9f3f14 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -16,10 +16,7 @@
package com.android.systemui.communal.widgets
-import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
-import android.graphics.Bitmap
-import android.os.UserHandle
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -27,7 +24,6 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
-import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -38,7 +34,6 @@
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
@@ -172,26 +167,23 @@
with(kosmos) {
testScope.runTest {
// Set up communal widgets
- val widget1 =
- mock<CommunalWidgetContentModel.Available> {
- whenever(this.appWidgetId).thenReturn(1)
- }
- val widget2 =
- mock<CommunalWidgetContentModel.Available> {
- whenever(this.appWidgetId).thenReturn(2)
- }
- val widget3 =
- mock<CommunalWidgetContentModel.Available> {
- whenever(this.appWidgetId).thenReturn(3)
- }
- fakeCommunalWidgetRepository.setCommunalWidgets(listOf(widget1, widget2, widget3))
+ fakeCommunalWidgetRepository.addWidget(appWidgetId = 1)
+ fakeCommunalWidgetRepository.addWidget(appWidgetId = 2)
+ fakeCommunalWidgetRepository.addWidget(appWidgetId = 3)
underTest.start()
// Assert communal widgets has 3
val communalWidgets by
collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
- assertThat(communalWidgets).containsExactly(widget1, widget2, widget3)
+ assertThat(communalWidgets).hasSize(3)
+
+ val widget1 = communalWidgets!![0]
+ val widget2 = communalWidgets!![1]
+ val widget3 = communalWidgets!![2]
+ assertThat(widget1.appWidgetId).isEqualTo(1)
+ assertThat(widget2.appWidgetId).isEqualTo(2)
+ assertThat(widget3.appWidgetId).isEqualTo(3)
// Report app widget 1 to remove and assert widget removed
appWidgetIdToRemove.emit(1)
@@ -216,18 +208,26 @@
selectedUserIndex = 0,
)
// One work widget, one pending work widget, and one personal widget.
- val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createPendingWidgetForUser(2, USER_INFO_WORK.id)
- val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
- val widgets = listOf(widget1, widget2, widget3)
- fakeCommunalWidgetRepository.setCommunalWidgets(widgets)
+ fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id)
+ fakeCommunalWidgetRepository.addPendingWidget(
+ appWidgetId = 2,
+ userId = USER_INFO_WORK.id
+ )
+ fakeCommunalWidgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id)
underTest.start()
runCurrent()
val communalWidgets by
collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
- assertThat(communalWidgets).containsExactly(widget1, widget2, widget3)
+ assertThat(communalWidgets).hasSize(3)
+
+ val widget1 = communalWidgets!![0]
+ val widget2 = communalWidgets!![1]
+ val widget3 = communalWidgets!![2]
+ assertThat(widget1.appWidgetId).isEqualTo(1)
+ assertThat(widget2.appWidgetId).isEqualTo(2)
+ assertThat(widget3.appWidgetId).isEqualTo(3)
// Unlock the device and remove work profile.
fakeKeyguardRepository.setKeyguardShowing(false)
@@ -259,32 +259,6 @@
)
}
- private fun createWidgetForUser(
- appWidgetId: Int,
- userId: Int
- ): CommunalWidgetContentModel.Available =
- mock<CommunalWidgetContentModel.Available> {
- whenever(this.appWidgetId).thenReturn(appWidgetId)
- val providerInfo = mock<AppWidgetProviderInfo>()
- whenever(providerInfo.profile).thenReturn(UserHandle(userId))
- whenever(this.providerInfo).thenReturn(providerInfo)
- }
-
- private fun createPendingWidgetForUser(
- appWidgetId: Int,
- userId: Int,
- priority: Int = 0,
- packageName: String = "",
- icon: Bitmap? = null,
- ): CommunalWidgetContentModel.Pending =
- CommunalWidgetContentModel.Pending(
- appWidgetId = appWidgetId,
- priority = priority,
- packageName = packageName,
- icon = icon,
- user = UserHandle(userId),
- )
-
private companion object {
val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
index 693fcda..18839e6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
@@ -22,6 +22,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.classifier.falsingManager
import com.android.systemui.haptics.vibratorHelper
import com.android.systemui.kosmos.testScope
@@ -52,6 +53,7 @@
private val vibratorHelper = kosmos.vibratorHelper
private val qsTile = kosmos.qsTileFactory.createTile("Test Tile")
@Mock private lateinit var callback: QSLongPressEffect.Callback
+ @Mock private lateinit var controller: ActivityTransitionAnimator.Controller
private val effectDuration = 400
private val lowTickDuration = 12
@@ -218,8 +220,9 @@
// GIVEN that the animation completes
longPressEffect.handleAnimationComplete()
- // THEN the effect ends in the idle state.
+ // THEN the effect ends in the idle state and the reversed callback is used.
assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
+ verify(callback, times(1)).onEffectFinishedReversing()
}
@Test
@@ -348,6 +351,23 @@
assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE)
}
+ @Test
+ fun onLongClickTransitionCancelled_whileInLongClickState_reversesEffect() =
+ testWhileInState(QSLongPressEffect.State.LONG_CLICKED) {
+ // GIVEN a transition controller delegate
+ val delegate = longPressEffect.createTransitionControllerDelegate(controller)
+
+ // WHEN the activity launch animation is cancelled
+ val newOccludedState = false
+ delegate.onTransitionAnimationCancelled(newOccludedState)
+
+ // THEN the effect reverses and ends in RUNNING_BACKWARDS_FROM_CANCEL
+ assertThat(longPressEffect.state)
+ .isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL)
+ verify(callback, times(1)).onReverseAnimator(false)
+ verify(controller).onTransitionAnimationCancelled(newOccludedState)
+ }
+
private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) =
with(kosmos) {
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index a3959d2..3fd1c20 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import android.app.StatusBarManager
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
@@ -25,9 +26,13 @@
import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
import com.android.systemui.Flags
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
+import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor
import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.communalSceneTransitionInteractor
import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.flags.BrokenWithSceneContainer
@@ -122,15 +127,22 @@
private val fromGlanceableHubTransitionInteractor by lazy {
kosmos.fromGlanceableHubTransitionInteractor
}
+ private val communalSceneTransitionInteractor by lazy {
+ kosmos.communalSceneTransitionInteractor
+ }
private val powerInteractor by lazy { kosmos.powerInteractor }
private val communalInteractor by lazy { kosmos.communalInteractor }
+ private val communalSceneInteractor by lazy { kosmos.communalSceneInteractor }
companion object {
@JvmStatic
@Parameters(name = "{0}")
fun getParams(): List<FlagsParameterization> {
- return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ return FlagsParameterization.allCombinationsOf(
+ FLAG_COMMUNAL_SCENE_KTF_REFACTOR,
+ )
+ .andSceneContainer()
}
}
@@ -163,6 +175,7 @@
fromOccludedTransitionInteractor.start()
fromAlternateBouncerTransitionInteractor.start()
fromGlanceableHubTransitionInteractor.start()
+ communalSceneTransitionInteractor.start()
}
@Test
@@ -636,6 +649,7 @@
@Test
@DisableSceneContainer
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun dozingToGlanceableHub() =
testScope.runTest {
// GIVEN a prior transition has run to DOZING
@@ -770,6 +784,7 @@
@Test
@BrokenWithSceneContainer(339465026)
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun goneToGlanceableHub() =
testScope.runTest {
// GIVEN a prior transition has run to GONE
@@ -799,6 +814,29 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
+ @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ fun goneToGlanceableHub_communalKtfRefactor() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to GONE
+ runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+
+ // WHEN the glanceable hub is shown
+ communalSceneInteractor.changeScene(CommunalScenes.Communal)
+ runCurrent()
+
+ assertThat(transitionRepository)
+ .startedTransition(
+ to = KeyguardState.GLANCEABLE_HUB,
+ from = KeyguardState.GONE,
+ ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+ animatorAssertion = { it.isNull() }
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
@DisableSceneContainer
fun alternateBouncerToPrimaryBouncer() =
testScope.runTest {
@@ -941,6 +979,11 @@
@Test
fun alternateBouncerToGlanceableHub() =
testScope.runTest {
+ // GIVEN the device is idle on the glanceable hub
+ communalSceneInteractor.changeScene(CommunalScenes.Communal)
+ runCurrent()
+ clearInvocations(transitionRepository)
+
// GIVEN a prior transition has run to ALTERNATE_BOUNCER
bouncerRepository.setAlternateVisible(true)
runTransitionAndSetWakefulness(
@@ -951,19 +994,11 @@
// GIVEN the primary bouncer isn't showing and device not sleeping
bouncerRepository.setPrimaryShow(false)
- // GIVEN the device is idle on the glanceable hub
- val idleTransitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Idle(CommunalScenes.Communal)
- )
- communalInteractor.setTransitionState(idleTransitionState)
- runCurrent()
-
// WHEN the alternateBouncer stops showing
bouncerRepository.setAlternateVisible(false)
advanceTimeBy(200L)
- // THEN a transition to LOCKSCREEN should occur
+ // THEN a transition to GLANCEABLE_HUB should occur
assertThat(transitionRepository)
.startedTransition(
ownerName = FromAlternateBouncerTransitionInteractor::class.simpleName,
@@ -1063,17 +1098,16 @@
@DisableSceneContainer
fun primaryBouncerToGlanceableHub() =
testScope.runTest {
+ // GIVEN the device is idle on the glanceable hub
+ communalSceneInteractor.changeScene(CommunalScenes.Communal)
+ runCurrent()
+
// GIVEN a prior transition has run to PRIMARY_BOUNCER
bouncerRepository.setPrimaryShow(true)
- runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER)
-
- // GIVEN the device is idle on the glanceable hub
- val idleTransitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Idle(CommunalScenes.Communal)
- )
- communalInteractor.setTransitionState(idleTransitionState)
- runCurrent()
+ runTransitionAndSetWakefulness(
+ KeyguardState.GLANCEABLE_HUB,
+ KeyguardState.PRIMARY_BOUNCER
+ )
// WHEN the primaryBouncer stops showing
bouncerRepository.setPrimaryShow(false)
@@ -1095,27 +1129,26 @@
@DisableSceneContainer
fun primaryBouncerToGlanceableHubWhileDreaming() =
testScope.runTest {
+ // GIVEN the device is idle on the glanceable hub
+ communalSceneInteractor.changeScene(CommunalScenes.Communal)
+ runCurrent()
+
// GIVEN a prior transition has run to PRIMARY_BOUNCER
bouncerRepository.setPrimaryShow(true)
- runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER)
+ runTransitionAndSetWakefulness(
+ KeyguardState.GLANCEABLE_HUB,
+ KeyguardState.PRIMARY_BOUNCER
+ )
// GIVEN that we are dreaming and occluded
keyguardRepository.setDreaming(true)
keyguardRepository.setKeyguardOccluded(true)
- // GIVEN the device is idle on the glanceable hub
- val idleTransitionState =
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Idle(CommunalScenes.Communal)
- )
- communalInteractor.setTransitionState(idleTransitionState)
- runCurrent()
-
// WHEN the primaryBouncer stops showing
bouncerRepository.setPrimaryShow(false)
runCurrent()
- // THEN a transition to LOCKSCREEN should occur
+ // THEN a transition to GLANCEABLE_HUB should occur
assertThat(transitionRepository)
.startedTransition(
ownerName = FromPrimaryBouncerTransitionInteractor::class.simpleName,
@@ -1219,6 +1252,7 @@
@Test
@BrokenWithSceneContainer(339465026)
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun occludedToGlanceableHub() =
testScope.runTest {
// GIVEN a device on lockscreen
@@ -1256,6 +1290,7 @@
@Test
@BrokenWithSceneContainer(339465026)
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun occludedToGlanceableHubWhenInitiallyOnHub() =
testScope.runTest {
// GIVEN a device on lockscreen and communal is available
@@ -1293,6 +1328,37 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
+ @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ fun occludedToGlanceableHub_communalKtfRefactor() =
+ testScope.runTest {
+ // GIVEN a device on lockscreen and communal is available
+ keyguardRepository.setKeyguardShowing(true)
+ kosmos.setCommunalAvailable(true)
+ runCurrent()
+
+ // GIVEN a prior transition has run to OCCLUDED from GLANCEABLE_HUB
+ runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.OCCLUDED)
+ keyguardRepository.setKeyguardOccluded(true)
+ runCurrent()
+
+ // WHEN occlusion ends
+ keyguardRepository.setKeyguardOccluded(false)
+ runCurrent()
+
+ // THEN a transition to GLANCEABLE_HUB should occur
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.GLANCEABLE_HUB,
+ animatorAssertion = { it.isNull() },
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
fun occludedToAlternateBouncer() =
testScope.runTest {
// GIVEN a prior transition has run to OCCLUDED
@@ -1511,6 +1577,7 @@
@Test
@DisableSceneContainer
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun dreamingToGlanceableHub() =
testScope.runTest {
// GIVEN a prior transition has run to DREAMING
@@ -1550,6 +1617,47 @@
}
@Test
+ @DisableSceneContainer
+ @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ fun dreamingToGlanceableHub_communalKtfRefactor() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to DREAMING
+ keyguardRepository.setDreaming(true)
+ runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.DREAMING)
+ runCurrent()
+
+ // WHEN a transition to the glanceable hub starts
+ val currentScene = CommunalScenes.Blank
+ val targetScene = CommunalScenes.Communal
+
+ val progress = MutableStateFlow(0f)
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Transition(
+ fromScene = currentScene,
+ toScene = targetScene,
+ currentScene = flowOf(targetScene),
+ progress = progress,
+ isInitiatedByUserInput = false,
+ isUserInputOngoing = flowOf(false),
+ )
+ )
+ communalSceneInteractor.setTransitionState(transitionState)
+ progress.value = .1f
+ runCurrent()
+
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+ from = KeyguardState.DREAMING,
+ to = KeyguardState.GLANCEABLE_HUB,
+ animatorAssertion = { it.isNull() }, // transition should be manually animated
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
@BrokenWithSceneContainer(339465026)
fun lockscreenToOccluded() =
testScope.runTest {
@@ -1679,6 +1787,7 @@
@Test
@DisableSceneContainer
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun lockscreenToGlanceableHub() =
testScope.runTest {
// GIVEN a prior transition has run to LOCKSCREEN
@@ -1737,6 +1846,48 @@
@Test
@DisableSceneContainer
+ @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ fun lockscreenToGlanceableHub_communalKtfRefactor() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to LOCKSCREEN
+ runTransitionAndSetWakefulness(KeyguardState.AOD, KeyguardState.LOCKSCREEN)
+ runCurrent()
+
+ // WHEN a glanceable hub transition starts
+ val currentScene = CommunalScenes.Blank
+ val targetScene = CommunalScenes.Communal
+
+ val progress = MutableStateFlow(0f)
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Transition(
+ fromScene = currentScene,
+ toScene = targetScene,
+ currentScene = flowOf(targetScene),
+ progress = progress,
+ isInitiatedByUserInput = false,
+ isUserInputOngoing = flowOf(false),
+ )
+ )
+ communalSceneInteractor.setTransitionState(transitionState)
+ progress.value = .1f
+ runCurrent()
+
+ // THEN a transition from LOCKSCREEN => GLANCEABLE_HUB should occur
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GLANCEABLE_HUB,
+ animatorAssertion = { it.isNull() }, // transition should be manually animated
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ @DisableSceneContainer
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun glanceableHubToLockscreen() =
testScope.runTest {
// GIVEN a prior transition has run to GLANCEABLE_HUB
@@ -1792,6 +1943,48 @@
@Test
@DisableSceneContainer
+ @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ fun glanceableHubToLockscreen_communalKtfRefactor() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to GLANCEABLE_HUB
+ communalSceneInteractor.changeScene(CommunalScenes.Communal)
+ runCurrent()
+ clearInvocations(transitionRepository)
+
+ // WHEN a transition away from glanceable hub starts
+ val currentScene = CommunalScenes.Communal
+ val targetScene = CommunalScenes.Blank
+
+ val progress = MutableStateFlow(0f)
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Transition(
+ fromScene = currentScene,
+ toScene = targetScene,
+ currentScene = flowOf(targetScene),
+ progress = progress,
+ isInitiatedByUserInput = false,
+ isUserInputOngoing = flowOf(false),
+ )
+ )
+ communalSceneInteractor.setTransitionState(transitionState)
+ progress.value = .1f
+ runCurrent()
+
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ animatorAssertion = { it.isNull() }, // transition should be manually animated
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ @DisableSceneContainer
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun glanceableHubToDozing() =
testScope.runTest {
// GIVEN a prior transition has run to GLANCEABLE_HUB
@@ -1814,6 +2007,31 @@
@Test
@DisableSceneContainer
+ @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ fun glanceableHubToDozing_communalKtfRefactor() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to GLANCEABLE_HUB
+ communalSceneInteractor.changeScene(CommunalScenes.Communal)
+ runCurrent()
+ clearInvocations(transitionRepository)
+
+ // WHEN the device begins to sleep
+ powerInteractor.setAsleepForTest()
+ runCurrent()
+
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.DOZING,
+ animatorAssertion = { it.isNull() },
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ @DisableSceneContainer
fun glanceableHubToPrimaryBouncer() =
testScope.runTest {
// GIVEN a prior transition has run to ALTERNATE_BOUNCER
@@ -1858,6 +2076,7 @@
@Test
@BrokenWithSceneContainer(339465026)
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun glanceableHubToOccluded() =
testScope.runTest {
// GIVEN a prior transition has run to GLANCEABLE_HUB
@@ -1888,7 +2107,33 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
+ @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ fun glanceableHubToOccluded_communalKtfRefactor() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to GLANCEABLE_HUB
+ communalSceneInteractor.changeScene(CommunalScenes.Communal)
+ runCurrent()
+ clearInvocations(transitionRepository)
+
+ // WHEN the keyguard is occluded
+ keyguardRepository.setKeyguardOccluded(true)
+ runCurrent()
+
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OCCLUDED,
+ animatorAssertion = { it.isNull() },
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
@DisableSceneContainer
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun glanceableHubToGone() =
testScope.runTest {
// GIVEN a prior transition has run to GLANCEABLE_HUB
@@ -1911,6 +2156,32 @@
@Test
@DisableSceneContainer
+ @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ fun glanceableHubToGone_communalKtfRefactor() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to GLANCEABLE_HUB
+ communalSceneInteractor.changeScene(CommunalScenes.Communal)
+ runCurrent()
+ clearInvocations(transitionRepository)
+
+ // WHEN keyguard goes away
+ keyguardRepository.setKeyguardGoingAway(true)
+ runCurrent()
+
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.GONE,
+ animatorAssertion = { it.isNull() },
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ @DisableSceneContainer
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun glanceableHubToDreaming() =
testScope.runTest {
// GIVEN that we are dreaming and not dozing
@@ -1939,7 +2210,7 @@
isUserInputOngoing = flowOf(false),
)
)
- communalInteractor.setTransitionState(transitionState)
+ communalSceneInteractor.setTransitionState(transitionState)
runCurrent()
assertThat(transitionRepository)
@@ -1953,6 +2224,54 @@
coroutineContext.cancelChildren()
}
+ @Test
+ @DisableSceneContainer
+ @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ fun glanceableHubToDreaming_communalKtfRefactor() =
+ testScope.runTest {
+ // GIVEN that we are dreaming and not dozing
+ powerInteractor.setAwakeForTest()
+ keyguardRepository.setDreaming(true)
+ keyguardRepository.setDreamingWithOverlay(true)
+ keyguardRepository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ advanceTimeBy(100L)
+
+ // GIVEN a prior transition has run to GLANCEABLE_HUB
+ communalSceneInteractor.changeScene(CommunalScenes.Communal)
+ runCurrent()
+ clearInvocations(transitionRepository)
+
+ // WHEN a transition away from glanceable hub starts
+ val currentScene = CommunalScenes.Communal
+ val targetScene = CommunalScenes.Blank
+
+ val transitionState =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Transition(
+ fromScene = currentScene,
+ toScene = targetScene,
+ currentScene = flowOf(targetScene),
+ progress = flowOf(0f, 0.1f),
+ isInitiatedByUserInput = false,
+ isUserInputOngoing = flowOf(false),
+ )
+ )
+ communalSceneInteractor.setTransitionState(transitionState)
+ runCurrent()
+
+ assertThat(transitionRepository)
+ .startedTransition(
+ ownerName = CommunalSceneTransitionInteractor::class.simpleName,
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.DREAMING,
+ animatorAssertion = { it.isNull() }, // transition should be manually animated
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
private suspend fun TestScope.runTransitionAndSetWakefulness(
from: KeyguardState,
to: KeyguardState
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index e9b2385..6b7712d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -20,6 +20,7 @@
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.TransitionKey
import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.communalSceneKtfRefactor
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
@@ -96,20 +97,22 @@
return
}
- // Handle automatically switching based on keyguard state.
- keyguardTransitionInteractor.startedKeyguardTransitionStep
- .mapLatest(::determineSceneAfterTransition)
- .filterNotNull()
- .onEach { (nextScene, nextTransition) ->
- if (!communalSceneInteractor.isLaunchingWidget.value) {
- // When launching a widget, we don't want to animate the scene change or the
- // Communal Hub will reveal the wallpaper even though it shouldn't. Instead we
- // snap to the new scene as part of the launch animation, once the activity
- // launch is done, so we don't change scene here.
- communalSceneInteractor.changeScene(nextScene, nextTransition)
+ if (!communalSceneKtfRefactor()) {
+ // Handle automatically switching based on keyguard state.
+ keyguardTransitionInteractor.startedKeyguardTransitionStep
+ .mapLatest(::determineSceneAfterTransition)
+ .filterNotNull()
+ .onEach { (nextScene, nextTransition) ->
+ if (!communalSceneInteractor.isLaunchingWidget.value) {
+ // When launching a widget, we don't want to animate the scene change or the
+ // Communal Hub will reveal the wallpaper even though it shouldn't. Instead
+ // we snap to the new scene as part of the launch animation, once the
+ // activity launch is done, so we don't change scene here.
+ communalSceneInteractor.changeScene(nextScene, nextTransition)
+ }
}
- }
- .launchIn(applicationScope)
+ .launchIn(applicationScope)
+ }
// TODO(b/322787129): re-enable once custom animations are in place
// Handle automatically switching to communal when docked.
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index a445335..ba2b7bf 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -28,6 +28,8 @@
import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule
import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule
import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor
+import com.android.systemui.communal.shared.log.CommunalMetricsLogger
+import com.android.systemui.communal.shared.log.CommunalStatsLogProxyImpl
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.util.CommunalColors
import com.android.systemui.communal.util.CommunalColorsImpl
@@ -44,6 +46,7 @@
import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
+import javax.inject.Named
import kotlinx.coroutines.CoroutineScope
@Module(
@@ -74,6 +77,11 @@
@Binds fun bindCommunalColors(impl: CommunalColorsImpl): CommunalColors
@Binds
+ fun bindCommunalStatsLogProxy(
+ impl: CommunalStatsLogProxyImpl
+ ): CommunalMetricsLogger.StatsLogProxy
+
+ @Binds
@IntoMap
@ClassKey(CommunalSceneTransitionInteractor::class)
abstract fun bindCommunalSceneTransitionInteractor(
@@ -81,6 +89,8 @@
): CoreStartable
companion object {
+ const val LOGGABLE_PREFIXES = "loggable_prefixes"
+
@Provides
@Communal
@SysUISingleton
@@ -107,5 +117,14 @@
): CommunalBackupUtils {
return CommunalBackupUtils(context)
}
+
+ /** The prefixes of widgets packages names that are considered loggable. */
+ @Provides
+ @Named(LOGGABLE_PREFIXES)
+ fun provideLoggablePrefixes(@Application context: Context): List<String> {
+ return context.resources
+ .getStringArray(com.android.internal.R.array.config_loggable_dream_prefixes)
+ .toList()
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index e65e5e5..ad0bfc7 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -398,16 +398,13 @@
)
}
- val session =
- installSessions.firstOrNull {
- it.packageName ==
- ComponentName.unflattenFromString(entry.componentName)?.packageName
- }
- return if (session != null) {
+ val componentName = ComponentName.unflattenFromString(entry.componentName)
+ val session = installSessions.firstOrNull { it.packageName == componentName?.packageName }
+ return if (componentName != null && session != null) {
CommunalWidgetContentModel.Pending(
appWidgetId = entry.appWidgetId,
priority = entry.priority,
- packageName = session.packageName,
+ componentName = componentName,
icon = session.icon,
user = session.user,
)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index dbddc23..6aaaf3d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -433,6 +433,7 @@
is CommunalWidgetContentModel.Available -> {
WidgetContent.Widget(
appWidgetId = widget.appWidgetId,
+ priority = widget.priority,
providerInfo = widget.providerInfo,
appWidgetHost = appWidgetHost,
inQuietMode = isQuietModeEnabled(widget.providerInfo.profile)
@@ -441,7 +442,8 @@
is CommunalWidgetContentModel.Pending -> {
WidgetContent.PendingWidget(
appWidgetId = widget.appWidgetId,
- packageName = widget.packageName,
+ priority = widget.priority,
+ componentName = widget.componentName,
icon = widget.icon,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
index aa9cbd0..e45a695 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
@@ -28,6 +28,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -52,7 +53,7 @@
@Application private val applicationScope: CoroutineScope,
private val communalSceneRepository: CommunalSceneRepository,
) {
- val _isLaunchingWidget = MutableStateFlow(false)
+ private val _isLaunchingWidget = MutableStateFlow(false)
/** Whether a widget launch is currently in progress. */
val isLaunchingWidget: StateFlow<Boolean> = _isLaunchingWidget.asStateFlow()
@@ -184,6 +185,10 @@
initialValue = false,
)
+ /** This flow will be true when idle on the hub and not transitioning to edit mode. */
+ val isIdleOnCommunalNotEditMode: Flow<Boolean> =
+ allOf(isIdleOnCommunal, editModeState.map { it == null })
+
/**
* Flow that emits a boolean if any portion of the communal UI is visible at all.
*
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
index 8351566..6a20610 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
@@ -85,16 +85,16 @@
*/
private val nextKeyguardStateInternal =
combine(
- keyguardInteractor.isDreaming,
+ keyguardInteractor.isAbleToDream,
keyguardInteractor.isKeyguardOccluded,
keyguardInteractor.isKeyguardGoingAway,
) { dreaming, occluded, keyguardGoingAway ->
if (keyguardGoingAway) {
KeyguardState.GONE
+ } else if (occluded && !dreaming) {
+ KeyguardState.OCCLUDED
} else if (dreaming) {
KeyguardState.DREAMING
- } else if (occluded) {
- KeyguardState.OCCLUDED
} else {
KeyguardState.LOCKSCREEN
}
@@ -162,10 +162,13 @@
// We may receive an Idle event without a corresponding Transition
// event, such as when snapping to a scene without an animation.
val targetState =
- if (idle.currentScene == CommunalScenes.Blank) {
+ if (idle.currentScene == CommunalScenes.Communal) {
+ KeyguardState.GLANCEABLE_HUB
+ } else if (currentToState == KeyguardState.GLANCEABLE_HUB) {
nextKeyguardState.value
} else {
- KeyguardState.GLANCEABLE_HUB
+ // Do nothing as we are no longer in the hub state.
+ return
}
transitionKtfTo(targetState)
repository.nextLockscreenTargetState.value = null
@@ -188,7 +191,7 @@
from = internalTransitionInteractor.currentTransitionInfoInternal.value.to,
to = state,
animator = null,
- modeOnCanceled = TransitionModeOnCanceled.REVERSE
+ modeOnCanceled = TransitionModeOnCanceled.REVERSE,
)
currentTransitionId = internalTransitionInteractor.startTransition(newTransition)
internalTransitionInteractor.updateTransition(
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
index 122240d..73c6ce3 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
@@ -18,6 +18,7 @@
import android.appwidget.AppWidgetProviderInfo
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
+import android.content.ComponentName
import android.content.pm.ApplicationInfo
import android.graphics.Bitmap
import android.widget.RemoteViews
@@ -46,14 +47,18 @@
sealed interface WidgetContent : CommunalContentModel {
val appWidgetId: Int
+ val priority: Int
+ val componentName: ComponentName
data class Widget(
override val appWidgetId: Int,
+ override val priority: Int,
val providerInfo: AppWidgetProviderInfo,
val appWidgetHost: CommunalAppWidgetHost,
val inQuietMode: Boolean,
) : WidgetContent {
override val key = KEY.widget(appWidgetId)
+ override val componentName: ComponentName = providerInfo.provider
// Widget size is always half.
override val size = CommunalContentSize.HALF
@@ -66,9 +71,11 @@
data class DisabledWidget(
override val appWidgetId: Int,
+ override val priority: Int,
val providerInfo: AppWidgetProviderInfo
) : WidgetContent {
override val key = KEY.disabledWidget(appWidgetId)
+ override val componentName: ComponentName = providerInfo.provider
// Widget size is always half.
override val size = CommunalContentSize.HALF
@@ -78,7 +85,8 @@
data class PendingWidget(
override val appWidgetId: Int,
- val packageName: String,
+ override val priority: Int,
+ override val componentName: ComponentName,
val icon: Bitmap? = null,
) : WidgetContent {
override val key = KEY.pendingWidget(appWidgetId)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalMetricsLogger.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalMetricsLogger.kt
new file mode 100644
index 0000000..12099f7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalMetricsLogger.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.shared.log
+
+import com.android.systemui.communal.dagger.CommunalModule.Companion.LOGGABLE_PREFIXES
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shared.system.SysUiStatsLog
+import javax.inject.Inject
+import javax.inject.Named
+
+@SysUISingleton
+class CommunalMetricsLogger
+@Inject
+constructor(
+ @Named(LOGGABLE_PREFIXES) private val loggablePrefixes: List<String>,
+ private val statsLogProxy: StatsLogProxy,
+) {
+ /** Logs an add widget event for metrics. No-op if widget is not loggable. */
+ fun logAddWidget(componentName: String, rank: Int) {
+ if (!componentName.isLoggable()) {
+ return
+ }
+
+ statsLogProxy.writeCommunalHubWidgetEventReported(
+ SysUiStatsLog.COMMUNAL_HUB_WIDGET_EVENT_REPORTED__ACTION__ADD,
+ componentName,
+ rank,
+ )
+ }
+
+ /** Logs a remove widget event for metrics. No-op if widget is not loggable. */
+ fun logRemoveWidget(componentName: String, rank: Int) {
+ if (!componentName.isLoggable()) {
+ return
+ }
+
+ statsLogProxy.writeCommunalHubWidgetEventReported(
+ SysUiStatsLog.COMMUNAL_HUB_WIDGET_EVENT_REPORTED__ACTION__REMOVE,
+ componentName,
+ rank,
+ )
+ }
+
+ /** Whether the component name matches any of the loggable prefixes. */
+ private fun String.isLoggable(): Boolean {
+ return loggablePrefixes.any { loggablePrefix -> startsWith(loggablePrefix) }
+ }
+
+ /** Proxy of [SysUiStatsLog] for testing purpose. */
+ interface StatsLogProxy {
+ /** Logs a [SysUiStatsLog.COMMUNAL_HUB_WIDGET_EVENT_REPORTED] stats event. */
+ fun writeCommunalHubWidgetEventReported(
+ action: Int,
+ componentName: String,
+ rank: Int,
+ )
+ }
+}
+
+/** Redirects calls to [SysUiStatsLog]. */
+@SysUISingleton
+class CommunalStatsLogProxyImpl @Inject constructor() : CommunalMetricsLogger.StatsLogProxy {
+ override fun writeCommunalHubWidgetEventReported(
+ action: Int,
+ componentName: String,
+ rank: Int,
+ ) {
+ SysUiStatsLog.write(
+ SysUiStatsLog.COMMUNAL_HUB_WIDGET_EVENT_REPORTED,
+ action,
+ componentName,
+ rank,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt
index b64c195..4ab56cc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalUiEvent.kt
@@ -53,7 +53,15 @@
@UiEvent(doc = "User performs a swipe up gesture from bottom to enter bouncer")
COMMUNAL_HUB_SWIPE_UP_TO_BOUNCER(1573),
@UiEvent(doc = "User performs a swipe down gesture from top to enter shade")
- COMMUNAL_HUB_SWIPE_DOWN_TO_SHADE(1574);
+ COMMUNAL_HUB_SWIPE_DOWN_TO_SHADE(1574),
+ @UiEvent(doc = "User performs a tap gesture on the UMO in Communal Hub")
+ COMMUNAL_HUB_UMO_TAP(1858),
+ @UiEvent(
+ doc =
+ "A transition from dream to Communal Hub starts. This can be triggered by a tap on " +
+ "the dream."
+ )
+ FROM_DREAM_TO_COMMUNAL_HUB_TRANSITION_START(1859);
override fun getId(): Int {
return id
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
index 53aecc1..7cddb72 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.communal.shared.model
import android.appwidget.AppWidgetProviderInfo
+import android.content.ComponentName
import android.graphics.Bitmap
import android.os.UserHandle
@@ -36,7 +37,7 @@
data class Pending(
override val appWidgetId: Int,
override val priority: Int,
- val packageName: String,
+ val componentName: ComponentName,
val icon: Bitmap?,
val user: UserHandle,
) : CommunalWidgetContentModel
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 01ed2b7..623e702 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -99,18 +99,6 @@
communalSceneInteractor.setTransitionState(transitionState)
}
- /**
- * Called when a widget is added via drag and drop from the widget picker into the communal hub.
- */
- open fun onAddWidget(
- componentName: ComponentName,
- user: UserHandle,
- priority: Int,
- configurator: WidgetConfigurator? = null
- ) {
- communalInteractor.addWidget(componentName, user, priority, configurator)
- }
-
open fun onOpenEnableWidgetDialog() {}
open fun onOpenEnableWorkProfileDialog() {}
@@ -136,8 +124,20 @@
/** Called as the UI request to dismiss the any displaying popup */
open fun onHidePopup() {}
+ /** Called as the UI requests adding a widget. */
+ open fun onAddWidget(
+ componentName: ComponentName,
+ user: UserHandle,
+ priority: Int,
+ configurator: WidgetConfigurator? = null,
+ ) {}
+
/** Called as the UI requests deleting a widget. */
- open fun onDeleteWidget(id: Int) {}
+ open fun onDeleteWidget(
+ id: Int,
+ componentName: ComponentName,
+ priority: Int,
+ ) {}
/**
* Called as the UI requests reordering widgets.
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 830f543..5b825d8 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -18,9 +18,11 @@
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
+import android.content.ComponentName
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Resources
+import android.os.UserHandle
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import com.android.internal.logging.UiEventLogger
@@ -30,8 +32,10 @@
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.shared.log.CommunalMetricsLogger
import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.communal.shared.model.EditModeState
+import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -71,6 +75,7 @@
@CommunalLog logBuffer: LogBuffer,
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val communalPrefsInteractor: CommunalPrefsInteractor,
+ private val metricsLogger: CommunalMetricsLogger,
) : BaseCommunalViewModel(communalSceneInteractor, communalInteractor, mediaHost) {
private val logger = Logger(logBuffer, "CommunalEditModeViewModel")
@@ -112,7 +117,24 @@
override val reorderingWidgets: StateFlow<Boolean>
get() = _reorderingWidgets
- override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
+ override fun onAddWidget(
+ componentName: ComponentName,
+ user: UserHandle,
+ priority: Int,
+ configurator: WidgetConfigurator?
+ ) {
+ communalInteractor.addWidget(componentName, user, priority, configurator)
+ metricsLogger.logAddWidget(componentName.flattenToString(), priority)
+ }
+
+ override fun onDeleteWidget(
+ id: Int,
+ componentName: ComponentName,
+ priority: Int,
+ ) {
+ communalInteractor.deleteWidget(id)
+ metricsLogger.logRemoveWidget(componentName.flattenToString(), priority)
+ }
override fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) =
communalInteractor.updateWidgetOrder(widgetIdToPriorityMap)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index 08fe42e..3985769 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -41,6 +41,7 @@
import com.android.systemui.communal.ui.compose.CommunalHub
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtraFromIntent
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
@@ -146,7 +147,8 @@
communalViewModel.canShowEditMode.collect {
communalViewModel.changeScene(
CommunalScenes.Blank,
- CommunalTransitionKeys.ToEditMode
+ CommunalTransitionKeys.ToEditMode,
+ KeyguardState.GONE,
)
// wait till transitioned to Blank scene, then animate in communal content in
// edit mode
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
index c44eb47..491c73d 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
@@ -16,9 +16,15 @@
package com.android.systemui.haptics.qs
+import android.content.ComponentName
import android.os.VibrationEffect
import android.service.quicksettings.Tile
+import android.view.View
import androidx.annotation.VisibleForTesting
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.DelegateTransitionAnimatorController
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.qs.QSTile
@@ -58,6 +64,7 @@
/** The [QSTile] and [Expandable] used to perform a long-click and click actions */
var qsTile: QSTile? = null
var expandable: Expandable? = null
+ private set
/** Haptic effects */
private val durations =
@@ -125,8 +132,10 @@
}
fun handleAnimationStart() {
- vibrate(longPressHint)
- setState(State.RUNNING_FORWARD)
+ if (state == State.TIMEOUT_WAIT) {
+ vibrate(longPressHint)
+ setState(State.RUNNING_FORWARD)
+ }
}
/** This function is called both when an animator completes or gets cancelled */
@@ -147,7 +156,10 @@
setState(getStateForClick())
qsTile?.click(expandable)
}
- State.RUNNING_BACKWARDS_FROM_CANCEL -> setState(State.IDLE)
+ State.RUNNING_BACKWARDS_FROM_CANCEL -> {
+ callback?.onEffectFinishedReversing()
+ setState(State.IDLE)
+ }
else -> {}
}
}
@@ -222,13 +234,58 @@
fun resetState() = setState(State.IDLE)
+ fun createExpandableFromView(view: View) {
+ expandable =
+ object : Expandable {
+ override fun activityTransitionController(
+ launchCujType: Int?,
+ cookie: ActivityTransitionAnimator.TransitionCookie?,
+ component: ComponentName?,
+ returnCujType: Int?,
+ ): ActivityTransitionAnimator.Controller? {
+ val delegatedController =
+ ActivityTransitionAnimator.Controller.fromView(
+ view,
+ launchCujType,
+ cookie,
+ component,
+ returnCujType,
+ )
+ return delegatedController?.let { createTransitionControllerDelegate(it) }
+ }
+
+ override fun dialogTransitionController(
+ cuj: DialogCuj?,
+ ): DialogTransitionAnimator.Controller? =
+ DialogTransitionAnimator.Controller.fromView(view, cuj)
+ }
+ }
+
+ @VisibleForTesting
+ fun createTransitionControllerDelegate(
+ controller: ActivityTransitionAnimator.Controller
+ ): DelegateTransitionAnimatorController {
+ val delegated =
+ object : DelegateTransitionAnimatorController(controller) {
+ override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+ if (state == State.LONG_CLICKED) {
+ setState(State.RUNNING_BACKWARDS_FROM_CANCEL)
+ callback?.onReverseAnimator(false)
+ }
+ delegate.onTransitionAnimationCancelled(newKeyguardOccludedState)
+ }
+ }
+ return delegated
+ }
+
enum class State {
IDLE, /* The effect is idle waiting for touch input */
TIMEOUT_WAIT, /* The effect is waiting for a tap timeout period */
RUNNING_FORWARD, /* The effect is running normally */
/* The effect was interrupted by an ACTION_UP and is now running backwards */
RUNNING_BACKWARDS_FROM_UP,
- /* The effect was interrupted by an ACTION_CANCEL and is now running backwards */
+ /* The effect was cancelled by an ACTION_CANCEL or a shade collapse and is now running
+ backwards */
RUNNING_BACKWARDS_FROM_CANCEL,
CLICKED, /* The effect has ended with a click */
LONG_CLICKED, /* The effect has ended with a long-click */
@@ -247,7 +304,7 @@
fun onStartAnimator()
/** Reverse the effect animator */
- fun onReverseAnimator()
+ fun onReverseAnimator(playHaptics: Boolean = true)
/** Cancel the effect animator */
fun onCancelAnimator()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 59ec87a..e5ccc4a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -119,7 +119,9 @@
// needed. Also, don't react to wake and unlock events, as we'll be
// receiving a call to #dismissAod() shortly when the authentication
// completes.
- !maybeStartTransitionToOccludedOrInsecureCamera() &&
+ !maybeStartTransitionToOccludedOrInsecureCamera { state, reason ->
+ startTransitionTo(state, ownerReason = reason)
+ } &&
!isWakeAndUnlock(biometricUnlockState.mode) &&
!primaryBouncerShowing
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 8f50b03..8ef138e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -20,8 +20,11 @@
import android.annotation.SuppressLint
import android.app.DreamManager
import com.android.app.animation.Interpolators
+import com.android.systemui.Flags.communalSceneKtfRefactor
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.communal.shared.model.CommunalTransitionKeys
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -166,7 +169,7 @@
}
} else if (occluded) {
startTransitionTo(KeyguardState.OCCLUDED)
- } else if (isIdleOnCommunal) {
+ } else if (isIdleOnCommunal && !communalSceneKtfRefactor()) {
if (SceneContainerFlag.isEnabled) {
// TODO(b/336576536): Check if adaptation for scene framework is needed
} else {
@@ -183,7 +186,7 @@
if (SceneContainerFlag.isEnabled) {
// TODO(b/336576536): Check if adaptation for scene framework is needed
} else {
- startTransitionTo(KeyguardState.GLANCEABLE_HUB)
+ transitionToGlanceableHub()
}
} else {
startTransitionTo(KeyguardState.LOCKSCREEN)
@@ -218,7 +221,9 @@
canWakeDirectlyToGone,
primaryBouncerShowing) ->
if (
- !maybeStartTransitionToOccludedOrInsecureCamera() &&
+ !maybeStartTransitionToOccludedOrInsecureCamera { state, reason ->
+ startTransitionTo(state, ownerReason = reason)
+ } &&
// Handled by dismissFromDozing().
!isWakeAndUnlock(biometricUnlockState.mode)
) {
@@ -242,7 +247,7 @@
ownerReason = "waking from dozing"
)
}
- } else if (isIdleOnCommunal) {
+ } else if (isIdleOnCommunal && !communalSceneKtfRefactor()) {
if (SceneContainerFlag.isEnabled) {
// TODO(b/336576536): Check if adaptation for scene framework is
// needed
@@ -264,10 +269,7 @@
// TODO(b/336576536): Check if adaptation for scene framework is
// needed
} else {
- startTransitionTo(
- KeyguardState.GLANCEABLE_HUB,
- ownerReason = "waking from dozing"
- )
+ transitionToGlanceableHub()
}
} else {
startTransitionTo(
@@ -280,6 +282,18 @@
}
}
+ private suspend fun transitionToGlanceableHub() {
+ if (communalSceneKtfRefactor()) {
+ communalSceneInteractor.changeScene(
+ CommunalScenes.Communal,
+ // Immediately show the hub when transitioning from dozing to hub.
+ CommunalTransitionKeys.Immediately,
+ )
+ } else {
+ startTransitionTo(KeyguardState.GLANCEABLE_HUB)
+ }
+ }
+
/** Dismisses keyguard from the DOZING state. */
fun dismissFromDozing() {
scope.launch { startTransitionTo(KeyguardState.GONE) }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 453401d..4c3a75e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -19,6 +19,7 @@
import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launch
+import com.android.systemui.Flags.communalSceneKtfRefactor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -81,7 +82,9 @@
listenForDreamingToLockscreenOrGone()
listenForDreamingToAodOrDozing()
listenForTransitionToCamera(scope, keyguardInteractor)
- listenForDreamingToGlanceableHub()
+ if (!communalSceneKtfRefactor()) {
+ listenForDreamingToGlanceableHub()
+ }
listenForDreamingToPrimaryBouncer()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 1a7012a..6b1be93c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -19,7 +19,12 @@
import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launch
+import com.android.systemui.Flags.communalSceneKtfRefactor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.communal.shared.model.CommunalTransitionKeys
+import com.android.systemui.communal.shared.model.EditModeState
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -30,7 +35,9 @@
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.BooleanFlowOperators.noneOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
+import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@@ -50,6 +57,7 @@
private val glanceableHubTransitions: GlanceableHubTransitions,
private val communalSettingsInteractor: CommunalSettingsInteractor,
keyguardInteractor: KeyguardInteractor,
+ private val communalSceneInteractor: CommunalSceneInteractor,
override val transitionRepository: KeyguardTransitionRepository,
override val internalTransitionInteractor: InternalKeyguardTransitionInteractor,
transitionInteractor: KeyguardTransitionInteractor,
@@ -72,7 +80,9 @@
if (!communalSettingsInteractor.isCommunalFlagEnabled()) {
return
}
- listenForHubToLockscreenOrDreaming()
+ if (!communalSceneKtfRefactor()) {
+ listenForHubToLockscreenOrDreaming()
+ }
listenForHubToDozing()
listenForHubToPrimaryBouncer()
listenForHubToAlternateBouncer()
@@ -120,7 +130,10 @@
scope.launch("$TAG#listenForHubToPrimaryBouncer") {
keyguardInteractor.primaryBouncerShowing
.filterRelevantKeyguardStateAnd { primaryBouncerShowing -> primaryBouncerShowing }
- .collect { startTransitionTo(KeyguardState.PRIMARY_BOUNCER) }
+ .collect {
+ // Bouncer shows on top of the hub, so do not change scenes here.
+ startTransitionTo(KeyguardState.PRIMARY_BOUNCER)
+ }
}
}
@@ -130,7 +143,10 @@
.filterRelevantKeyguardStateAnd { alternateBouncerShowing ->
alternateBouncerShowing
}
- .collect { pair -> startTransitionTo(KeyguardState.ALTERNATE_BOUNCER) }
+ .collect { pair ->
+ // Bouncer shows on top of the hub, so do not change scenes here.
+ startTransitionTo(KeyguardState.ALTERNATE_BOUNCER)
+ }
}
}
@@ -139,10 +155,18 @@
powerInteractor.isAsleep
.filterRelevantKeyguardStateAnd { isAsleep -> isAsleep }
.collect {
- startTransitionTo(
- toState = KeyguardState.DOZING,
- modeOnCanceled = TransitionModeOnCanceled.LAST_VALUE,
- )
+ if (communalSceneKtfRefactor()) {
+ communalSceneInteractor.changeScene(
+ newScene = CommunalScenes.Blank,
+ transitionKey = CommunalTransitionKeys.Immediately,
+ keyguardState = KeyguardState.DOZING,
+ )
+ } else {
+ startTransitionTo(
+ toState = KeyguardState.DOZING,
+ modeOnCanceled = TransitionModeOnCanceled.LAST_VALUE,
+ )
+ }
}
}
}
@@ -152,7 +176,44 @@
scope.launch {
keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop
.filterRelevantKeyguardStateAnd { onTop -> onTop }
- .collect { maybeStartTransitionToOccludedOrInsecureCamera() }
+ .collect {
+ maybeStartTransitionToOccludedOrInsecureCamera { state, reason ->
+ if (communalSceneKtfRefactor()) {
+ communalSceneInteractor.changeScene(
+ newScene = CommunalScenes.Blank,
+ transitionKey = CommunalTransitionKeys.SimpleFade,
+ keyguardState = state,
+ )
+ null
+ } else {
+ startTransitionTo(state, ownerReason = reason)
+ }
+ }
+ }
+ }
+ } else if (communalSceneKtfRefactor()) {
+ scope.launch {
+ allOf(
+ keyguardInteractor.isKeyguardOccluded,
+ noneOf(
+ // Dream is a special-case of occluded, so filter out the dreaming
+ // case here.
+ keyguardInteractor.isDreaming,
+ // When launching activities from widgets on the hub, we have a
+ // custom occlusion animation.
+ communalSceneInteractor.isLaunchingWidget,
+ ),
+ )
+ .filterRelevantKeyguardStateAnd { isOccludedAndNotDreamingNorLaunchingWidget ->
+ isOccludedAndNotDreamingNorLaunchingWidget
+ }
+ .collect { _ ->
+ communalSceneInteractor.changeScene(
+ newScene = CommunalScenes.Blank,
+ transitionKey = CommunalTransitionKeys.SimpleFade,
+ keyguardState = KeyguardState.OCCLUDED,
+ )
+ }
}
} else {
scope.launch {
@@ -160,9 +221,7 @@
.filterRelevantKeyguardStateAnd { isOccludedAndNotDreaming ->
isOccludedAndNotDreaming
}
- .collect { isOccludedAndNotDreaming ->
- startTransitionTo(KeyguardState.OCCLUDED)
- }
+ .collect { _ -> startTransitionTo(KeyguardState.OCCLUDED) }
}
}
}
@@ -170,10 +229,42 @@
private fun listenForHubToGone() {
// TODO(b/336576536): Check if adaptation for scene framework is needed
if (SceneContainerFlag.isEnabled) return
- scope.launch {
- keyguardInteractor.isKeyguardGoingAway
- .filterRelevantKeyguardStateAnd { isKeyguardGoingAway -> isKeyguardGoingAway }
- .collect { startTransitionTo(KeyguardState.GONE) }
+ if (communalSceneKtfRefactor()) {
+ scope.launch {
+ allOf(
+ keyguardInteractor.isKeyguardGoingAway,
+ // TODO(b/327225415): Handle edit mode opening here to avoid going to GONE
+ // state until after edit mode is ready to be shown.
+ noneOf(
+ // When launching activities from widgets on the hub, we wait to change
+ // scenes until the activity launch is complete.
+ communalSceneInteractor.isLaunchingWidget,
+ ),
+ )
+ .filterRelevantKeyguardStateAnd { isKeyguardGoingAway -> isKeyguardGoingAway }
+ .sample(communalSceneInteractor.editModeState, ::Pair)
+ .collect { (_, editModeState) ->
+ if (
+ editModeState == EditModeState.STARTING ||
+ editModeState == EditModeState.SHOWING
+ ) {
+ // Don't change scenes here as that is handled by the edit activity.
+ startTransitionTo(KeyguardState.GONE)
+ } else {
+ communalSceneInteractor.changeScene(
+ newScene = CommunalScenes.Blank,
+ transitionKey = CommunalTransitionKeys.SimpleFade,
+ keyguardState = KeyguardState.GONE
+ )
+ }
+ }
+ }
+ } else {
+ scope.launch {
+ keyguardInteractor.isKeyguardGoingAway
+ .filterRelevantKeyguardStateAnd { isKeyguardGoingAway -> isKeyguardGoingAway }
+ .collect { startTransitionTo(KeyguardState.GONE) }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index b084824..ef76f38 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -19,7 +19,7 @@
import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launch
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -52,7 +52,7 @@
@Main mainDispatcher: CoroutineDispatcher,
keyguardInteractor: KeyguardInteractor,
powerInteractor: PowerInteractor,
- private val communalInteractor: CommunalInteractor,
+ private val communalSceneInteractor: CommunalSceneInteractor,
keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
private val biometricSettingsRepository: BiometricSettingsRepository,
private val keyguardRepository: KeyguardRepository,
@@ -88,7 +88,7 @@
biometricSettingsRepository.isCurrentUserInLockdown
.distinctUntilChanged()
.filterRelevantKeyguardStateAnd { inLockdown -> inLockdown }
- .sample(communalInteractor.isIdleOnCommunal, ::Pair)
+ .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair)
.collect { (_, isIdleOnCommunal) ->
val to =
if (isIdleOnCommunal) {
@@ -120,7 +120,7 @@
scope.launch("$TAG#listenForGoneToLockscreenOrHub") {
keyguardInteractor.isKeyguardShowing
.filterRelevantKeyguardStateAnd { isKeyguardShowing -> isKeyguardShowing }
- .sample(communalInteractor.isIdleOnCommunal, ::Pair)
+ .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair)
.collect { (_, isIdleOnCommunal) ->
val to =
if (isIdleOnCommunal) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 5c7adf0..16c014f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -20,6 +20,7 @@
import android.util.MathUtils
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launch
+import com.android.systemui.Flags.communalSceneKtfRefactor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -90,7 +91,9 @@
listenForLockscreenToPrimaryBouncerDragging()
listenForLockscreenToAlternateBouncer()
listenForLockscreenTransitionToCamera()
- listenForLockscreenToGlanceableHub()
+ if (!communalSceneKtfRefactor()) {
+ listenForLockscreenToGlanceableHub()
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index f3ca9df..2f32040 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -18,8 +18,12 @@
import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
+import com.android.systemui.Flags.communalSceneKtfRefactor
import com.android.systemui.Flags.restartDreamOnUnocclude
import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.communal.shared.model.CommunalTransitionKeys
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -49,6 +53,7 @@
keyguardInteractor: KeyguardInteractor,
powerInteractor: PowerInteractor,
private val communalInteractor: CommunalInteractor,
+ private val communalSceneInteractor: CommunalSceneInteractor,
keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
) :
TransitionInteractor(
@@ -140,7 +145,14 @@
} else if (isIdleOnCommunal || showCommunalFromOccluded) {
// TODO(b/336576536): Check if adaptation for scene framework is needed
if (SceneContainerFlag.isEnabled) return
- startTransitionTo(KeyguardState.GLANCEABLE_HUB)
+ if (communalSceneKtfRefactor()) {
+ communalSceneInteractor.changeScene(
+ CommunalScenes.Communal,
+ CommunalTransitionKeys.SimpleFade
+ )
+ } else {
+ startTransitionTo(KeyguardState.GLANCEABLE_HUB)
+ }
} else {
startTransitionTo(KeyguardState.LOCKSCREEN)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index 2429088..9adcaa2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -18,7 +18,9 @@
import android.animation.ValueAnimator
import com.android.keyguard.KeyguardSecurityModel
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.Flags.communalSceneKtfRefactor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -55,7 +57,7 @@
@Background bgDispatcher: CoroutineDispatcher,
@Main mainDispatcher: CoroutineDispatcher,
keyguardInteractor: KeyguardInteractor,
- private val communalInteractor: CommunalInteractor,
+ private val communalSceneInteractor: CommunalSceneInteractor,
private val keyguardSecurityModel: KeyguardSecurityModel,
private val selectedUserInteractor: SelectedUserInteractor,
powerInteractor: PowerInteractor,
@@ -94,7 +96,10 @@
.distinctUntilChanged()
fun dismissPrimaryBouncer() {
- scope.launch { startTransitionTo(KeyguardState.GONE) }
+ scope.launch {
+ startTransitionTo(KeyguardState.GONE)
+ closeHubImmediatelyIfNeeded()
+ }
}
private fun listenForPrimaryBouncerToLockscreenHubOrOccluded() {
@@ -106,17 +111,16 @@
.sample(
powerInteractor.isAwake,
keyguardInteractor.isActiveDreamLockscreenHosted,
- communalInteractor.isIdleOnCommunal
+ communalSceneInteractor.isIdleOnCommunal
)
.filterRelevantKeyguardState()
.collect {
(isBouncerShowing, isAwake, isActiveDreamLockscreenHosted, isIdleOnCommunal)
->
if (
- !maybeStartTransitionToOccludedOrInsecureCamera() &&
- !isBouncerShowing &&
- isAwake &&
- !isActiveDreamLockscreenHosted
+ !maybeStartTransitionToOccludedOrInsecureCamera { state, reason ->
+ startTransitionTo(state, ownerReason = reason)
+ } && !isBouncerShowing && isAwake && !isActiveDreamLockscreenHosted
) {
val toState =
if (isIdleOnCommunal) {
@@ -136,7 +140,7 @@
keyguardInteractor.isKeyguardOccluded,
keyguardInteractor.isDreaming,
keyguardInteractor.isActiveDreamLockscreenHosted,
- communalInteractor.isIdleOnCommunal,
+ communalSceneInteractor.isIdleOnCommunal,
)
.filterRelevantKeyguardStateAnd {
(isBouncerShowing, isAwake, _, _, isActiveDreamLockscreenHosted, _) ->
@@ -159,6 +163,19 @@
}
}
+ private fun closeHubImmediatelyIfNeeded() {
+ // If the hub is showing, and we are not animating a widget launch nor transitioning to
+ // edit mode, then close the hub immediately.
+ if (
+ communalSceneKtfRefactor() &&
+ communalSceneInteractor.isIdleOnCommunal.value &&
+ !communalSceneInteractor.isLaunchingWidget.value &&
+ communalSceneInteractor.editModeState.value == null
+ ) {
+ communalSceneInteractor.snapToScene(CommunalScenes.Blank)
+ }
+ }
+
private fun listenForPrimaryBouncerToAsleep() {
// TODO(b/336576536): Check if adaptation for scene framework is needed
if (SceneContainerFlag.isEnabled) return
@@ -213,6 +230,7 @@
},
modeOnCanceled = TransitionModeOnCanceled.RESET,
)
+ closeHubImmediatelyIfNeeded()
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index 89c7178..d06ee64 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -122,9 +122,14 @@
* SHOW_WHEN_LOCKED activity, or back to [KeyguardState.GONE], for some power button launch
* gesture cases. If so, start the transition.
*
+ * @param startTransition A callback which is triggered to start the transition to the desired
+ * KeyguardState. Allows caller to hook into the transition start if needed.
+ *
* Returns true if a transition was started, false otherwise.
*/
- suspend fun maybeStartTransitionToOccludedOrInsecureCamera(): Boolean {
+ suspend fun maybeStartTransitionToOccludedOrInsecureCamera(
+ startTransition: suspend (state: KeyguardState, reason: String) -> UUID?
+ ): Boolean {
// The refactor is required for the occlusion interactor to work.
KeyguardWmStateRefactor.isUnexpectedlyInLegacyMode()
@@ -136,10 +141,7 @@
if (!maybeHandleInsecurePowerGesture()) {
// Otherwise, the double tap gesture occurred while not GONE and not dismissable,
// which means we will launch the secure camera, which OCCLUDES the keyguard.
- startTransitionTo(
- KeyguardState.OCCLUDED,
- ownerReason = "Power button gesture on lockscreen"
- )
+ startTransition(KeyguardState.OCCLUDED, "Power button gesture on lockscreen")
}
return true
@@ -147,10 +149,7 @@
// A SHOW_WHEN_LOCKED activity is on top of the task stack. Transition to OCCLUDED so
// it's visible.
// TODO(b/307976454) - Centralize transition to DREAMING here.
- startTransitionTo(
- KeyguardState.OCCLUDED,
- ownerReason = "SHOW_WHEN_LOCKED activity on top"
- )
+ startTransition(KeyguardState.OCCLUDED, "SHOW_WHEN_LOCKED activity on top")
return true
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 6b3dfe1..dbfe818 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -107,7 +107,9 @@
set(value) {
if (field == value) return
field = value
- updateHeight()
+ if (longPressEffect?.state != QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL) {
+ updateHeight()
+ }
}
override var squishinessFraction: Float = 1f
@@ -381,14 +383,6 @@
}
private fun updateHeight() {
- // TODO(b/332900989): Find a more robust way of resetting the tile if not reset by the
- // launch animation.
- if (!haveLongPressPropertiesBeenReset && longPressEffect != null) {
- // The launch animation of a long-press effect did not reset the long-press effect so
- // we must do it here
- resetLongPressEffectProperties()
- longPressEffect.resetState()
- }
val actualHeight =
if (heightOverride != HeightOverrideable.NO_OVERRIDE) {
heightOverride
@@ -417,17 +411,17 @@
}
override fun init(tile: QSTile) {
- val expandable = Expandable.fromView(this)
if (longPressEffect != null) {
isHapticFeedbackEnabled = false
longPressEffect.qsTile = tile
- longPressEffect.expandable = expandable
+ longPressEffect.createExpandableFromView(this)
initLongPressEffectCallback()
init(
{ _: View -> longPressEffect.onTileClick() },
null, // Haptics and long-clicks will be handled by the [QSLongPressEffect]
)
} else {
+ val expandable = Expandable.fromView(this)
init(
{ _: View? -> tile.click(expandable) },
{ _: View? ->
@@ -475,10 +469,10 @@
}
}
- override fun onReverseAnimator() {
+ override fun onReverseAnimator(playHaptics: Boolean) {
longPressEffectAnimator?.let {
val pausedProgress = it.animatedFraction
- longPressEffect?.playReverseHaptics(pausedProgress)
+ if (playHaptics) longPressEffect?.playReverseHaptics(pausedProgress)
it.reverse()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index 4914409..54ae225 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -49,6 +49,7 @@
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
/**
* An AsyncTask that saves an image to the media store in the background.
@@ -59,12 +60,73 @@
private static final String SCREENSHOT_ID_TEMPLATE = "Screenshot_%s";
private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
+ /**
+ * POD used in the AsyncTask which saves an image in the background.
+ */
+ static class SaveImageInBackgroundData {
+ public Bitmap image;
+ public Consumer<Uri> finisher;
+ public ActionsReadyListener mActionsReadyListener;
+ public QuickShareActionReadyListener mQuickShareActionsReadyListener;
+ public UserHandle owner;
+ public int displayId;
+
+ void clearImage() {
+ image = null;
+ }
+ }
+
+ /**
+ * Structure returned by the SaveImageInBackgroundTask
+ */
+ public static class SavedImageData {
+ public Uri uri;
+ public List<Notification.Action> smartActions;
+ public Notification.Action quickShareAction;
+ public UserHandle owner;
+ public String subject; // Title for sharing
+ public Long imageTime; // Time at which screenshot was saved
+
+ /**
+ * Used to reset the return data on error
+ */
+ public void reset() {
+ uri = null;
+ smartActions = null;
+ quickShareAction = null;
+ subject = null;
+ imageTime = null;
+ }
+ }
+
+ /**
+ * Structure returned by the QueryQuickShareInBackgroundTask
+ */
+ static class QuickShareData {
+ public Notification.Action quickShareAction;
+
+ /**
+ * Used to reset the return data on error
+ */
+ public void reset() {
+ quickShareAction = null;
+ }
+ }
+
+ interface ActionsReadyListener {
+ void onActionsReady(SavedImageData imageData);
+ }
+
+ interface QuickShareActionReadyListener {
+ void onActionsReady(QuickShareData quickShareData);
+ }
+
private final Context mContext;
private FeatureFlags mFlags;
private final ScreenshotSmartActions mScreenshotSmartActions;
- private final ScreenshotController.SaveImageInBackgroundData mParams;
- private final ScreenshotController.SavedImageData mImageData;
- private final ScreenshotController.QuickShareData mQuickShareData;
+ private final SaveImageInBackgroundData mParams;
+ private final SavedImageData mImageData;
+ private final QuickShareData mQuickShareData;
private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
private String mScreenshotId;
@@ -77,15 +139,15 @@
FeatureFlags flags,
ImageExporter exporter,
ScreenshotSmartActions screenshotSmartActions,
- ScreenshotController.SaveImageInBackgroundData data,
+ SaveImageInBackgroundData data,
ScreenshotNotificationSmartActionsProvider
screenshotNotificationSmartActionsProvider
) {
mContext = context;
mFlags = flags;
mScreenshotSmartActions = screenshotSmartActions;
- mImageData = new ScreenshotController.SavedImageData();
- mQuickShareData = new ScreenshotController.QuickShareData();
+ mImageData = new SavedImageData();
+ mQuickShareData = new QuickShareData();
mImageExporter = exporter;
// Prepare all the output metadata
@@ -195,7 +257,7 @@
* Update the listener run when the saving task completes. Used to avoid showing UI for the
* first screenshot when a second one is taken.
*/
- void setActionsReadyListener(ScreenshotController.ActionsReadyListener listener) {
+ void setActionsReadyListener(ActionsReadyListener listener) {
mParams.mActionsReadyListener = listener;
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 7739009..0a4635e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 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.
@@ -35,10 +35,7 @@
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.ActivityOptions;
-import android.app.ExitTransitionCoordinator;
import android.app.ICompatCameraControlCallback;
-import android.app.Notification;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -55,7 +52,6 @@
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
-import android.util.Pair;
import android.view.Display;
import android.view.ScrollCaptureResponse;
import android.view.View;
@@ -67,7 +63,6 @@
import android.widget.Toast;
import android.window.WindowContext;
-import com.android.internal.app.ChooserActivity;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.policy.PhoneWindow;
import com.android.settingslib.applications.InterestingConfigChanges;
@@ -89,7 +84,6 @@
import kotlin.Unit;
-import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
@@ -104,67 +98,6 @@
public class ScreenshotController implements ScreenshotHandler {
private static final String TAG = logTag(ScreenshotController.class);
- /**
- * POD used in the AsyncTask which saves an image in the background.
- */
- static class SaveImageInBackgroundData {
- public Bitmap image;
- public Consumer<Uri> finisher;
- public ScreenshotController.ActionsReadyListener mActionsReadyListener;
- public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener;
- public UserHandle owner;
- public int displayId;
-
- void clearImage() {
- image = null;
- }
- }
-
- /**
- * Structure returned by the SaveImageInBackgroundTask
- */
- public static class SavedImageData {
- public Uri uri;
- public List<Notification.Action> smartActions;
- public Notification.Action quickShareAction;
- public UserHandle owner;
- public String subject; // Title for sharing
- public Long imageTime; // Time at which screenshot was saved
-
- /**
- * Used to reset the return data on error
- */
- public void reset() {
- uri = null;
- smartActions = null;
- quickShareAction = null;
- subject = null;
- imageTime = null;
- }
- }
-
- /**
- * Structure returned by the QueryQuickShareInBackgroundTask
- */
- static class QuickShareData {
- public Notification.Action quickShareAction;
-
- /**
- * Used to reset the return data on error
- */
- public void reset() {
- quickShareAction = null;
- }
- }
-
- interface ActionsReadyListener {
- void onActionsReady(ScreenshotController.SavedImageData imageData);
- }
-
- interface QuickShareActionReadyListener {
- void onActionsReady(ScreenshotController.QuickShareData quickShareData);
- }
-
public interface TransitionDestination {
/**
* Allows the long screenshot activity to call back with a destination location (the bounds
@@ -213,7 +146,6 @@
private final ScreenshotNotificationSmartActionsProvider
mScreenshotNotificationSmartActionsProvider;
private final TimeoutHandler mScreenshotHandler;
- private final ActionIntentExecutor mActionIntentExecutor;
private final UserManager mUserManager;
private final AssistContentRequester mAssistContentRequester;
private final ActionExecutor mActionExecutor;
@@ -259,7 +191,6 @@
BroadcastDispatcher broadcastDispatcher,
ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
ScreenshotActionsController.Factory screenshotActionsControllerFactory,
- ActionIntentExecutor actionIntentExecutor,
ActionExecutor.Factory actionExecutorFactory,
UserManager userManager,
AssistContentRequester assistContentRequester,
@@ -289,7 +220,6 @@
final Context displayContext = context.createDisplayContext(display);
mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
mFlags = flags;
- mActionIntentExecutor = actionIntentExecutor;
mUserManager = userManager;
mMessageContainerController = messageContainerController;
mAssistContentRequester = assistContentRequester;
@@ -765,33 +695,6 @@
mScreenshotAnimation.start();
}
- /**
- * Supplies the necessary bits for the shared element transition to share sheet.
- * Note that once called, the action intent to share must be sent immediately after.
- */
- private Pair<ActivityOptions, ExitTransitionCoordinator> createWindowTransition() {
- ExitTransitionCoordinator.ExitTransitionCallbacks callbacks =
- new ExitTransitionCoordinator.ExitTransitionCallbacks() {
- @Override
- public boolean isReturnTransitionAllowed() {
- return false;
- }
-
- @Override
- public void hideSharedElements() {
- finishDismiss();
- }
-
- @Override
- public void onFinish() {
- }
- };
-
- return ActivityOptions.startSharedElementAnimation(mWindow, callbacks, null,
- Pair.create(mViewProxy.getScreenshotPreview(),
- ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME));
- }
-
/** Reset screenshot view and then call onCompleteRunnable */
private void finishDismiss() {
Log.d(TAG, "finishDismiss");
@@ -838,11 +741,11 @@
private void saveScreenshotInWorkerThread(
UserHandle owner,
@NonNull Consumer<Uri> finisher,
- @Nullable ActionsReadyListener actionsReadyListener,
- @Nullable QuickShareActionReadyListener
+ @Nullable SaveImageInBackgroundTask.ActionsReadyListener actionsReadyListener,
+ @Nullable SaveImageInBackgroundTask.QuickShareActionReadyListener
quickShareActionsReadyListener) {
- ScreenshotController.SaveImageInBackgroundData
- data = new ScreenshotController.SaveImageInBackgroundData();
+ SaveImageInBackgroundTask.SaveImageInBackgroundData
+ data = new SaveImageInBackgroundTask.SaveImageInBackgroundData();
data.image = mScreenBitmap;
data.finisher = finisher;
data.mActionsReadyListener = actionsReadyListener;
@@ -881,7 +784,7 @@
/**
* Logs success/failure of the screenshot saving task, and shows an error if it failed.
*/
- private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) {
+ private void logSuccessOnActionsReady(SaveImageInBackgroundTask.SavedImageData imageData) {
logScreenshotResultStatus(imageData.uri, imageData.owner);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index 696298e..0f6f03a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -143,9 +143,12 @@
this::onLaunchingActivityChanged);
mJavaAdapter.alwaysCollectFlow(mCommunalInteractor.isIdleOnCommunal(),
this::onCommunalShowingChanged);
- mJavaAdapter.alwaysCollectFlow(mKeyguardTransitionInteractor.transitionValue(
- KeyguardState.LOCKSCREEN),
- this::onLockscreenKeyguardStateTransitionValueChanged);
+
+ if (SceneContainerFlag.isEnabled()) {
+ mJavaAdapter.alwaysCollectFlow(mKeyguardTransitionInteractor.transitionValue(
+ KeyguardState.LOCKSCREEN),
+ this::onLockscreenKeyguardStateTransitionValueChanged);
+ }
pipeline.setVisualStabilityManager(mNotifStabilityManager);
}
@@ -381,6 +384,10 @@
}
final boolean isShowing = value > 0.0f;
+ if (isShowing == mLockscreenShowing) {
+ return;
+ }
+
mLockscreenShowing = isShowing;
updateAllowedStates("lockscreenShowing", isShowing);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 789a6f4..e08dbb9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -308,6 +308,9 @@
HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.getKey());
if (headsUpEntry != null && headsUpEntry.mRemoteInputActive != remoteInputActive) {
headsUpEntry.mRemoteInputActive = remoteInputActive;
+ if (ExpandHeadsUpOnInlineReply.isEnabled() && remoteInputActive) {
+ headsUpEntry.mRemoteInputActivatedAtLeastOnce = true;
+ }
if (remoteInputActive) {
headsUpEntry.cancelAutoRemovalCallbacks("setRemoteInputActive(true)");
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index a0eb989..6517135 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -41,6 +41,7 @@
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
+import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply;
import com.android.systemui.util.ListenerSet;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.settings.GlobalSettings;
@@ -726,6 +727,7 @@
* of AvalancheController that take it as param.
*/
public class HeadsUpEntry implements Comparable<HeadsUpEntry> {
+ public boolean mRemoteInputActivatedAtLeastOnce;
public boolean mRemoteInputActive;
public boolean mUserActionMayIndirectlyRemove;
@@ -835,6 +837,15 @@
*/
public boolean isSticky() {
if (mEntry == null) return false;
+
+ if (ExpandHeadsUpOnInlineReply.isEnabled()) {
+ // we don't consider pinned and expanded huns as sticky after the remote input
+ // has been activated for them
+ if (!mRemoteInputActive && mRemoteInputActivatedAtLeastOnce) {
+ return false;
+ }
+ }
+
return (mEntry.isRowPinned() && mExpanded)
|| mRemoteInputActive
|| hasFullScreenIntent(mEntry);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt
index 25dd9fe..24e8b18 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt
@@ -27,7 +27,6 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.screenshot.ScreenshotController.SaveImageInBackgroundData
import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
@@ -44,7 +43,7 @@
private val imageExporter = mock<ImageExporter>()
private val smartActions = mock<ScreenshotSmartActions>()
private val smartActionsProvider = mock<ScreenshotNotificationSmartActionsProvider>()
- private val saveImageData = SaveImageInBackgroundData()
+ private val saveImageData = SaveImageInBackgroundTask.SaveImageInBackgroundData()
private val testScreenshotId: String = "testScreenshotId"
private val testBitmap = mock<Bitmap>()
private val testUser = UserHandle.getUserHandleForUid(0)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index f7ce367..c00454f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -2,6 +2,9 @@
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.graphics.Bitmap
import android.os.UserHandle
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.widgets.WidgetConfigurator
@@ -13,6 +16,8 @@
/** Fake implementation of [CommunalWidgetRepository] */
class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) :
CommunalWidgetRepository {
+ private val fakeDatabase = mutableMapOf<Int, CommunalWidgetContentModel>()
+
private val _communalWidgets = MutableStateFlow<List<CommunalWidgetContentModel>>(emptyList())
override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = _communalWidgets
@@ -38,12 +43,54 @@
}
}
- override fun deleteWidget(widgetId: Int) {
- if (_communalWidgets.value.none { it.appWidgetId == widgetId }) {
- return
- }
+ fun addWidget(
+ appWidgetId: Int,
+ componentName: String = "pkg/cls",
+ priority: Int = 0,
+ category: Int = AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD,
+ userId: Int = 0,
+ ) {
+ fakeDatabase[appWidgetId] =
+ CommunalWidgetContentModel.Available(
+ appWidgetId = appWidgetId,
+ priority = priority,
+ providerInfo =
+ AppWidgetProviderInfo().apply {
+ provider = ComponentName.unflattenFromString(componentName)!!
+ widgetCategory = category
+ providerInfo =
+ ActivityInfo().apply {
+ applicationInfo =
+ ApplicationInfo().apply {
+ uid = userId * UserHandle.PER_USER_RANGE
+ }
+ }
+ },
+ )
+ _communalWidgets.value = fakeDatabase.values.toList()
+ }
- _communalWidgets.value = _communalWidgets.value.filter { it.appWidgetId != widgetId }
+ fun addPendingWidget(
+ appWidgetId: Int,
+ componentName: String = "pkg/cls",
+ priority: Int = 0,
+ icon: Bitmap? = null,
+ userId: Int = 0,
+ ) {
+ fakeDatabase[appWidgetId] =
+ CommunalWidgetContentModel.Pending(
+ appWidgetId = appWidgetId,
+ priority = priority,
+ componentName = ComponentName.unflattenFromString(componentName)!!,
+ icon = icon,
+ user = UserHandle(userId),
+ )
+ _communalWidgets.value = fakeDatabase.values.toList()
+ }
+
+ override fun deleteWidget(widgetId: Int) {
+ fakeDatabase.remove(widgetId)
+ _communalWidgets.value = fakeDatabase.values.toList()
}
override fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) {}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt
index 079852a..494f08b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractorKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.domain.interactor
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
import com.android.systemui.kosmos.Kosmos
@@ -38,5 +39,6 @@
transitionInteractor = keyguardTransitionInteractor,
powerInteractor = powerInteractor,
keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+ communalSceneInteractor = communalSceneInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
index 317294f..c694114 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
@@ -16,7 +16,7 @@
package com.android.systemui.keyguard.domain.interactor
-import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.keyguardRepository
@@ -37,7 +37,7 @@
mainDispatcher = testDispatcher,
keyguardInteractor = keyguardInteractor,
powerInteractor = powerInteractor,
- communalInteractor = communalInteractor,
+ communalSceneInteractor = communalSceneInteractor,
keyguardOcclusionInteractor = keyguardOcclusionInteractor,
biometricSettingsRepository = biometricSettingsRepository,
keyguardRepository = keyguardRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt
index c216945..7827655 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractorKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -37,5 +38,6 @@
powerInteractor = powerInteractor,
communalInteractor = communalInteractor,
keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+ communalSceneInteractor = communalSceneInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
index 42ee152..3c369d7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
@@ -17,7 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.keyguard.keyguardSecurityModel
-import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -36,7 +36,7 @@
bgDispatcher = testDispatcher,
mainDispatcher = testDispatcher,
keyguardInteractor = keyguardInteractor,
- communalInteractor = communalInteractor,
+ communalSceneInteractor = communalSceneInteractor,
keyguardSecurityModel = keyguardSecurityModel,
selectedUserInteractor = selectedUserInteractor,
powerInteractor = powerInteractor,
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index 3d53deb..4fc9d55 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -103,9 +103,10 @@
String packageName = getNextArgRequired();
String address = getNextArgRequired();
String deviceProfile = getNextArg();
+ boolean selfManaged = getNextBooleanArg();
final MacAddress macAddress = MacAddress.fromString(address);
mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress,
- deviceProfile, deviceProfile, /* associatedDevice */ null, false,
+ deviceProfile, deviceProfile, /* associatedDevice */ null, selfManaged,
/* callback */ null, /* resultReceiver */ null);
}
break;
@@ -462,6 +463,17 @@
}
}
+ private boolean getNextBooleanArg() {
+ String arg = getNextArg();
+ if (arg == null || "false".equalsIgnoreCase(arg)) {
+ return false;
+ } else if ("true".equalsIgnoreCase(arg)) {
+ return Boolean.parseBoolean(arg);
+ } else {
+ throw new IllegalArgumentException("Expected a boolean argument but was: " + arg);
+ }
+ }
+
@Override
public void onHelp() {
PrintWriter pw = getOutPrintWriter();
@@ -470,7 +482,7 @@
pw.println(" Print this help text.");
pw.println(" list USER_ID");
pw.println(" List all Associations for a user.");
- pw.println(" associate USER_ID PACKAGE MAC_ADDRESS [DEVICE_PROFILE]");
+ pw.println(" associate USER_ID PACKAGE MAC_ADDRESS [DEVICE_PROFILE] [SELF_MANAGED]");
pw.println(" Create a new Association.");
pw.println(" disassociate USER_ID PACKAGE MAC_ADDRESS");
pw.println(" Remove an existing Association.");
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 25b9228..92553b9 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1432,6 +1432,8 @@
}
mCurCommunicationPortId = portId;
+ mAudioService.postScoDeviceActive(isBluetoothScoActive());
+
final int nbDispatchers = mCommDevDispatchers.beginBroadcast();
for (int i = 0; i < nbDispatchers; i++) {
try {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index fe3bbb0..3d41f05 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -65,6 +65,7 @@
import static com.android.media.audio.Flags.alarmMinVolumeZero;
import static com.android.media.audio.Flags.audioserverPermissions;
import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
+import static com.android.media.audio.Flags.replaceStreamBtSco;
import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
import static com.android.media.audio.Flags.setStreamVolumeOrder;
import static com.android.media.audio.Flags.vgsVssSyncMuteOrder;
@@ -287,7 +288,6 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;
@@ -462,6 +462,7 @@
private static final int MSG_CONFIGURATION_CHANGED = 54;
private static final int MSG_BROADCAST_MASTER_MUTE = 55;
private static final int MSG_UPDATE_CONTEXTUAL_VOLUMES = 56;
+ private static final int MSG_SCO_DEVICE_ACTIVE_UPDATE = 57;
/**
* Messages handled by the {@link SoundDoseHelper}, do not exceed
@@ -716,6 +717,8 @@
* @see System#MUTE_STREAMS_AFFECTED */
private int mUserMutableStreams;
+ private final AtomicBoolean mScoDeviceActive = new AtomicBoolean(false);
+
@NonNull
private SoundEffectsHelper mSfxHelper;
@@ -1471,6 +1474,9 @@
synchronized (VolumeStreamState.class) {
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
VolumeStreamState streamState = mStreamStates[streamType];
+ if (streamState == null) {
+ continue;
+ }
int groupId = getVolumeGroupForStreamType(streamType);
if (groupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP
&& sVolumeGroupStates.indexOfKey(groupId) >= 0) {
@@ -2079,7 +2085,7 @@
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
VolumeStreamState streamState = mStreamStates[streamType];
final int res = AudioSystem.initStreamVolume(
- streamType, streamState.mIndexMin / 10, streamState.mIndexMax / 10);
+ streamType, MIN_STREAM_VOLUME[streamType], MAX_STREAM_VOLUME[streamType]);
if (res != AudioSystem.AUDIO_STATUS_OK) {
status = res;
Log.e(TAG, "Failed to initStreamVolume (" + res + ") for stream " + streamType);
@@ -2237,11 +2243,13 @@
synchronized (VolumeStreamState.class) {
int numStreamTypes = AudioSystem.getNumStreamTypes();
for (int streamType = 0; streamType < numStreamTypes; streamType++) {
- mStreamStates[streamType]
- .setAllIndexes(mStreamStates[mStreamVolumeAlias[streamType]], TAG);
- // apply stream volume
- if (!mStreamStates[streamType].mIsMuted) {
- mStreamStates[streamType].applyAllVolumes();
+ if (mStreamVolumeAlias[streamType] >= 0) {
+ mStreamStates[streamType]
+ .setAllIndexes(mStreamStates[mStreamVolumeAlias[streamType]], TAG);
+ // apply stream volume
+ if (!mStreamStates[streamType].mIsMuted) {
+ mStreamStates[streamType].applyAllVolumes();
+ }
}
}
}
@@ -2332,6 +2340,10 @@
* @param caller caller of this method
*/
private void updateVolumeStates(int device, int streamType, String caller) {
+ if (replaceStreamBtSco() && streamType == AudioSystem.STREAM_BLUETOOTH_SCO) {
+ return;
+ }
+
// Handle device volume aliasing of SPEAKER_SAFE.
if (device == AudioSystem.DEVICE_OUT_SPEAKER_SAFE) {
device = AudioSystem.DEVICE_OUT_SPEAKER;
@@ -2367,7 +2379,9 @@
{
int numStreamTypes = AudioSystem.getNumStreamTypes();
for (int streamType = 0; streamType < numStreamTypes; streamType++) {
- mStreamStates[streamType].checkFixedVolumeDevices();
+ if (mStreamStates[streamType] != null) {
+ mStreamStates[streamType].checkFixedVolumeDevices();
+ }
}
}
@@ -2381,9 +2395,9 @@
// that has the the MODIFY_PHONE_STATE permission.
for (int i = 0; i < mStreamStates.length; i++) {
final VolumeStreamState vss = mStreamStates[i];
- if (vss.mIndexMin > 0 &&
- (vss.mStreamType != AudioSystem.STREAM_VOICE_CALL &&
- vss.mStreamType != AudioSystem.STREAM_BLUETOOTH_SCO)) {
+ if (vss != null && vss.mIndexMin > 0
+ && (vss.mStreamType != AudioSystem.STREAM_VOICE_CALL
+ && vss.mStreamType != AudioSystem.STREAM_BLUETOOTH_SCO)) {
mMuteAffectedStreams &= ~(1 << vss.mStreamType);
}
}
@@ -2395,8 +2409,11 @@
VolumeStreamState[] streams = mStreamStates = new VolumeStreamState[numStreamTypes];
for (int i = 0; i < numStreamTypes; i++) {
- streams[i] =
- new VolumeStreamState(System.VOLUME_SETTINGS_INT[mStreamVolumeAlias[i]], i);
+ // a negative mStreamVolumeAlias value means the stream state type is not supported
+ if (mStreamVolumeAlias[i] >= 0) {
+ streams[i] =
+ new VolumeStreamState(System.VOLUME_SETTINGS_INT[mStreamVolumeAlias[i]], i);
+ }
}
checkAllFixedVolumeDevices();
@@ -2429,7 +2446,7 @@
continue;
}
}
- if (stream != streamVolumeAlias) {
+ if (streamVolumeAlias >= 0 && stream != streamVolumeAlias) {
AudioSystem.DEFAULT_STREAM_VOLUME[stream] =
getUiDefaultRescaledIndex(streamVolumeAlias, stream);
}
@@ -2441,10 +2458,37 @@
srcStream, dstStream) + 5) / 10;
}
+ private static int replaceBtScoStreamWithVoiceCall(int streamType, String caller) {
+ if (replaceStreamBtSco() && streamType == AudioSystem.STREAM_BLUETOOTH_SCO) {
+ if (DEBUG_VOL) {
+ Log.d(TAG,
+ "Deprecating STREAM_BLUETOOTH_SCO, using STREAM_VOICE_CALL instead for "
+ + "caller: " + caller);
+ }
+ streamType = AudioSystem.STREAM_VOICE_CALL;
+ }
+ return streamType;
+ }
+
+ private boolean isStreamBluetoothSco(int streamType) {
+ if (replaceStreamBtSco()) {
+ if (streamType == AudioSystem.STREAM_BLUETOOTH_SCO) {
+ // this should not happen, throwing exception
+ throw new IllegalArgumentException("STREAM_BLUETOOTH_SCO is deprecated");
+ }
+ return streamType == AudioSystem.STREAM_VOICE_CALL && mScoDeviceActive.get();
+ } else {
+ return streamType == AudioSystem.STREAM_BLUETOOTH_SCO;
+ }
+ }
+
private void dumpStreamStates(PrintWriter pw) {
pw.println("\nStream volumes (device: index)");
int numStreamTypes = AudioSystem.getNumStreamTypes();
for (int i = 0; i < numStreamTypes; i++) {
+ if (replaceStreamBtSco() && i == AudioSystem.STREAM_BLUETOOTH_SCO) {
+ continue;
+ }
StringBuilder alias = new StringBuilder();
if (mStreamVolumeAlias[i] != i) {
alias.append(" (aliased to: ")
@@ -2506,6 +2550,13 @@
mStreamVolumeAlias[AudioSystem.STREAM_ACCESSIBILITY] = a11yStreamAlias;
mStreamVolumeAlias[AudioSystem.STREAM_ASSISTANT] = assistantStreamAlias;
+ if (replaceStreamBtSco()) {
+ // we do not support STREAM_BLUETOOTH_SCO, this will lead to having
+ // mStreanStates[STREAM_BLUETOOTH_SCO] = null
+ // TODO: replace arrays with SparseIntArrays to avoid null checks
+ mStreamVolumeAlias[AudioSystem.STREAM_BLUETOOTH_SCO] = -1;
+ }
+
if (updateVolumes && mStreamStates != null) {
updateDefaultVolumes();
@@ -3065,7 +3116,7 @@
return 0;
}
- return ((step * dstRange + srcRange / 2) / srcRange);
+ return (step * dstRange + srcRange / 2) / srcRange;
}
///////////////////////////////////////////////////////////////////////////
@@ -3539,7 +3590,7 @@
return;
}
- final int streamType;
+ int streamType;
synchronized (mForceControlStreamLock) {
// Request lock in case mVolumeControlStream is changed by other thread.
if (mUserSelectedVolumeControlStream) { // implies mVolumeControlStream != -1
@@ -3564,6 +3615,8 @@
final boolean isMute = isMuteAdjust(direction);
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "adjustSuggestedStreamVolume");
+
ensureValidStreamType(streamType);
final int resolvedStream = mStreamVolumeAlias[streamType];
@@ -3641,6 +3694,8 @@
if (mUseFixedVolume) {
return;
}
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "adjustStreamVolume");
+
if (DEBUG_VOL) Log.d(TAG, "adjustStreamVolume() stream=" + streamType + ", dir=" + direction
+ ", flags=" + flags + ", caller=" + caller);
@@ -3657,8 +3712,9 @@
// that the calling app have the MODIFY_PHONE_STATE permission.
if (isMuteAdjust &&
(streamType == AudioSystem.STREAM_VOICE_CALL ||
- streamType == AudioSystem.STREAM_BLUETOOTH_SCO) &&
- mContext.checkPermission(MODIFY_PHONE_STATE, pid, uid)
+ // TODO: when replaceStreamBtSco flag is rolled out remove next condition
+ isStreamBluetoothSco(streamType))
+ && mContext.checkPermission(MODIFY_PHONE_STATE, pid, uid)
!= PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "MODIFY_PHONE_STATE Permission Denial: adjustStreamVolume from pid="
+ Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
@@ -3726,7 +3782,8 @@
}
} else {
// convert one UI step (+/-1) into a number of internal units on the stream alias
- step = rescaleStep(10, streamType, streamTypeAlias);
+ step = rescaleStep((int) (10 * streamState.getIndexStepFactor()), streamType,
+ streamTypeAlias);
}
// If either the client forces allowing ringer modes for this adjustment,
@@ -3866,7 +3923,6 @@
}
final int newIndex = mStreamStates[streamType].getIndex(device);
-
if (adjustVolume) {
synchronized (mHdmiClientLock) {
if (mHdmiManager != null) {
@@ -3950,9 +4006,9 @@
List<Integer> streamsToMute = new ArrayList<>();
for (int stream = 0; stream < mStreamStates.length; stream++) {
VolumeStreamState vss = mStreamStates[stream];
- if (streamAlias == mStreamVolumeAlias[stream] && vss.isMutable()) {
- if (!(mCameraSoundForced
- && (vss.getStreamType()
+ if (vss != null && streamAlias == mStreamVolumeAlias[stream]
+ && vss.isMutable()) {
+ if (!(mCameraSoundForced && (vss.getStreamType()
== AudioSystem.STREAM_SYSTEM_ENFORCED))) {
boolean changed = vss.mute(state, /* apply= */ false,
"muteAliasStreams");
@@ -3973,8 +4029,14 @@
private void broadcastMuteSetting(int streamType, boolean isMuted) {
// Stream mute changed, fire the intent.
Intent intent = new Intent(AudioManager.STREAM_MUTE_CHANGED_ACTION);
- intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType);
intent.putExtra(AudioManager.EXTRA_STREAM_VOLUME_MUTED, isMuted);
+ if (replaceStreamBtSco() && isStreamBluetoothSco(streamType)) {
+ intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
+ AudioSystem.STREAM_BLUETOOTH_SCO);
+ // in this case broadcast for both sco and voice_call streams the mute status
+ sendBroadcastToAll(intent, null /* options */);
+ }
+ intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType);
sendBroadcastToAll(intent, null /* options */);
}
@@ -4093,7 +4155,7 @@
setStreamVolumeInt(stream, index, device, false, caller, hasModifyAudioSettings);
// setting non-zero volume for a muted stream unmutes the stream and vice versa
// except for BT SCO stream where only explicit mute is allowed to comply to BT requirements
- if ((streamType != AudioSystem.STREAM_BLUETOOTH_SCO) && canChangeMute) {
+ if (!isStreamBluetoothSco(streamType) && canChangeMute) {
// As adjustStreamVolume with muteAdjust flags mute/unmutes stream and aliased streams.
muteAliasStreams(stream, index == 0);
}
@@ -4382,6 +4444,9 @@
@Nullable AudioDeviceAttributes ada,
String callingPackage, String attributionTag,
boolean canChangeMuteAndUpdateController) {
+ streamType = replaceBtScoStreamWithVoiceCall(streamType,
+ "setStreamVolumeWithAttributionInt");
+
if ((streamType == AudioManager.STREAM_ACCESSIBILITY) && !canChangeAccessibilityVolume()) {
Log.w(TAG, "Trying to call setStreamVolume() for a11y without"
+ " CHANGE_ACCESSIBILITY_VOLUME callingPackage=" + callingPackage);
@@ -4389,7 +4454,7 @@
}
if ((streamType == AudioManager.STREAM_VOICE_CALL) && (index == 0)
&& (mContext.checkCallingOrSelfPermission(MODIFY_PHONE_STATE)
- != PackageManager.PERMISSION_GRANTED)) {
+ != PackageManager.PERMISSION_GRANTED) && !isStreamBluetoothSco(streamType)) {
Log.w(TAG, "Trying to call setStreamVolume() for STREAM_VOICE_CALL and index 0 without"
+ " MODIFY_PHONE_STATE callingPackage=" + callingPackage);
return;
@@ -4632,6 +4697,8 @@
+ vgsVssSyncMuteOrder());
pw.println("\tcom.android.media.audio.absVolumeIndexFix:"
+ absVolumeIndexFix());
+ pw.println("\tcom.android.media.audio.replaceStreamBtSco:"
+ + replaceStreamBtSco());
}
private void dumpAudioMode(PrintWriter pw) {
@@ -4720,12 +4787,13 @@
if (mUseFixedVolume) {
return;
}
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "setStreamVolume");
ensureValidStreamType(streamType);
int streamTypeAlias = mStreamVolumeAlias[streamType];
VolumeStreamState streamState = mStreamStates[streamTypeAlias];
- if ((streamType == AudioManager.STREAM_VOICE_CALL)
+ if (!replaceStreamBtSco() && (streamType == AudioManager.STREAM_VOICE_CALL)
&& isInCommunication() && mDeviceBroker.isBluetoothScoActive()) {
Log.i(TAG, "setStreamVolume for STREAM_VOICE_CALL, switching to STREAM_BLUETOOTH_SCO");
streamType = AudioManager.STREAM_BLUETOOTH_SCO;
@@ -4919,6 +4987,9 @@
!= PackageManager.PERMISSION_GRANTED) {
return;
}
+
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "forceVolumeControlStream");
+
if (DEBUG_VOL) { Log.d(TAG, String.format("forceVolumeControlStream(%d)", streamType)); }
synchronized(mForceControlStreamLock) {
if (mVolumeControlStream != -1 && streamType != -1) {
@@ -5096,6 +5167,8 @@
if (streamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
streamType = getActiveStreamType(streamType);
}
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "isStreamMute");
+
synchronized (VolumeStreamState.class) {
ensureValidStreamType(streamType);
return mStreamStates[streamType].mIsMuted;
@@ -5269,6 +5342,8 @@
/** @see AudioManager#getStreamVolume(int) */
public int getStreamVolume(int streamType) {
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "getStreamVolume");
+
ensureValidStreamType(streamType);
int device = getDeviceForStream(streamType);
return getStreamVolume(streamType, device);
@@ -5329,6 +5404,7 @@
/** @see AudioManager#getStreamMaxVolume(int) */
public int getStreamMaxVolume(int streamType) {
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "getStreamMaxVolume");
ensureValidStreamType(streamType);
return (mStreamStates[streamType].getMaxIndex() + 5) / 10;
}
@@ -5336,6 +5412,7 @@
/** @see AudioManager#getStreamMinVolumeInt(int)
* Part of service interface, check permissions here */
public int getStreamMinVolume(int streamType) {
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "getStreamMinVolume");
ensureValidStreamType(streamType);
final boolean isPrivileged =
Binder.getCallingUid() == Process.SYSTEM_UID
@@ -5350,7 +5427,10 @@
public int getLastAudibleStreamVolume(int streamType) {
super.getLastAudibleStreamVolume_enforcePermission();
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "getLastAudibleStreamVolume");
+
ensureValidStreamType(streamType);
+
int device = getDeviceForStream(streamType);
return (mStreamStates[streamType].getIndex(device) + 5) / 10;
}
@@ -5423,7 +5503,7 @@
}
ArrayList<Integer> res = new ArrayList(1);
for (int stream : mStreamVolumeAlias) {
- if (!res.contains(stream)) {
+ if (stream >= 0 && !res.contains(stream)) {
res.add(stream);
}
}
@@ -5439,6 +5519,9 @@
public @AudioManager.PublicStreamTypes
int getStreamTypeAlias(@AudioManager.PublicStreamTypes int sourceStreamType) {
super.getStreamTypeAlias_enforcePermission();
+
+ sourceStreamType = replaceBtScoStreamWithVoiceCall(sourceStreamType, "getStreamTypeAlias");
+
// verify parameters
ensureValidStreamType(sourceStreamType);
@@ -6549,6 +6632,10 @@
for (int streamType = 0; streamType < numStreamTypes; streamType++) {
VolumeStreamState streamState = mStreamStates[streamType];
+ if (streamState == null) {
+ continue;
+ }
+
if (userSwitch && mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC) {
continue;
}
@@ -7024,14 +7111,17 @@
@Override
public boolean isStreamAffectedByRingerMode(int streamType) {
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "isStreamAffectedByRingerMode");
return (mRingerModeAffectedStreams & (1 << streamType)) != 0;
}
public boolean isStreamAffectedByCurrentZen(int streamType) {
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "isStreamAffectedByCurrentZen");
return (mZenModeAffectedStreams & (1 << streamType)) != 0;
}
private boolean isStreamMutedByRingerOrZenMode(int streamType) {
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "isStreamMutedByRingerOrZenMode");
return (sRingerAndZenModeMutedStreams & (1 << streamType)) != 0;
}
@@ -7146,6 +7236,7 @@
@Override
public boolean isStreamAffectedByMute(int streamType) {
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "isStreamAffectedByMute");
return (mMuteAffectedStreams & (1 << streamType)) != 0;
}
@@ -7221,11 +7312,15 @@
case AudioSystem.PLATFORM_VOICE:
if (isInCommunication()
|| mAudioSystem.isStreamActive(AudioManager.STREAM_VOICE_CALL, 0)) {
- if (mDeviceBroker.isBluetoothScoActive()) {
- // Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO...");
+ if (!replaceStreamBtSco() && mDeviceBroker.isBluetoothScoActive()) {
+ if (DEBUG_VOL) {
+ Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO...");
+ }
return AudioSystem.STREAM_BLUETOOTH_SCO;
} else {
- // Log.v(TAG, "getActiveStreamType: Forcing STREAM_VOICE_CALL...");
+ if (DEBUG_VOL) {
+ Log.v(TAG, "getActiveStreamType: Forcing STREAM_VOICE_CALL...");
+ }
return AudioSystem.STREAM_VOICE_CALL;
}
} else if (suggestedStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
@@ -7261,7 +7356,7 @@
}
default:
if (isInCommunication()) {
- if (mDeviceBroker.isBluetoothScoActive()) {
+ if (!replaceStreamBtSco() && mDeviceBroker.isBluetoothScoActive()) {
if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Forcing STREAM_BLUETOOTH_SCO");
return AudioSystem.STREAM_BLUETOOTH_SCO;
} else {
@@ -7295,6 +7390,10 @@
}
break;
}
+
+ suggestedStreamType = replaceBtScoStreamWithVoiceCall(suggestedStreamType,
+ "getActiveStreamType");
+
if (DEBUG_VOL) Log.v(TAG, "getActiveStreamType: Returning suggested type "
+ suggestedStreamType);
return suggestedStreamType;
@@ -7427,7 +7526,7 @@
: Math.min(idx + 1, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM]);
// update the VolumeStreamState for STREAM_ALARM and its aliases
for (int stream : mStreamVolumeAlias) {
- if (mStreamVolumeAlias[stream] == AudioSystem.STREAM_ALARM) {
+ if (stream >= 0 && mStreamVolumeAlias[stream] == AudioSystem.STREAM_ALARM) {
mStreamStates[stream].updateNoPermMinIndex(safeIndex);
}
}
@@ -7442,6 +7541,7 @@
*/
@VisibleForTesting
public int getDeviceForStream(int stream) {
+ stream = replaceBtScoStreamWithVoiceCall(stream, "getDeviceForStream");
return selectOneAudioDevice(getDeviceSetForStream(stream));
}
@@ -7503,6 +7603,8 @@
@Override
@Deprecated
public int getDeviceMaskForStream(int streamType) {
+ streamType = replaceBtScoStreamWithVoiceCall(streamType, "getDeviceMaskForStream");
+
ensureValidStreamType(streamType);
// no permission required
final long token = Binder.clearCallingIdentity();
@@ -7537,6 +7639,7 @@
*/
@NonNull
public Set<Integer> getDeviceSetForStream(int stream) {
+ stream = replaceBtScoStreamWithVoiceCall(stream, "getDeviceSetForStream");
ensureValidStreamType(stream);
synchronized (VolumeStreamState.class) {
return mStreamStates[stream].observeDevicesForStream_syncVSS(true);
@@ -7547,7 +7650,7 @@
synchronized (mSettingsLock) {
synchronized (VolumeStreamState.class) {
for (int stream = 0; stream < mStreamStates.length; stream++) {
- if (stream != skipStream) {
+ if (stream != skipStream && mStreamStates[stream] != null) {
Set<Integer> deviceSet =
mStreamStates[stream].observeDevicesForStream_syncVSS(
false /*checkOthers*/);
@@ -7577,6 +7680,19 @@
0 /*delay*/);
}
+ /*package*/ void postScoDeviceActive(boolean scoDeviceActive) {
+ sendMsg(mAudioHandler,
+ MSG_SCO_DEVICE_ACTIVE_UPDATE,
+ SENDMSG_QUEUE, scoDeviceActive ? 1 : 0 /*arg1*/, 0 /*arg2*/, null /*obj*/,
+ 0 /*delay*/);
+ }
+
+ private void onUpdateScoDeviceActive(boolean scoDeviceActive) {
+ if (mScoDeviceActive.compareAndSet(!scoDeviceActive, scoDeviceActive)) {
+ getVssVolumeForStream(AudioSystem.STREAM_VOICE_CALL).updateIndexFactors();
+ }
+ }
+
/**
* @see AudioDeviceVolumeManager#setDeviceAbsoluteMultiVolumeBehavior
*
@@ -8033,9 +8149,10 @@
private void initVolumeGroupStates() {
for (final AudioVolumeGroup avg : getAudioVolumeGroups()) {
try {
- // if no valid attributes, this volume group is not controllable, throw exception
- ensureValidAttributes(avg);
- sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg));
+ // if no valid attributes, this volume group is not controllable
+ if (ensureValidAttributes(avg)) {
+ sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg));
+ }
} catch (IllegalArgumentException e) {
// Volume Groups without attributes are not controllable through set/get volume
// using attributes. Do not append them.
@@ -8056,13 +8173,21 @@
}
}
- private void ensureValidAttributes(AudioVolumeGroup avg) {
+ private boolean ensureValidAttributes(AudioVolumeGroup avg) {
boolean hasAtLeastOneValidAudioAttributes = avg.getAudioAttributes().stream()
.anyMatch(aa -> !aa.equals(AudioProductStrategy.getDefaultAttributes()));
if (!hasAtLeastOneValidAudioAttributes) {
throw new IllegalArgumentException("Volume Group " + avg.name()
+ " has no valid audio attributes");
}
+ if (replaceStreamBtSco()) {
+ for (int streamType : avg.getLegacyStreamTypes()) {
+ if (streamType == AudioSystem.STREAM_BLUETOOTH_SCO) {
+ return false;
+ }
+ }
+ }
+ return true;
}
private void readVolumeGroupsSettings(boolean userSwitch) {
@@ -8169,8 +8294,14 @@
break;
}
}
- mIndexMin = MIN_STREAM_VOLUME[mPublicStreamType];
- mIndexMax = MAX_STREAM_VOLUME[mPublicStreamType];
+
+ if (replaceStreamBtSco()) {
+ mIndexMin = mStreamStates[mPublicStreamType].getMinIndex() / 10;
+ mIndexMax = mStreamStates[mPublicStreamType].getMaxIndex() / 10;
+ } else {
+ mIndexMin = MIN_STREAM_VOLUME[mPublicStreamType];
+ mIndexMax = MAX_STREAM_VOLUME[mPublicStreamType];
+ }
} else if (!avg.getAudioAttributes().isEmpty()) {
mIndexMin = AudioSystem.getMinVolumeIndexForAttributes(mAudioAttributes);
mIndexMax = AudioSystem.getMaxVolumeIndexForAttributes(mAudioAttributes);
@@ -8203,7 +8334,7 @@
*/
private boolean isVssMuteBijective(int stream) {
return isStreamAffectedByMute(stream)
- && (getMinIndex() == (mStreamStates[stream].mIndexMin + 5) / 10)
+ && (getMinIndex() == (mStreamStates[stream].getMinIndex() + 5) / 10)
&& (getMinIndex() == 0 || isCallStream(stream));
}
@@ -8248,6 +8379,8 @@
}
return;
}
+
+ float stepFactor = mStreamStates[mPublicStreamType].getIndexStepFactor();
switch (direction) {
case AudioManager.ADJUST_TOGGLE_MUTE: {
// Note: If muted by volume 0, unmute will restore volume 0.
@@ -8268,7 +8401,8 @@
break;
case AudioManager.ADJUST_RAISE:
// As for stream, RAISE during mute will increment the index
- setVolumeIndex(Math.min(previousIndex + 1, mIndexMax), device, flags);
+ setVolumeIndex(Math.min((int) ((previousIndex + 1) * stepFactor),
+ mIndexMax), device, flags);
break;
case AudioManager.ADJUST_LOWER:
// For stream, ADJUST_LOWER on a muted VSS is a no-op
@@ -8277,7 +8411,8 @@
if (isMuted() && previousIndex != 0) {
mute(false);
} else {
- int newIndex = Math.max(previousIndex - 1, mIndexMin);
+ int newIndex = Math.max((int) ((previousIndex - 1) * stepFactor),
+ mIndexMin);
setVolumeIndex(newIndex, device, flags);
}
break;
@@ -8343,9 +8478,20 @@
if (mHasValidStreamType && isVssMuteBijective(mPublicStreamType)
&& mStreamStates[mPublicStreamType].isFullyMuted()) {
index = 0;
- } else if (mPublicStreamType == AudioSystem.STREAM_BLUETOOTH_SCO && index == 0) {
+ } else if (isStreamBluetoothSco(mPublicStreamType) && index == 0) {
index = 1;
}
+
+ if (replaceStreamBtSco()) {
+ index = (int) (mIndexMin + (index - mIndexMin)
+ / mStreamStates[mPublicStreamType].getIndexStepFactor());
+ }
+
+ if (DEBUG_VOL) {
+ Log.d(TAG, "setVolumeIndexInt(" + mAudioVolumeGroup.getId() + ", " + index + ", "
+ + device + ")");
+ }
+
// Set the volume index
mAudioSystem.setVolumeIndexForAttributes(mAudioAttributes, index, device);
}
@@ -8611,7 +8757,6 @@
}
}
-
// NOTE: Locking order for synchronized objects related to volume or ringer mode management:
// 1 mScoclient OR mSafeMediaVolumeState
// 2 mSetModeLock
@@ -8625,6 +8770,17 @@
private int mIndexMinNoPerm;
private int mIndexMax;
+ /**
+ * Variable used to determine the size of an incremental step when calling the
+ * adjustStreamVolume methods with raise/lower adjustments. This can change dynamically
+ * for some streams.
+ *
+ * <p>STREAM_VOICE_CALL has a different step value when is streaming on a SCO device.
+ * Internally we are using the same volume range but through the step factor we force the
+ * number of UI volume steps.
+ */
+ private float mIndexStepFactor = 1.f;
+
private boolean mIsMuted = false;
private boolean mIsMutedInternally = false;
private String mVolumeIndexSettingName;
@@ -8666,10 +8822,10 @@
mStreamType = streamType;
mIndexMin = MIN_STREAM_VOLUME[streamType] * 10;
- mIndexMinNoPerm = mIndexMin; // may be overwritten later in updateNoPermMinIndex()
mIndexMax = MAX_STREAM_VOLUME[streamType] * 10;
+
final int status = AudioSystem.initStreamVolume(
- streamType, mIndexMin / 10, mIndexMax / 10);
+ streamType, MIN_STREAM_VOLUME[streamType], MAX_STREAM_VOLUME[streamType]);
if (status != AudioSystem.AUDIO_STATUS_OK) {
sLifecycleLogger.enqueue(new EventLogger.StringEvent(
"VSS() stream:" + streamType + " initStreamVolume=" + status)
@@ -8678,6 +8834,9 @@
"VSS()" /*obj*/, 2 * INDICATE_SYSTEM_READY_RETRY_DELAY_MS);
}
+ updateIndexFactors();
+ mIndexMinNoPerm = mIndexMin; // may be overwritten later in updateNoPermMinIndex()
+
readSettings();
mVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, mStreamType);
@@ -8701,6 +8860,38 @@
mStreamDevicesChangedOptions = streamDevicesChangedOptions.toBundle();
}
+ public void updateIndexFactors() {
+ if (!replaceStreamBtSco()) {
+ return;
+ }
+
+ synchronized (this) {
+ if (mStreamType == AudioSystem.STREAM_VOICE_CALL) {
+ if (MAX_STREAM_VOLUME[AudioSystem.STREAM_BLUETOOTH_SCO]
+ > MAX_STREAM_VOLUME[mStreamType]) {
+ mIndexMax = MAX_STREAM_VOLUME[AudioSystem.STREAM_BLUETOOTH_SCO] * 10;
+ }
+
+ // SCO devices have a different min index
+ if (isStreamBluetoothSco(mStreamType)) {
+ mIndexMin = MIN_STREAM_VOLUME[AudioSystem.STREAM_BLUETOOTH_SCO] * 10;
+ mIndexStepFactor = 1.f;
+ } else {
+ mIndexMin = MIN_STREAM_VOLUME[AudioSystem.STREAM_VOICE_CALL] * 10;
+ mIndexStepFactor = (float) (mIndexMax - mIndexMin) / (float) (
+ MAX_STREAM_VOLUME[mStreamType] * 10
+ - MIN_STREAM_VOLUME[mStreamType] * 10);
+ }
+
+ if (mVolumeGroupState != null) {
+ mVolumeGroupState.mIndexMin = mIndexMin;
+ }
+
+ mIndexMinNoPerm = mIndexMin;
+ }
+ }
+ }
+
/**
* Associate a {@link volumeGroupState} on the {@link VolumeStreamState}.
* <p> It helps to synchronize the index, mute attributes on the maching
@@ -8713,6 +8904,11 @@
mVolumeGroupState.setSettingName(mVolumeIndexSettingName);
}
}
+
+ public float getIndexStepFactor() {
+ return mIndexStepFactor;
+ }
+
/**
* Update the minimum index that can be used without MODIFY_AUDIO_SETTINGS permission
* @param index minimum index expressed in "UI units", i.e. no 10x factor
@@ -8871,15 +9067,20 @@
}
}
+ @GuardedBy("VolumeStreamState.class")
private void setStreamVolumeIndex(int index, int device) {
// Only set audio policy BT SCO stream volume to 0 when the stream is actually muted.
// This allows RX path muting by the audio HAL only when explicitly muted but not when
// index is just set to 0 to repect BT requirements
- if (mStreamType == AudioSystem.STREAM_BLUETOOTH_SCO && index == 0
- && !isFullyMuted()) {
+ if (isStreamBluetoothSco(mStreamType) && index == 0 && !isFullyMuted()) {
index = 1;
}
+ if (replaceStreamBtSco()) {
+ index = (int) (mIndexMin + (index * 10 - mIndexMin) / getIndexStepFactor() + 5)
+ / 10;
+ }
+
if (DEBUG_VOL) {
Log.d(TAG, "setStreamVolumeIndexAS(" + mStreamType + ", " + index + ", " + device
+ ")");
@@ -8888,6 +9089,7 @@
}
// must be called while synchronized VolumeStreamState.class
+ @GuardedBy("VolumeStreamState.class")
/*package*/ void applyDeviceVolume_syncVSS(int device) {
int index;
if (isFullyMuted()) {
@@ -9054,8 +9256,19 @@
mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index);
mVolumeChanged.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE,
oldIndex);
+ int extraStreamType = mStreamType;
+ // TODO: remove this when deprecating STREAM_BLUETOOTH_SCO
+ if (isStreamBluetoothSco(mStreamType)) {
+ mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
+ AudioSystem.STREAM_BLUETOOTH_SCO);
+ extraStreamType = AudioSystem.STREAM_BLUETOOTH_SCO;
+ } else {
+ mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
+ mStreamType);
+ }
mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS,
mStreamVolumeAlias[mStreamType]);
+
if (mStreamType == mStreamVolumeAlias[mStreamType]) {
String aliasStreamIndexesString = "";
if (!aliasStreamIndexes.isEmpty()) {
@@ -9063,9 +9276,21 @@
" aliased streams: " + aliasStreamIndexes;
}
AudioService.sVolumeLogger.enqueue(new VolChangedBroadcastEvent(
- mStreamType, aliasStreamIndexesString, index, oldIndex));
+ extraStreamType, aliasStreamIndexesString, index, oldIndex));
+ if (extraStreamType != mStreamType) {
+ AudioService.sVolumeLogger.enqueue(new VolChangedBroadcastEvent(
+ mStreamType, aliasStreamIndexesString, index, oldIndex));
+ }
}
sendBroadcastToAll(mVolumeChanged, mVolumeChangedOptions);
+ if (extraStreamType != mStreamType) {
+ // send multiple intents in case we merged voice call and bt sco streams
+ mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
+ mStreamType);
+ // do not use the options in thid case which could discard
+ // the previous intent
+ sendBroadcastToAll(mVolumeChanged, null);
+ }
}
}
}
@@ -9091,8 +9316,8 @@
index = mIndexMap.get(AudioSystem.DEVICE_OUT_DEFAULT);
}
final VolumeInfo vi = new VolumeInfo.Builder(mStreamType)
- .setMinVolumeIndex(mIndexMin)
- .setMaxVolumeIndex(mIndexMax)
+ .setMinVolumeIndex(getMinIndex())
+ .setMaxVolumeIndex(getMaxIndex())
.setVolumeIndex(index)
.setMuted(isFullyMuted())
.build();
@@ -9281,7 +9506,7 @@
public void doMute() {
synchronized (VolumeStreamState.class) {
// If associated to volume group, update group cache
- updateVolumeGroupIndex(getDeviceForStream(mStreamType), /* forceMuteState= */ true);
+ updateVolumeGroupIndex(getDeviceForStream(mStreamType), /* forceMuteState= */true);
// Set the new mute volume. This propagates the values to
// the audio system, otherwise the volume won't be changed
@@ -9845,6 +10070,10 @@
onUpdateContextualVolumes();
break;
+ case MSG_SCO_DEVICE_ACTIVE_UPDATE:
+ onUpdateScoDeviceActive(msg.arg1 != 0);
+ break;
+
case MusicFxHelper.MSG_EFFECT_CLIENT_GONE:
mMusicFxHelper.handleMessage(msg);
break;
@@ -12258,6 +12487,11 @@
if (mController == null)
return;
try {
+ // TODO: remove this when deprecating STREAM_BLUETOOTH_SCO
+ if (isStreamBluetoothSco(streamType)) {
+ // TODO: notify both sco and voice_call about volume changes
+ streamType = AudioSystem.STREAM_BLUETOOTH_SCO;
+ }
mController.volumeChanged(streamType, flags);
} catch (RemoteException e) {
Log.w(TAG, "Error calling volumeChanged", e);
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 8e8a037..8ec835b 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -21,6 +21,7 @@
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.Authenticators;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_NO_AUTHENTICATION;
+import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE;
@@ -41,6 +42,7 @@
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.BiometricStateListener;
import android.hardware.biometrics.Flags;
import android.hardware.biometrics.IBiometricAuthenticator;
import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
@@ -54,8 +56,12 @@
import android.hardware.biometrics.PromptInfo;
import android.hardware.biometrics.SensorPropertiesInternal;
import android.hardware.camera2.CameraManager;
+import android.hardware.face.FaceManager;
+import android.hardware.face.FaceSensorPropertiesInternal;
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
import android.hardware.security.keymint.HardwareAuthenticatorType;
import android.net.Uri;
import android.os.Binder;
@@ -234,6 +240,8 @@
private static final boolean DEFAULT_APP_ENABLED = true;
private static final boolean DEFAULT_ALWAYS_REQUIRE_CONFIRMATION = false;
private static final boolean DEFAULT_MANDATORY_BIOMETRICS_STATUS = false;
+ private static final boolean DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS =
+ true;
// Some devices that shipped before S already have face-specific settings. Instead of
// migrating, which is complicated, let's just keep using the existing settings.
@@ -256,6 +264,8 @@
Settings.Secure.getUriFor(Settings.Secure.BIOMETRIC_APP_ENABLED);
private final Uri MANDATORY_BIOMETRICS_ENABLED =
Settings.Secure.getUriFor(Settings.Secure.MANDATORY_BIOMETRICS);
+ private final Uri MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED = Settings.Secure.getUriFor(
+ Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED);
private final ContentResolver mContentResolver;
private final List<BiometricService.EnabledOnKeyguardCallback> mCallbacks;
@@ -264,6 +274,12 @@
private final Map<Integer, Boolean> mBiometricEnabledForApps = new HashMap<>();
private final Map<Integer, Boolean> mFaceAlwaysRequireConfirmation = new HashMap<>();
private final Map<Integer, Boolean> mMandatoryBiometricsEnabled = new HashMap<>();
+ private final Map<Integer, Boolean> mMandatoryBiometricsRequirementsSatisfied =
+ new HashMap<>();
+ private final Map<Integer, Boolean> mFingerprintEnrolledForUser =
+ new HashMap<>();
+ private final Map<Integer, Boolean> mFaceEnrolledForUser =
+ new HashMap<>();
/**
* Creates a content observer.
@@ -288,7 +304,13 @@
mMandatoryBiometricsEnabled.put(context.getUserId(), Settings.Secure.getIntForUser(
mContentResolver, Settings.Secure.MANDATORY_BIOMETRICS,
DEFAULT_MANDATORY_BIOMETRICS_STATUS ? 1 : 0, context.getUserId()) != 0);
+ mMandatoryBiometricsRequirementsSatisfied.put(context.getUserId(),
+ Settings.Secure.getIntForUser(mContentResolver,
+ Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED,
+ DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS ? 1 : 0,
+ context.getUserId()) != 0);
+ addBiometricListenersForMandatoryBiometrics(context);
updateContentObserver();
}
@@ -322,6 +344,10 @@
false /* notifyForDescendants */,
this /* observer */,
UserHandle.USER_ALL);
+ mContentResolver.registerContentObserver(MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED,
+ false /* notifyForDescendants */,
+ this /* observer */,
+ UserHandle.USER_ALL);
}
@Override
@@ -370,6 +396,13 @@
Settings.Secure.MANDATORY_BIOMETRICS,
DEFAULT_MANDATORY_BIOMETRICS_STATUS ? 1 : 0 /* default */,
userId) != 0);
+ } else if (MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED.equals(uri)) {
+ mMandatoryBiometricsRequirementsSatisfied.put(userId, Settings.Secure.getIntForUser(
+ mContentResolver,
+ Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED,
+ DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS
+ ? 1 : 0 /* default */,
+ userId) != 0);
}
}
@@ -411,9 +444,15 @@
}
}
- public boolean getMandatoryBiometricsEnabledForUser(int userId) {
+ public boolean getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(int userId) {
return mMandatoryBiometricsEnabled.getOrDefault(userId,
- DEFAULT_MANDATORY_BIOMETRICS_STATUS);
+ DEFAULT_MANDATORY_BIOMETRICS_STATUS)
+ && mMandatoryBiometricsRequirementsSatisfied.getOrDefault(userId,
+ DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS)
+ && mBiometricEnabledForApps.getOrDefault(userId, DEFAULT_APP_ENABLED)
+ && getEnabledForApps(userId)
+ && (mFingerprintEnrolledForUser.getOrDefault(userId, false /* default */)
+ || mFaceEnrolledForUser.getOrDefault(userId, false /* default */));
}
void notifyEnabledOnKeyguardCallbacks(int userId) {
@@ -424,6 +463,79 @@
userId);
}
}
+
+ private void addBiometricListenersForMandatoryBiometrics(Context context) {
+ final FingerprintManager fingerprintManager = context.getSystemService(
+ FingerprintManager.class);
+ final FaceManager faceManager = context.getSystemService(FaceManager.class);
+ if (fingerprintManager != null) {
+ fingerprintManager.addAuthenticatorsRegisteredCallback(
+ new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
+ @Override
+ public void onAllAuthenticatorsRegistered(
+ List<FingerprintSensorPropertiesInternal> list) {
+ if (list == null || list.isEmpty()) {
+ Slog.d(TAG, "No fingerprint authenticators registered.");
+ return;
+ }
+ final FingerprintSensorPropertiesInternal
+ fingerprintSensorProperties = list.get(0);
+ if (fingerprintSensorProperties.sensorStrength
+ == STRENGTH_STRONG) {
+ fingerprintManager.registerBiometricStateListener(
+ new BiometricStateListener() {
+ @Override
+ public void onEnrollmentsChanged(
+ int userId,
+ int sensorId,
+ boolean hasEnrollments
+ ) {
+ if (sensorId == fingerprintSensorProperties
+ .sensorId) {
+ mFingerprintEnrolledForUser.put(userId,
+ hasEnrollments);
+ }
+ }
+ });
+ }
+ }
+ });
+ }
+ if (faceManager != null) {
+ faceManager.addAuthenticatorsRegisteredCallback(
+ new IFaceAuthenticatorsRegisteredCallback.Stub() {
+ @Override
+ public void onAllAuthenticatorsRegistered(
+ List<FaceSensorPropertiesInternal> list) {
+ if (list == null || list.isEmpty()) {
+ Slog.d(TAG, "No face authenticators registered.");
+ return;
+ }
+ final FaceSensorPropertiesInternal
+ faceSensorPropertiesInternal = list.get(0);
+ if (faceSensorPropertiesInternal.sensorStrength
+ == STRENGTH_STRONG) {
+ faceManager.registerBiometricStateListener(
+ new BiometricStateListener() {
+ @Override
+ public void onEnrollmentsChanged(
+ int userId,
+ int sensorId,
+ boolean hasEnrollments
+ ) {
+ if (sensorId
+ == faceSensorPropertiesInternal
+ .sensorId) {
+ mFaceEnrolledForUser.put(userId,
+ hasEnrollments);
+ }
+ }
+ });
+ }
+ }
+ });
+ }
+ }
}
final class EnabledOnKeyguardCallback implements IBinder.DeathRecipient {
diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
index b9e6563..0bd22f3 100644
--- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java
+++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
@@ -112,8 +112,8 @@
== BiometricManager.Authenticators.MANDATORY_BIOMETRICS;
if (dropCredentialFallback(promptInfo.getAuthenticators(),
- settingObserver.getMandatoryBiometricsEnabledForUser(userId),
- trustManager)) {
+ settingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(
+ userId), trustManager)) {
promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
promptInfo.setNegativeButtonText(context.getString(R.string.cancel));
}
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 73aa14b..78f7187 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -684,7 +684,8 @@
if (clipboard == null) {
return null;
}
- showAccessNotificationLocked(pkg, intendingUid, intendingUserId, clipboard);
+ showAccessNotificationLocked(
+ pkg, intendingUid, intendingUserId, clipboard, deviceId);
notifyTextClassifierLocked(clipboard, pkg, intendingUid);
if (clipboard.primaryClip != null) {
scheduleAutoClear(userId, intendingUid, intendingDeviceId);
@@ -1438,7 +1439,7 @@
*/
@GuardedBy("mLock")
private void showAccessNotificationLocked(String callingPackage, int uid, @UserIdInt int userId,
- Clipboard clipboard) {
+ Clipboard clipboard, int accessDeviceId) {
if (clipboard.primaryClip == null) {
return;
}
@@ -1477,7 +1478,7 @@
return;
}
- final ArraySet<Context> toastContexts = getToastContexts(clipboard);
+ final ArraySet<Context> toastContexts = getToastContexts(clipboard, accessDeviceId);
Binder.withCleanCallingIdentity(() -> {
try {
CharSequence callingAppLabel = mPm.getApplicationLabel(
@@ -1516,40 +1517,55 @@
* If the clipboard is for a VirtualDevice, we attempt to return the single DisplayContext for
* the focused VirtualDisplay for that device, but might need to return the contexts for
* multiple displays if the VirtualDevice has several but none of them were focused.
+ *
+ * If the clipboard is NOT for a VirtualDevice, but it's being accessed from a VirtualDevice,
+ * this means that the clipboard is shared between the default and that device. In this case we
+ * need to show a toast in both places.
*/
- private ArraySet<Context> getToastContexts(Clipboard clipboard) throws IllegalStateException {
+ private ArraySet<Context> getToastContexts(Clipboard clipboard, int accessDeviceId)
+ throws IllegalStateException {
ArraySet<Context> contexts = new ArraySet<>();
-
- if (mVdmInternal != null && clipboard.deviceId != DEVICE_ID_DEFAULT) {
- DisplayManager displayManager = getContext().getSystemService(DisplayManager.class);
-
- int topFocusedDisplayId = mWm.getTopFocusedDisplayId();
- ArraySet<Integer> displayIds = mVdmInternal.getDisplayIdsForDevice(clipboard.deviceId);
-
- if (displayIds.contains(topFocusedDisplayId)) {
- Display display = displayManager.getDisplay(topFocusedDisplayId);
- if (display != null) {
- contexts.add(getContext().createDisplayContext(display));
- return contexts;
- }
- }
-
- for (int i = 0; i < displayIds.size(); i++) {
- Display display = displayManager.getDisplay(displayIds.valueAt(i));
- if (display != null) {
- contexts.add(getContext().createDisplayContext(display));
- }
- }
- if (!contexts.isEmpty()) {
- return contexts;
- }
- Slog.e(TAG, "getToastContexts Couldn't find any VirtualDisplays for VirtualDevice "
- + clipboard.deviceId);
- // Since we couldn't find any VirtualDisplays to use at all, just fall through to using
- // the default display below.
+ if (clipboard.deviceId == DEVICE_ID_DEFAULT || accessDeviceId == DEVICE_ID_DEFAULT) {
+ // Always show the toast on the default display when the default clipboard is accessed -
+ // also when the clipboard is shared with a virtual device and accessed from there.
+ // Same when any clipboard is accessed from the default device.
+ contexts.add(getContext());
}
- contexts.add(getContext());
+ if ((accessDeviceId == DEVICE_ID_DEFAULT && clipboard.deviceId == DEVICE_ID_DEFAULT)
+ || mVdmInternal == null) {
+ // No virtual devices involved.
+ return contexts;
+ }
+
+ // At this point the clipboard is either accessed from a virtual device, or it is a virtual
+ // device clipboard, so show a toast on the relevant virtual display(s).
+ DisplayManager displayManager = getContext().getSystemService(DisplayManager.class);
+ ArraySet<Integer> displayIds = mVdmInternal.getDisplayIdsForDevice(accessDeviceId);
+ int topFocusedDisplayId = mWm.getTopFocusedDisplayId();
+
+ if (displayIds.contains(topFocusedDisplayId)) {
+ Display display = displayManager.getDisplay(topFocusedDisplayId);
+ if (display != null) {
+ contexts.add(getContext().createDisplayContext(display));
+ return contexts;
+ }
+ }
+
+ for (int i = 0; i < displayIds.size(); i++) {
+ Display display = displayManager.getDisplay(displayIds.valueAt(i));
+ if (display != null) {
+ contexts.add(getContext().createDisplayContext(display));
+ }
+ }
+ if (contexts.isEmpty()) {
+ Slog.e(TAG, "getToastContexts Couldn't find any VirtualDisplays for VirtualDevice "
+ + accessDeviceId);
+ // Since we couldn't find any VirtualDisplays to use at all, just fall through to using
+ // the default display below.
+ contexts.add(getContext());
+ }
+
return contexts;
}
diff --git a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
index 3c3cfe6..256905d 100644
--- a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
+++ b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java
@@ -54,7 +54,9 @@
// The maximum number of times we send <Give Device Power Status> before we give up.
// We wait up to RESPONSE_TIMEOUT_MS * LOOP_COUNTER_MAX = 20 seconds.
- private static final int LOOP_COUNTER_MAX = 10;
+ // Every 3 timeouts we send a <Text View On> in case the TV missed it and ignored it.
+ @VisibleForTesting
+ static final int LOOP_COUNTER_MAX = 10;
private final int mTargetAddress;
private final boolean mIsCec20;
@@ -181,6 +183,7 @@
if (cmd.getOpcode() == Constants.MESSAGE_REPORT_POWER_STATUS) {
int status = cmd.getParams()[0];
if (status == HdmiControlManager.POWER_STATUS_ON) {
+ HdmiLogger.debug("TV's power status is on. Action finished successfully");
// If the device is still the active source, send the <Active Source> message
// again.
maySendActiveSource();
@@ -199,6 +202,12 @@
switch (state) {
case STATE_WAITING_FOR_REPORT_POWER_STATUS:
if (mPowerStatusCounter++ < LOOP_COUNTER_MAX) {
+ if (mPowerStatusCounter % 3 == 0) {
+ HdmiLogger.debug("Retry sending <Text View On> in case the TV "
+ + "missed the message.");
+ sendCommand(HdmiCecMessageBuilder.buildTextViewOn(getSourceAddress(),
+ mTargetAddress));
+ }
queryDevicePowerStatus();
addTimer(mState, HdmiConfig.TIMEOUT_MS);
} else {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index c82e5be..13209d8 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -27,6 +27,7 @@
import android.os.IBinder;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.inputmethod.IAccessibilityInputMethodSession;
import com.android.internal.inputmethod.InlineSuggestionsRequestCallback;
@@ -89,6 +90,8 @@
* @param userId the user ID to be queried
* @return a list of {@link InputMethodInfo}. VR-only IMEs are already excluded
*/
+ @ImfLockFree
+ @NonNull
public abstract List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId);
/**
@@ -97,9 +100,24 @@
* @param userId the user ID to be queried
* @return a list of {@link InputMethodInfo} that are enabled for {@code userId}
*/
+ @ImfLockFree
+ @NonNull
public abstract List<InputMethodInfo> getEnabledInputMethodListAsUser(@UserIdInt int userId);
/**
+ * Returns the list of installed input methods that are enabled for the specified user.
+ *
+ * @param imiId IME ID to be queried about
+ * @param allowsImplicitlyEnabledSubtypes {@code true} to return the implicitly enabled subtypes
+ * @param userId the user ID to be queried about
+ * @return a list of {@link InputMethodSubtype} that are enabled for {@code userId}
+ */
+ @ImfLockFree
+ @NonNull
+ public abstract List<InputMethodSubtype> getEnabledInputMethodSubtypeListAsUser(
+ String imiId, boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId);
+
+ /**
* Called by the Autofill Frameworks to request an {@link InlineSuggestionsRequest} from
* the input method.
*
@@ -301,17 +319,29 @@
int originatingDisplayId) {
}
+ @ImfLockFree
+ @NonNull
@Override
public List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId) {
return Collections.emptyList();
}
+ @ImfLockFree
+ @NonNull
@Override
public List<InputMethodInfo> getEnabledInputMethodListAsUser(
@UserIdInt int userId) {
return Collections.emptyList();
}
+ @ImfLockFree
+ @NonNull
+ @Override
+ public List<InputMethodSubtype> getEnabledInputMethodSubtypeListAsUser(String imiId,
+ boolean allowsImplicitlyEnabledSubtypes, int userId) {
+ return Collections.emptyList();
+ }
+
@Override
public void onCreateInlineSuggestionsRequest(@UserIdInt int userId,
InlineSuggestionsRequestInfo requestInfo,
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index a9e9dcf..a9723cc 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1469,9 +1469,8 @@
mContext.enforceCallingOrSelfPermission(
Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
}
- synchronized (ImfLock.class) {
- return queryDefaultInputMethodForUserIdLocked(userId);
- }
+ final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
+ return settings.getMethodMap().get(settings.getSelectedInputMethod());
}
@BinderThread
@@ -1483,20 +1482,16 @@
mContext.enforceCallingOrSelfPermission(
Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
}
- synchronized (ImfLock.class) {
- final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId,
- mCurrentUserId, null);
- if (resolvedUserIds.length != 1) {
- return InputMethodInfoSafeList.empty();
- }
- final int callingUid = Binder.getCallingUid();
- final long ident = Binder.clearCallingIdentity();
- try {
- return InputMethodInfoSafeList.create(getInputMethodListLocked(
- resolvedUserIds[0], directBootAwareness, callingUid));
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
+ if (!mUserManagerInternal.exists(userId)) {
+ return InputMethodInfoSafeList.empty();
+ }
+ final int callingUid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return InputMethodInfoSafeList.create(getInputMethodListInternal(
+ userId, directBootAwareness, callingUid));
+ } finally {
+ Binder.restoreCallingIdentity(ident);
}
}
@@ -1508,20 +1503,16 @@
mContext.enforceCallingOrSelfPermission(
Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
}
- synchronized (ImfLock.class) {
- final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId,
- mCurrentUserId, null);
- if (resolvedUserIds.length != 1) {
- return InputMethodInfoSafeList.empty();
- }
- final int callingUid = Binder.getCallingUid();
- final long ident = Binder.clearCallingIdentity();
- try {
- return InputMethodInfoSafeList.create(
- getEnabledInputMethodListLocked(resolvedUserIds[0], callingUid));
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
+ if (!mUserManagerInternal.exists(userId)) {
+ return InputMethodInfoSafeList.empty();
+ }
+ final int callingUid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return InputMethodInfoSafeList.create(
+ getEnabledInputMethodListInternal(userId, callingUid));
+ } finally {
+ Binder.restoreCallingIdentity(ident);
}
}
@@ -1534,20 +1525,15 @@
mContext.enforceCallingOrSelfPermission(
Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
}
- synchronized (ImfLock.class) {
- final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId,
- mCurrentUserId, null);
- if (resolvedUserIds.length != 1) {
- return Collections.emptyList();
- }
- final int callingUid = Binder.getCallingUid();
- final long ident = Binder.clearCallingIdentity();
- try {
- return getInputMethodListLocked(
- resolvedUserIds[0], directBootAwareness, callingUid);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
+ if (!mUserManagerInternal.exists(userId)) {
+ return Collections.emptyList();
+ }
+ final int callingUid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return getInputMethodListInternal(userId, directBootAwareness, callingUid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
}
}
@@ -1559,19 +1545,15 @@
mContext.enforceCallingOrSelfPermission(
Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
}
- synchronized (ImfLock.class) {
- final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId,
- mCurrentUserId, null);
- if (resolvedUserIds.length != 1) {
- return Collections.emptyList();
- }
- final int callingUid = Binder.getCallingUid();
- final long ident = Binder.clearCallingIdentity();
- try {
- return getEnabledInputMethodListLocked(resolvedUserIds[0], callingUid);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
+ if (!mUserManagerInternal.exists(userId)) {
+ return Collections.emptyList();
+ }
+ final int callingUid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return getEnabledInputMethodListInternal(userId, callingUid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
}
}
@@ -1615,8 +1597,7 @@
return true;
}
- @GuardedBy("ImfLock.class")
- private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId,
+ private List<InputMethodInfo> getInputMethodListInternal(@UserIdInt int userId,
@DirectBootAwareness int directBootAwareness, int callingUid) {
final InputMethodSettings settings;
if (directBootAwareness == DirectBootAwareness.AUTO) {
@@ -1635,8 +1616,7 @@
return methodList;
}
- @GuardedBy("ImfLock.class")
- private List<InputMethodInfo> getEnabledInputMethodListLocked(@UserIdInt int userId,
+ private List<InputMethodInfo> getEnabledInputMethodListInternal(@UserIdInt int userId,
int callingUid) {
final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
final ArrayList<InputMethodInfo> methodList = settings.getEnabledInputMethodList();
@@ -1663,20 +1643,17 @@
Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
}
- synchronized (ImfLock.class) {
- final int callingUid = Binder.getCallingUid();
- final long ident = Binder.clearCallingIdentity();
- try {
- return getEnabledInputMethodSubtypeListLocked(imiId,
- allowsImplicitlyEnabledSubtypes, userId, callingUid);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
+ final int callingUid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return getEnabledInputMethodSubtypeListInternal(imiId,
+ allowsImplicitlyEnabledSubtypes, userId, callingUid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
}
}
- @GuardedBy("ImfLock.class")
- private List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(String imiId,
+ private List<InputMethodSubtype> getEnabledInputMethodSubtypeListInternal(String imiId,
boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId, int callingUid) {
final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
final InputMethodInfo imi = settings.getMethodMap().get(imiId);
@@ -5712,17 +5689,6 @@
}
}
- /**
- * Returns the default {@link InputMethodInfo} for the specific userId.
- *
- * @param userId user ID to query
- */
- @GuardedBy("ImfLock.class")
- private InputMethodInfo queryDefaultInputMethodForUserIdLocked(@UserIdInt int userId) {
- final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
- return settings.getMethodMap().get(settings.getSelectedInputMethod());
- }
-
@GuardedBy("ImfLock.class")
private boolean switchToInputMethodLocked(@NonNull String imeId, int subtypeId,
@UserIdInt int userId) {
@@ -5859,19 +5825,27 @@
mHandler.obtainMessage(MSG_HIDE_ALL_INPUT_METHODS, reason).sendToTarget();
}
+ @ImfLockFree
+ @NonNull
@Override
public List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId) {
- synchronized (ImfLock.class) {
- return getInputMethodListLocked(userId, DirectBootAwareness.AUTO,
- Process.SYSTEM_UID);
- }
+ return getInputMethodListInternal(userId, DirectBootAwareness.AUTO, Process.SYSTEM_UID);
}
+ @ImfLockFree
+ @NonNull
@Override
public List<InputMethodInfo> getEnabledInputMethodListAsUser(@UserIdInt int userId) {
- synchronized (ImfLock.class) {
- return getEnabledInputMethodListLocked(userId, Process.SYSTEM_UID);
- }
+ return getEnabledInputMethodListInternal(userId, Process.SYSTEM_UID);
+ }
+
+ @ImfLockFree
+ @NonNull
+ @Override
+ public List<InputMethodSubtype> getEnabledInputMethodSubtypeListAsUser(
+ String imiId, boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) {
+ return getEnabledInputMethodSubtypeListInternal(imiId, allowsImplicitlyEnabledSubtypes,
+ userId, Process.SYSTEM_UID);
}
@Override
@@ -6617,28 +6591,29 @@
break;
}
}
+ final int[] userIds;
synchronized (ImfLock.class) {
- final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
- mCurrentUserId, shellCommand.getErrPrintWriter());
- try (PrintWriter pr = shellCommand.getOutPrintWriter()) {
- for (int userId : userIds) {
- final List<InputMethodInfo> methods = all
- ? getInputMethodListLocked(
- userId, DirectBootAwareness.AUTO, Process.SHELL_UID)
- : getEnabledInputMethodListLocked(userId, Process.SHELL_UID);
- if (userIds.length > 1) {
- pr.print("User #");
- pr.print(userId);
+ userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, mCurrentUserId,
+ shellCommand.getErrPrintWriter());
+ }
+ try (PrintWriter pr = shellCommand.getOutPrintWriter()) {
+ for (int userId : userIds) {
+ final List<InputMethodInfo> methods = all
+ ? getInputMethodListInternal(
+ userId, DirectBootAwareness.AUTO, Process.SHELL_UID)
+ : getEnabledInputMethodListInternal(userId, Process.SHELL_UID);
+ if (userIds.length > 1) {
+ pr.print("User #");
+ pr.print(userId);
+ pr.println(":");
+ }
+ for (InputMethodInfo info : methods) {
+ if (brief) {
+ pr.println(info.getId());
+ } else {
+ pr.print(info.getId());
pr.println(":");
- }
- for (InputMethodInfo info : methods) {
- if (brief) {
- pr.println(info.getId());
- } else {
- pr.print(info.getId());
- pr.println(":");
- info.dump(pr::println, " ");
- }
+ info.dump(pr::println, " ");
}
}
}
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
index 3d0b079..741513c 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerService.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -616,9 +616,10 @@
LocaleConfig resLocaleConfig = null;
try {
resLocaleConfig = LocaleConfig.fromContextIgnoringOverride(
- mContext.createPackageContext(appPackageName, 0));
+ mContext.createPackageContextAsUser(appPackageName, /* flags= */ 0,
+ UserHandle.of(userId)));
} catch (PackageManager.NameNotFoundException e) {
- Slog.e(TAG, "Unknown package name " + appPackageName);
+ Slog.e(TAG, "Unknown package name " + appPackageName + " for user " + userId);
return;
}
final File file = getXmlFileNameForUser(appPackageName, userId);
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 7de1045..3f4a9bb 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -297,7 +297,10 @@
}
public boolean isExpired() {
- return mTimestamp + ContextHubTransactionManager.RELIABLE_MESSAGE_TIMEOUT.toNanos()
+ return mTimestamp
+ + ContextHubTransactionManager
+ .RELIABLE_MESSAGE_DUPLICATE_DETECTION_TIMEOUT
+ .toNanos()
< SystemClock.elapsedRealtimeNanos();
}
}
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTestModeManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTestModeManager.java
index f2714db..2bb3be6 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTestModeManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTestModeManager.java
@@ -17,10 +17,12 @@
package com.android.server.location.contexthub;
import android.chre.flags.Flags;
+import android.hardware.location.ContextHubTransaction;
import android.hardware.location.NanoAppMessage;
import android.util.Log;
-import java.util.Random;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.Callable;
/**
* A class to manage behaviors during test mode. This is used for testing.
@@ -29,32 +31,31 @@
public class ContextHubTestModeManager {
private static final String TAG = "ContextHubTestModeManager";
- /** Probability of duplicating a message. */
- private static final double MESSAGE_DROP_PROBABILITY = 0.05;
-
- /** Probability of duplicating a message. */
- private static final double MESSAGE_DUPLICATION_PROBABILITY = 0.05;
+ private static final int DROP_MESSAGE_TO_HOST_EVENT = 0;
+ private static final int DROP_MESSAGE_TO_CONTEXT_HUB_EVENT = 1;
+ private static final int DUPLICATE_MESSAGE_TO_HOST_EVENT = 2;
+ private static final int DUPLICATE_MESSAGE_TO_CONTEXT_HUB_EVENT = 3;
+ private static final int NUMBER_OF_EVENTS = 4;
/** The number of total messages to send when the duplication event happens. */
private static final int NUM_MESSAGES_TO_DUPLICATE = 3;
- /**
- * The seed for the random number generator. This is used to make the
- * test more deterministic.
- */
- private static final long SEED = 0xDEADBEEF;
-
- private final Random mRandom = new Random(SEED);
+ /** The counter to track the number of interactions with the test mode manager. */
+ private final AtomicLong mCounter = new AtomicLong(0);
/**
* @return whether the message was handled
* @see ContextHubServiceCallback#handleNanoappMessage
*/
public boolean handleNanoappMessage(Runnable handleMessage, NanoAppMessage message) {
+ if (!message.isReliable()) {
+ return false;
+ }
+
+ long counterValue = mCounter.getAndIncrement();
if (Flags.reliableMessageDuplicateDetectionService()
- && message.isReliable()
- && mRandom.nextDouble() < MESSAGE_DUPLICATION_PROBABILITY) {
- Log.i(TAG, "[TEST MODE] Duplicating message ("
+ && counterValue % NUMBER_OF_EVENTS == DUPLICATE_MESSAGE_TO_HOST_EVENT) {
+ Log.i(TAG, "[TEST MODE] Duplicating message to host ("
+ NUM_MESSAGES_TO_DUPLICATE
+ " sends) with message sequence number: "
+ message.getMessageSequenceNumber());
@@ -63,6 +64,14 @@
}
return true;
}
+
+ if (counterValue % NUMBER_OF_EVENTS == DROP_MESSAGE_TO_HOST_EVENT) {
+ Log.i(TAG, "[TEST MODE] Dropping message to host with "
+ + "message sequence number: "
+ + message.getMessageSequenceNumber());
+ return true;
+ }
+
return false;
}
@@ -70,14 +79,39 @@
* @return whether the message was handled
* @see IContextHubWrapper#sendMessageToContextHub
*/
- public boolean sendMessageToContextHub(NanoAppMessage message) {
+ public boolean sendMessageToContextHub(Callable<Integer> sendMessage, NanoAppMessage message) {
+ if (!message.isReliable()) {
+ return false;
+ }
+
+ long counterValue = mCounter.getAndIncrement();
+ if (counterValue % NUMBER_OF_EVENTS == DUPLICATE_MESSAGE_TO_CONTEXT_HUB_EVENT) {
+ Log.i(TAG, "[TEST MODE] Duplicating message to the Context Hub ("
+ + NUM_MESSAGES_TO_DUPLICATE
+ + " sends) with message sequence number: "
+ + message.getMessageSequenceNumber());
+ for (int i = 0; i < NUM_MESSAGES_TO_DUPLICATE; ++i) {
+ try {
+ int result = sendMessage.call();
+ if (result != ContextHubTransaction.RESULT_SUCCESS) {
+ Log.e(TAG, "sendMessage returned an error: " + result);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Exception in sendMessageToContextHub: "
+ + e.getMessage());
+ }
+ }
+ return true;
+ }
+
if (Flags.reliableMessageRetrySupportService()
- && message.isReliable()
- && mRandom.nextDouble() < MESSAGE_DROP_PROBABILITY) {
- Log.i(TAG, "[TEST MODE] Dropping message with message sequence number: "
+ && counterValue % NUMBER_OF_EVENTS == DROP_MESSAGE_TO_CONTEXT_HUB_EVENT) {
+ Log.i(TAG, "[TEST MODE] Dropping message to the Context Hub with "
+ + "message sequence number: "
+ message.getMessageSequenceNumber());
return true;
}
+
return false;
}
}
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
index e6d330f8..cd69eba 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -56,6 +56,9 @@
public static final Duration RELIABLE_MESSAGE_TIMEOUT = Duration.ofSeconds(1);
+ public static final Duration RELIABLE_MESSAGE_DUPLICATE_DETECTION_TIMEOUT =
+ RELIABLE_MESSAGE_TIMEOUT.multipliedBy(3);
+
private static final int MAX_PENDING_REQUESTS = 10000;
private static final int RELIABLE_MESSAGE_MAX_NUM_RETRY = 3;
diff --git a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
index 4fc3d87..a8ad418 100644
--- a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
+++ b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
@@ -53,6 +53,7 @@
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.Callable;
/**
* @hide
@@ -659,32 +660,40 @@
@ContextHubTransaction.Result
public int sendMessageToContextHub(short hostEndpointId, int contextHubId,
- NanoAppMessage message) throws RemoteException {
+ NanoAppMessage message) {
android.hardware.contexthub.IContextHub hub = getHub();
if (hub == null) {
return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
}
- try {
- var msg = ContextHubServiceUtil.createAidlContextHubMessage(
- hostEndpointId, message);
-
- // Only process the message normally if not using test mode manager or if
- // the test mode manager call returned false as this indicates it did not
- // process the message.
- boolean useTestModeManager = Flags.reliableMessageImplementation()
- && Flags.reliableMessageTestModeBehavior()
- && mIsTestModeEnabled.get();
- if (!useTestModeManager || !mTestModeManager.sendMessageToContextHub(message)) {
+ Callable<Integer> sendMessage = () -> {
+ try {
+ var msg = ContextHubServiceUtil.createAidlContextHubMessage(
+ hostEndpointId, message);
hub.sendMessageToHub(contextHubId, msg);
+ return ContextHubTransaction.RESULT_SUCCESS;
+ } catch (RemoteException | ServiceSpecificException e) {
+ return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
+ } catch (IllegalArgumentException e) {
+ return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
}
+ };
- return ContextHubTransaction.RESULT_SUCCESS;
- } catch (RemoteException | ServiceSpecificException e) {
- return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
- } catch (IllegalArgumentException e) {
- return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
+ // Only process the message normally if not using test mode manager or if
+ // the test mode manager call returned false as this indicates it did not
+ // process the message.
+ boolean useTestModeManager = Flags.reliableMessageImplementation()
+ && Flags.reliableMessageTestModeBehavior()
+ && mIsTestModeEnabled.get();
+ if (!useTestModeManager || !mTestModeManager.sendMessageToContextHub(
+ sendMessage, message)) {
+ try {
+ return sendMessage.call();
+ } catch (Exception e) {
+ return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
+ }
}
+ return ContextHubTransaction.RESULT_SUCCESS;
}
@ContextHubTransaction.Result
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 803b125..621c090 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -702,7 +702,7 @@
}
}
- private final class BinderService extends IMediaProjectionManager.Stub {
+ final class BinderService extends IMediaProjectionManager.Stub {
BinderService(Context context) {
super(PermissionEnforcer.fromContext(context));
@@ -891,6 +891,13 @@
@Override
public void requestConsentForInvalidProjection(@NonNull IMediaProjection projection) {
requestConsentForInvalidProjection_enforcePermission();
+
+ if (android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()
+ && mKeyguardManager.isKeyguardLocked()) {
+ Slog.v(TAG, "Reusing token: Won't request consent while the keyguard is locked");
+ return;
+ }
+
synchronized (mLock) {
if (!isCurrentProjection(projection)) {
Slog.v(TAG, "Reusing token: Won't request consent again for a token that "
diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING
index e12b70f..c40608d 100644
--- a/services/core/java/com/android/server/pm/TEST_MAPPING
+++ b/services/core/java/com/android/server/pm/TEST_MAPPING
@@ -160,6 +160,9 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
+ },
+ {
+ "name": "CtsUpdateOwnershipEnforcementTestCases"
}
],
"imports": [
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 8be20b0..aaa38a3 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -149,6 +149,13 @@
CONTACTS_PERMISSIONS.add(Manifest.permission.GET_ACCOUNTS);
}
+ private static final Set<String> CALL_LOG_PERMISSIONS = new ArraySet<>();
+ static {
+ CALL_LOG_PERMISSIONS.add(Manifest.permission.READ_CALL_LOG);
+ CALL_LOG_PERMISSIONS.add(Manifest.permission.WRITE_CALL_LOG);
+ }
+
+
private static final Set<String> ALWAYS_LOCATION_PERMISSIONS = new ArraySet<>();
static {
ALWAYS_LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_FINE_LOCATION);
@@ -753,7 +760,7 @@
String contactsProviderPackage =
getDefaultProviderAuthorityPackage(ContactsContract.AUTHORITY, userId);
grantSystemFixedPermissionsToSystemPackage(pm, contactsProviderPackage, userId,
- CONTACTS_PERMISSIONS, PHONE_PERMISSIONS);
+ CONTACTS_PERMISSIONS, PHONE_PERMISSIONS, CALL_LOG_PERMISSIONS);
grantPermissionsToSystemPackage(pm, contactsProviderPackage, userId, STORAGE_PERMISSIONS);
// Device provisioning
diff --git a/services/core/java/com/android/server/policy/ModifierShortcutManager.java b/services/core/java/com/android/server/policy/ModifierShortcutManager.java
index fde23b7..9b64488 100644
--- a/services/core/java/com/android/server/policy/ModifierShortcutManager.java
+++ b/services/core/java/com/android/server/policy/ModifierShortcutManager.java
@@ -16,6 +16,7 @@
package com.android.server.policy;
+import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.role.RoleManager;
import android.content.ActivityNotFoundException;
@@ -248,31 +249,7 @@
+ " className=" + className + " shortcutChar=" + shortcutChar);
continue;
}
- ComponentName componentName = new ComponentName(packageName, className);
- try {
- mPackageManager.getActivityInfo(componentName,
- PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
- | PackageManager.MATCH_UNINSTALLED_PACKAGES);
- } catch (PackageManager.NameNotFoundException e) {
- String[] packages = mPackageManager.canonicalToCurrentPackageNames(
- new String[] { packageName });
- componentName = new ComponentName(packages[0], className);
- try {
- mPackageManager.getActivityInfo(componentName,
- PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
- | PackageManager.MATCH_UNINSTALLED_PACKAGES);
- } catch (PackageManager.NameNotFoundException e1) {
- Log.w(TAG, "Unable to add bookmark: " + packageName
- + "/" + className + " not found.");
- continue;
- }
- }
-
- intent = new Intent(Intent.ACTION_MAIN);
- intent.addCategory(Intent.CATEGORY_LAUNCHER);
- intent.setComponent(componentName);
+ intent = resolveComponentNameIntent(packageName, className);
} else if (categoryName != null) {
if (roleName != null) {
Log.w(TAG, "Cannot specify role bookmark when category is present for"
@@ -310,6 +287,32 @@
}
}
+ @Nullable
+ private Intent resolveComponentNameIntent(String packageName, String className) {
+ int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_UNINSTALLED_PACKAGES;
+ ComponentName componentName = new ComponentName(packageName, className);
+ try {
+ mPackageManager.getActivityInfo(componentName, flags);
+ } catch (PackageManager.NameNotFoundException e) {
+ String[] packages = mPackageManager.canonicalToCurrentPackageNames(
+ new String[] { packageName });
+ componentName = new ComponentName(packages[0], className);
+ try {
+ mPackageManager.getActivityInfo(componentName, flags);
+ } catch (PackageManager.NameNotFoundException e1) {
+ Log.w(TAG, "Unable to add bookmark: " + packageName
+ + "/" + className + " not found.");
+ return null;
+ }
+ }
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ intent.setComponent(componentName);
+ return intent;
+ }
+
void registerShortcutKey(long shortcutCode, IShortcutService shortcutService)
throws RemoteException {
IShortcutService service = mShortcutKeyServices.get(shortcutCode);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 400919a..516fc65 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -53,7 +53,6 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.activityTypeToString;
-import static android.app.WindowConfiguration.isFloating;
import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION;
import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE;
import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
@@ -336,7 +335,6 @@
import android.service.dreams.DreamActivity;
import android.service.voice.IVoiceInteractionSession;
import android.util.ArraySet;
-import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.Log;
import android.util.MergedConfiguration;
@@ -8648,7 +8646,14 @@
resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds);
}
- applySizeOverrideIfNeeded(newParentConfiguration, parentWindowingMode, resolvedConfig);
+ applySizeOverrideIfNeeded(
+ mDisplayContent,
+ info.applicationInfo,
+ newParentConfiguration,
+ resolvedConfig,
+ mOptOutEdgeToEdge,
+ hasFixedRotationTransform(),
+ getCompatDisplayInsets() != null);
mResolveConfigHint.resetTmpOverrides();
logAppCompatState();
@@ -8658,100 +8663,6 @@
return Rect.copyOrNull(mResolveConfigHint.mParentAppBoundsOverride);
}
- /**
- * If necessary, override configuration fields related to app bounds.
- * This will happen when the app is targeting SDK earlier than 35.
- * The insets and configuration has decoupled since SDK level 35, to make the system
- * compatible to existing apps, override the configuration with legacy metrics. In legacy
- * metrics, fields such as appBounds will exclude some of the system bar areas.
- * The override contains all potentially affected fields in Configuration, including
- * screenWidthDp, screenHeightDp, smallestScreenWidthDp, and orientation.
- * All overrides to those fields should be in this method.
- *
- * TODO: Consider integrate this with computeConfigByResolveHint()
- */
- private void applySizeOverrideIfNeeded(Configuration newParentConfiguration,
- int parentWindowingMode, Configuration inOutConfig) {
- if (mDisplayContent == null) {
- return;
- }
- final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds();
- int rotation = newParentConfiguration.windowConfiguration.getRotation();
- if (rotation == ROTATION_UNDEFINED && !isFixedRotationTransforming()) {
- rotation = mDisplayContent.getRotation();
- }
- if (!mOptOutEdgeToEdge && (!mResolveConfigHint.mUseOverrideInsetsForConfig
- || getCompatDisplayInsets() != null
- || (isFloating(parentWindowingMode)
- // Check the requested windowing mode of activity as well in case it is
- // switching between PiP and fullscreen.
- && (inOutConfig.windowConfiguration.getWindowingMode()
- == WINDOWING_MODE_UNDEFINED
- || isFloating(inOutConfig.windowConfiguration.getWindowingMode())))
- || rotation == ROTATION_UNDEFINED)) {
- // If the insets configuration decoupled logic is not enabled for the app, or the app
- // already has a compat override, or the context doesn't contain enough info to
- // calculate the override, skip the override.
- return;
- }
- // Make sure the orientation related fields will be updated by the override insets, because
- // fixed rotation has assigned the fields from display's configuration.
- if (hasFixedRotationTransform()) {
- inOutConfig.windowConfiguration.setAppBounds(null);
- inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED;
- inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
- inOutConfig.smallestScreenWidthDp = Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
- inOutConfig.orientation = ORIENTATION_UNDEFINED;
- }
-
- // Override starts here.
- final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
- final int dw = rotated ? mDisplayContent.mBaseDisplayHeight
- : mDisplayContent.mBaseDisplayWidth;
- final int dh = rotated ? mDisplayContent.mBaseDisplayWidth
- : mDisplayContent.mBaseDisplayHeight;
- final Rect nonDecorInsets = mDisplayContent.getDisplayPolicy()
- .getDecorInsetsInfo(rotation, dw, dh).mOverrideNonDecorInsets;
- // This should be the only place override the configuration for ActivityRecord. Override
- // the value if not calculated yet.
- Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
- if (outAppBounds == null || outAppBounds.isEmpty()) {
- inOutConfig.windowConfiguration.setAppBounds(parentBounds);
- outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
- outAppBounds.inset(nonDecorInsets);
- }
- float density = inOutConfig.densityDpi;
- if (density == Configuration.DENSITY_DPI_UNDEFINED) {
- density = newParentConfiguration.densityDpi;
- }
- density *= DisplayMetrics.DENSITY_DEFAULT_SCALE;
- if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
- inOutConfig.screenWidthDp = (int) (outAppBounds.width() / density + 0.5f);
- }
- if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
- inOutConfig.screenHeightDp = (int) (outAppBounds.height() / density + 0.5f);
- }
- if (inOutConfig.smallestScreenWidthDp
- == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED
- && parentWindowingMode == WINDOWING_MODE_FULLSCREEN) {
- // For the case of PIP transition and multi-window environment, the
- // smallestScreenWidthDp is handled already. Override only if the app is in
- // fullscreen.
- final DisplayInfo info = new DisplayInfo(mDisplayContent.getDisplayInfo());
- mDisplayContent.computeSizeRanges(info, rotated, dw, dh,
- mDisplayContent.getDisplayMetrics().density,
- inOutConfig, true /* overrideConfig */);
- }
-
- // It's possible that screen size will be considered in different orientation with or
- // without considering the system bar insets. Override orientation as well.
- if (inOutConfig.orientation == ORIENTATION_UNDEFINED) {
- inOutConfig.orientation =
- (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp)
- ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
- }
- }
-
private void computeConfigByResolveHint(@NonNull Configuration resolvedConfig,
@NonNull Configuration parentConfig) {
task.computeConfigResourceOverrides(resolvedConfig, parentConfig, mResolveConfigHint);
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index efd5202..3ebaf03 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -22,14 +22,23 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.activityTypeToString;
+import static android.app.WindowConfiguration.isFloating;
import static android.app.WindowConfiguration.windowingModeToString;
import static android.app.WindowConfigurationProto.WINDOWING_MODE;
import static android.content.ConfigurationProto.WINDOW_CONFIGURATION;
+import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
import static com.android.server.wm.ConfigurationContainerProto.FULL_CONFIGURATION;
import static com.android.server.wm.ConfigurationContainerProto.MERGED_OVERRIDE_CONFIGURATION;
@@ -38,11 +47,14 @@
import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.app.WindowConfiguration;
+import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.LocaleList;
+import android.util.DisplayMetrics;
import android.util.proto.ProtoOutputStream;
+import android.view.DisplayInfo;
import com.android.internal.annotations.VisibleForTesting;
@@ -173,6 +185,110 @@
mResolvedOverrideConfiguration.setTo(mRequestedOverrideConfiguration);
}
+ /**
+ * If necessary, override configuration fields related to app bounds.
+ * This will happen when the app is targeting SDK earlier than 35.
+ * The insets and configuration has decoupled since SDK level 35, to make the system
+ * compatible to existing apps, override the configuration with legacy metrics. In legacy
+ * metrics, fields such as appBounds will exclude some of the system bar areas.
+ * The override contains all potentially affected fields in Configuration, including
+ * screenWidthDp, screenHeightDp, smallestScreenWidthDp, and orientation.
+ * All overrides to those fields should be in this method.
+ *
+ * TODO: Consider integrate this with computeConfigByResolveHint()
+ */
+ static void applySizeOverrideIfNeeded(DisplayContent displayContent, ApplicationInfo appInfo,
+ Configuration newParentConfiguration, Configuration inOutConfig,
+ boolean optsOutEdgeToEdge, boolean hasFixedRotationTransform,
+ boolean hasCompatDisplayInsets) {
+ if (displayContent == null) {
+ return;
+ }
+ final boolean useOverrideInsetsForConfig =
+ displayContent.mWmService.mFlags.mInsetsDecoupledConfiguration
+ ? !appInfo.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED)
+ && !appInfo.isChangeEnabled(
+ OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION)
+ : appInfo.isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION);
+ final int parentWindowingMode =
+ newParentConfiguration.windowConfiguration.getWindowingMode();
+ final boolean isFloating = isFloating(parentWindowingMode)
+ // Check the requested windowing mode of activity as well in case it is
+ // switching between PiP and fullscreen.
+ && (inOutConfig.windowConfiguration.getWindowingMode() == WINDOWING_MODE_UNDEFINED
+ || isFloating(inOutConfig.windowConfiguration.getWindowingMode()));
+ int rotation = newParentConfiguration.windowConfiguration.getRotation();
+ if (rotation == ROTATION_UNDEFINED && !hasFixedRotationTransform) {
+ rotation = displayContent.getRotation();
+ }
+ if (!optsOutEdgeToEdge && (!useOverrideInsetsForConfig
+ || hasCompatDisplayInsets
+ || isFloating
+ || rotation == ROTATION_UNDEFINED)) {
+ // If the insets configuration decoupled logic is not enabled for the app, or the app
+ // already has a compat override, or the context doesn't contain enough info to
+ // calculate the override, skip the override.
+ return;
+ }
+ // Make sure the orientation related fields will be updated by the override insets, because
+ // fixed rotation has assigned the fields from display's configuration.
+ if (hasFixedRotationTransform) {
+ inOutConfig.windowConfiguration.setAppBounds(null);
+ inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED;
+ inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
+ inOutConfig.smallestScreenWidthDp = Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
+ inOutConfig.orientation = ORIENTATION_UNDEFINED;
+ }
+
+ // Override starts here.
+ final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+ final int dw = rotated
+ ? displayContent.mBaseDisplayHeight
+ : displayContent.mBaseDisplayWidth;
+ final int dh = rotated
+ ? displayContent.mBaseDisplayWidth
+ : displayContent.mBaseDisplayHeight;
+ // This should be the only place override the configuration for ActivityRecord. Override
+ // the value if not calculated yet.
+ Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
+ if (outAppBounds == null || outAppBounds.isEmpty()) {
+ inOutConfig.windowConfiguration.setAppBounds(
+ newParentConfiguration.windowConfiguration.getBounds());
+ outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
+ outAppBounds.inset(displayContent.getDisplayPolicy()
+ .getDecorInsetsInfo(rotation, dw, dh).mOverrideNonDecorInsets);
+ }
+ float density = inOutConfig.densityDpi;
+ if (density == Configuration.DENSITY_DPI_UNDEFINED) {
+ density = newParentConfiguration.densityDpi;
+ }
+ density *= DisplayMetrics.DENSITY_DEFAULT_SCALE;
+ if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
+ inOutConfig.screenWidthDp = (int) (outAppBounds.width() / density + 0.5f);
+ }
+ if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
+ inOutConfig.screenHeightDp = (int) (outAppBounds.height() / density + 0.5f);
+ }
+ if (inOutConfig.smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED
+ && parentWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+ // For the case of PIP transition and multi-window environment, the
+ // smallestScreenWidthDp is handled already. Override only if the app is in
+ // fullscreen.
+ final DisplayInfo info = new DisplayInfo(displayContent.getDisplayInfo());
+ displayContent.computeSizeRanges(info, rotated, dw, dh,
+ displayContent.getDisplayMetrics().density,
+ inOutConfig, true /* overrideConfig */);
+ }
+
+ // It's possible that screen size will be considered in different orientation with or
+ // without considering the system bar insets. Override orientation as well.
+ if (inOutConfig.orientation == ORIENTATION_UNDEFINED) {
+ inOutConfig.orientation = (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp)
+ ? ORIENTATION_PORTRAIT
+ : ORIENTATION_LANDSCAPE;
+ }
+ }
+
/** Returns {@code true} if requested override override configuration is not empty. */
boolean hasRequestedOverrideConfiguration() {
return mHasOverrideConfiguration;
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 63fe94c..e50a089 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -44,6 +44,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
import com.android.server.UiThread;
+import com.android.window.flags.Flags;
/**
* Controls camera compatibility treatment that handles orientation mismatch between camera
@@ -69,6 +70,9 @@
@NonNull
private final ActivityRefresher mActivityRefresher;
+ @Nullable
+ private Task mCameraTask;
+
@ScreenOrientation
private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET;
@@ -104,7 +108,7 @@
* guaranteed to match, the rotation can cause letterboxing.
*
* <p>If treatment isn't applicable returns {@link SCREEN_ORIENTATION_UNSPECIFIED}. See {@link
- * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment.
+ * #isTreatmentEnabledForDisplay} for conditions enabling the treatment.
*/
@ScreenOrientation
int getOrientation() {
@@ -136,9 +140,9 @@
// are aligned when they compute orientation of the preview.
// This means that even for a landscape-only activity and a device with landscape natural
// orientation this would return SCREEN_ORIENTATION_PORTRAIT because an assumption that
- // natural orientation = portrait window = portait camera is the main wrong assumption
+ // natural orientation = portrait window = portrait camera is the main wrong assumption
// that apps make when they implement camera previews so landscape windows need be
- // rotated in the orientation oposite to the natural one even if it's portrait.
+ // rotated in the orientation opposite to the natural one even if it's portrait.
// TODO(b/261475895): Consider allowing more rotations for "sensor" and "user" versions
// of the portrait and landscape orientation requests.
final int orientation = (isPortraitActivity && isNaturalDisplayOrientationPortrait)
@@ -296,6 +300,7 @@
@Override
public boolean onCameraOpened(@NonNull ActivityRecord cameraActivity,
@NonNull String cameraId) {
+ mCameraTask = cameraActivity.getTask();
// Checking whether an activity in fullscreen rather than the task as this camera
// compat treatment doesn't cover activity embedding.
if (cameraActivity.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
@@ -305,7 +310,7 @@
}
// Checking that the whole app is in multi-window mode as we shouldn't show toast
// for the activity embedding case.
- if (cameraActivity.getTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW
+ if (mCameraTask != null && mCameraTask.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW
&& isTreatmentEnabledForActivity(
cameraActivity, /* mustBeFullscreen */ false)) {
final PackageManager packageManager = mWmService.mContext.getPackageManager();
@@ -343,10 +348,15 @@
@Override
public boolean onCameraClosed(@NonNull String cameraId) {
- // Top activity in the same task as the camera activity, or `null` if the task is
- // closed.
- final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
- /* considerKeyguardState= */ true);
+ final ActivityRecord topActivity;
+ if (Flags.cameraCompatFullscreenPickSameTaskActivity()) {
+ topActivity = mCameraTask != null ? mCameraTask.getTopActivity(
+ /* includeFinishing= */ true, /* includeOverlays= */ false) : null;
+ } else {
+ topActivity = mDisplayContent.topRunningActivity(/* considerKeyguardState= */ true);
+ }
+
+ mCameraTask = null;
if (topActivity == null) {
return true;
}
@@ -368,8 +378,6 @@
mDisplayContent.mDisplayId);
// Checking whether an activity in fullscreen rather than the task as this camera compat
// treatment doesn't cover activity embedding.
- // TODO(b/350495350): Consider checking whether this activity is the camera activity, or
- // whether the top activity has the same task as the one which opened camera.
if (topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
return true;
}
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index 3123018..63af5c6 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -240,7 +240,7 @@
}
r.setVisibility(true);
}
- if (r != starting) {
+ if (r != starting && mNotifyClients) {
mTaskFragment.mTaskSupervisor.startSpecificActivity(r, andResume,
true /* checkConfig */);
}
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 12c5073..984caf1 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -1674,6 +1674,22 @@
// Otherwise if other places send wpc.getConfiguration() to client, the configuration may
// be ignored due to the seq is older.
resolvedConfig.seq = newParentConfig.seq;
+
+ if (mConfigActivityRecord != null) {
+ // Let the activity decide whether to apply the size override.
+ return;
+ }
+ final DisplayContent displayContent = mAtm.mWindowManager != null
+ ? mAtm.mWindowManager.getDefaultDisplayContentLocked()
+ : null;
+ applySizeOverrideIfNeeded(
+ displayContent,
+ mInfo,
+ newParentConfig,
+ resolvedConfig,
+ false /* optsOutEdgeToEdge */,
+ false /* hasFixedRotationTransform */,
+ false /* hasCompatDisplayInsets */);
}
void dispatchConfiguration(@NonNull Configuration config) {
diff --git a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
index 4211764..3559e62 100644
--- a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
+++ b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
@@ -47,16 +47,13 @@
Flags::enableDesktopWindowingWallpaperActivity, /* shouldOverrideByDevOption= */ true);
private static final String TAG = "DesktopModeFlagsUtil";
- private static final String SYSTEM_PROPERTY_OVERRIDE_KEY =
- "sys.wmshell.desktopmode.dev_toggle_override";
-
// Function called to obtain aconfig flag value.
private final Supplier<Boolean> mFlagFunction;
// Whether the flag state should be affected by developer option.
private final boolean mShouldOverrideByDevOption;
// Local cache for toggle override, which is initialized once on its first access. It needs to
- // be refreshed only on reboots as overridden state takes effect on reboots.
+ // be refreshed only on reboots as overridden state is expected to take effect on reboots.
private static ToggleOverride sCachedToggleOverride;
DesktopModeFlagsUtil(Supplier<Boolean> flagFunction, boolean shouldOverrideByDevOption) {
@@ -67,9 +64,6 @@
/**
* Determines state of flag based on the actual flag and desktop mode developer option
* overrides.
- *
- * Note: this method makes sure that a constant developer toggle overrides is read until
- * reboot.
*/
public boolean isEnabled(Context context) {
if (!Flags.showDesktopWindowingDevOption()
@@ -102,49 +96,15 @@
}
/**
- * Returns {@link ToggleOverride} from a non-persistent system property if present. Otherwise
- * initializes the system property by reading Settings.Global.
+ * Returns {@link ToggleOverride} from Settings.Global set by toggle.
*/
private ToggleOverride getToggleOverrideFromSystem(Context context) {
- // A non-persistent System Property is used to store override to ensure it remains
- // constant till reboot.
- String overrideProperty = System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, null);
- ToggleOverride overrideFromSystemProperties = convertToToggleOverride(overrideProperty);
-
- // If valid system property, return it
- if (overrideFromSystemProperties != null) {
- return overrideFromSystemProperties;
- }
-
- // Fallback when System Property is not present (just after reboot) or not valid (user
- // manually changed the value): Read from Settings.Global
int settingValue = Settings.Global.getInt(
context.getContentResolver(),
Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES,
OVERRIDE_UNSET.getSetting()
);
- ToggleOverride overrideFromSettingsGlobal =
- ToggleOverride.fromSetting(settingValue, OVERRIDE_UNSET);
- // Initialize System Property
- System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(settingValue));
- return overrideFromSettingsGlobal;
- }
-
- /**
- * Converts {@code intString} into {@link ToggleOverride}. Return {@code null} if
- * {@code intString} does not correspond to a {@link ToggleOverride}.
- */
- private static @Nullable ToggleOverride convertToToggleOverride(
- @Nullable String intString
- ) {
- if (intString == null) return null;
- try {
- int intValue = Integer.parseInt(intString);
- return ToggleOverride.fromSetting(intValue, null);
- } catch (NumberFormatException e) {
- Log.w(TAG, "Unknown toggleOverride int " + intString);
- return null;
- }
+ return ToggleOverride.fromSetting(settingValue, OVERRIDE_UNSET);
}
/** Override state of desktop mode developer option toggle. */
diff --git a/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt b/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt
index 996daf5..95ee958 100644
--- a/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt
+++ b/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt
@@ -19,6 +19,7 @@
import android.os.FileUtils
import android.util.AtomicFile
import android.util.Slog
+import com.android.server.security.FileIntegrity;
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
@@ -49,6 +50,7 @@
inline fun AtomicFile.writeWithReserveCopy(block: (FileOutputStream) -> Unit) {
writeInlined(block)
val reserveFile = File(baseFile.parentFile, baseFile.name + ".reservecopy")
+ reserveFile.delete()
try {
FileInputStream(baseFile).use { inputStream ->
FileOutputStream(reserveFile).use { outputStream ->
@@ -59,6 +61,12 @@
} catch (e: Exception) {
Slog.e("AccessPersistence", "Failed to write $reserveFile", e)
}
+ try {
+ FileIntegrity.setUpFsVerity(baseFile)
+ FileIntegrity.setUpFsVerity(reserveFile)
+ } catch (e: Exception) {
+ Slog.e("AccessPersistence", "Failed to verity-protect runtime-permissions", e)
+ }
}
/** Write to an [AtomicFile] and close everything safely when done. */
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index 3ed6ad7..acdbbde 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -398,13 +398,13 @@
if (randomNum >= traceFrequency) {
return;
}
- // For a small percentage a traces, we collect the initialization behavior.
- boolean traceInitialization = ThreadLocalRandom.current().nextInt(10) < 1;
- int traceDelay = traceInitialization ? 0 : 1000;
- String traceTag = traceInitialization ? "camera_init" : "camera";
+ final int traceDelay = 1000;
+ final int traceDuration = 5000;
+ final String traceTag = "camera";
BackgroundThread.get().getThreadHandler().postDelayed(() -> {
try {
- mIProfcollect.trace_process(traceTag, "android.hardware.camera.provider");
+ mIProfcollect.trace_process(traceTag, "android.hardware.camera.provider",
+ traceDuration);
} catch (RemoteException e) {
Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
}
diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java
index 99968d5..9da695a 100644
--- a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java
+++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java
@@ -19,10 +19,12 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
import android.content.Context;
import android.content.res.Resources;
@@ -44,9 +46,6 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.ArrayList;
-import java.util.Collections;
-
@SmallTest
@RunWith(AndroidJUnit4.class)
public class DreamAccessibilityTest {
@@ -73,7 +72,8 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mDreamAccessibility = new DreamAccessibility(mContext, mView);
+ Runnable mDismissCallback = () -> {};
+ mDreamAccessibility = new DreamAccessibility(mContext, mView, mDismissCallback);
when(mContext.getResources()).thenReturn(mResources);
when(mResources.getString(R.string.dream_accessibility_action_click))
@@ -84,80 +84,55 @@
*/
@Test
public void testConfigureAccessibilityActions() {
- when(mAccessibilityNodeInfo.getActionList()).thenReturn(new ArrayList<>());
+ when(mView.getAccessibilityDelegate()).thenReturn(null);
- mDreamAccessibility.updateAccessibilityConfiguration(false);
+ mDreamAccessibility.updateAccessibilityConfiguration();
verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
- View.AccessibilityDelegate capturedDelegate =
- mAccessibilityDelegateArgumentCaptor.getValue();
+ View.AccessibilityDelegate capturedDelegate = mAccessibilityDelegateArgumentCaptor
+ .getValue();
capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo);
verify(mAccessibilityNodeInfo).addAction(argThat(action ->
- action.getId() == AccessibilityNodeInfo.ACTION_CLICK
+ action.getId() == AccessibilityNodeInfo.ACTION_DISMISS
&& TextUtils.equals(action.getLabel(), CUSTOM_ACTION)));
}
/**
- * Test to verify the configuration of accessibility actions within a view delegate,
- * specifically checking the removal of an existing click action and addition
- * of a new custom action.
+ * Test to verify no accessibility configuration is added if one exist.
*/
@Test
- public void testConfigureAccessibilityActions_RemovesExistingClickAction() {
- AccessibilityNodeInfo.AccessibilityAction existingAction =
- new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK,
- EXISTING_ACTION);
- when(mAccessibilityNodeInfo.getActionList())
- .thenReturn(Collections.singletonList(existingAction));
+ public void testNotAddingDuplicateAccessibilityConfiguration() {
+ View.AccessibilityDelegate existingDelegate = mock(View.AccessibilityDelegate.class);
+ when(mView.getAccessibilityDelegate()).thenReturn(existingDelegate);
- mDreamAccessibility.updateAccessibilityConfiguration(false);
+ mDreamAccessibility.updateAccessibilityConfiguration();
- verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
- View.AccessibilityDelegate capturedDelegate =
- mAccessibilityDelegateArgumentCaptor.getValue();
-
- capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo);
-
- verify(mAccessibilityNodeInfo).removeAction(existingAction);
- verify(mAccessibilityNodeInfo).addAction(argThat(action ->
- action.getId() == AccessibilityNodeInfo.ACTION_CLICK
- && TextUtils.equals(action.getLabel(), CUSTOM_ACTION)));
-
- }
-
- /**
- * Test to verify the removal of a custom accessibility action within a view delegate.
- */
- @Test
- public void testRemoveCustomAccessibilityAction() {
-
- AccessibilityNodeInfo.AccessibilityAction existingAction =
- new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK,
- EXISTING_ACTION);
- when(mAccessibilityNodeInfo.getActionList())
- .thenReturn(Collections.singletonList(existingAction));
-
- mDreamAccessibility.updateAccessibilityConfiguration(false);
- verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
- View.AccessibilityDelegate capturedDelegate =
- mAccessibilityDelegateArgumentCaptor.getValue();
- when(mView.getAccessibilityDelegate()).thenReturn(capturedDelegate);
- clearInvocations(mView);
-
- mDreamAccessibility.updateAccessibilityConfiguration(true);
- verify(mView).setAccessibilityDelegate(null);
- }
-
- /**
- * Test to verify the removal of custom accessibility action is not called if delegate is not
- * set by the dreamService.
- */
- @Test
- public void testRemoveCustomAccessibility_DoesNotRemoveDelegateNotSetByDreamAccessibility() {
- mDreamAccessibility.updateAccessibilityConfiguration(true);
verify(mView, never()).setAccessibilityDelegate(any());
}
+
+ /**
+ * Test to verify dismiss callback is called
+ */
+ @Test
+ public void testPerformAccessibilityAction() {
+ Runnable mockDismissCallback = mock(Runnable.class);
+ DreamAccessibility dreamAccessibility = new DreamAccessibility(mContext,
+ mView, mockDismissCallback);
+
+ dreamAccessibility.updateAccessibilityConfiguration();
+
+ verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
+ View.AccessibilityDelegate capturedDelegate = mAccessibilityDelegateArgumentCaptor
+ .getValue();
+
+ boolean result = capturedDelegate.performAccessibilityAction(mView,
+ AccessibilityNodeInfo.ACTION_DISMISS, null);
+
+ assertTrue(result);
+ verify(mockDismissCallback).run();
+ }
+
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index 0f38532..a4222ff 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -1518,7 +1518,8 @@
mBiometricService.onStart();
when(mTrustManager.isInSignificantPlace()).thenReturn(false);
- when(mBiometricService.mSettingObserver.getMandatoryBiometricsEnabledForUser(anyInt()))
+ when(mBiometricService.mSettingObserver
+ .getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(anyInt()))
.thenReturn(true);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
@@ -1540,7 +1541,8 @@
mBiometricService.onStart();
when(mTrustManager.isInSignificantPlace()).thenReturn(false);
- when(mBiometricService.mSettingObserver.getMandatoryBiometricsEnabledForUser(anyInt()))
+ when(mBiometricService.mSettingObserver
+ .getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(anyInt()))
.thenReturn(true);
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
@@ -1564,7 +1566,8 @@
mBiometricService.onStart();
when(mTrustManager.isInSignificantPlace()).thenReturn(false);
- when(mBiometricService.mSettingObserver.getMandatoryBiometricsEnabledForUser(anyInt()))
+ when(mBiometricService.mSettingObserver
+ .getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(anyInt()))
.thenReturn(true);
setupAuthForOnly(TYPE_CREDENTIAL, Authenticators.DEVICE_CREDENTIAL);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
index b831ef5..240da9f 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
@@ -90,7 +90,8 @@
when(mDevicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt()))
.thenReturn(KEYGUARD_DISABLE_FEATURES_NONE);
when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true);
- when(mSettingObserver.getMandatoryBiometricsEnabledForUser(anyInt())).thenReturn(true);
+ when(mSettingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(
+ anyInt())).thenReturn(true);
when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true);
when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true);
when(mFaceAuthenticator.getLockoutModeForUser(anyInt()))
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
index 2f4a660..a8856dd 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
@@ -20,6 +20,7 @@
import static com.android.server.hdmi.Constants.ADDR_TV;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
+import static com.android.server.hdmi.OneTouchPlayAction.LOOP_COUNTER_MAX;
import static com.android.server.hdmi.OneTouchPlayAction.STATE_WAITING_FOR_REPORT_POWER_STATUS;
import static com.google.common.truth.Truth.assertThat;
@@ -335,7 +336,7 @@
mTestLooper.dispatchAll();
}
- assertThat(mNativeWrapper.getResultMessages()).doesNotContain(textViewOn);
+ assertThat(mNativeWrapper.getResultMessages()).contains(textViewOn);
assertThat(mNativeWrapper.getResultMessages()).doesNotContain(activeSource);
assertThat(mNativeWrapper.getResultMessages()).contains(giveDevicePowerStatus);
action.handleTimerEvent(STATE_WAITING_FOR_REPORT_POWER_STATUS);
@@ -672,7 +673,122 @@
mHdmiControlService.playback().getDeviceInfo().getLogicalAddress(),
ADDR_TV);
assertThat(mNativeWrapper.getResultMessages()).doesNotContain(textViewOn);
+ }
+ @Test
+ public void waitForReportPowerStatus_resendTextViewOn_timeout() throws Exception {
+ setUp(true);
+
+ HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
+ mTestLooper.dispatchAll();
+
+ mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
+ mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
+ mTestLooper.dispatchAll();
+ mNativeWrapper.clearResultMessages();
+
+ TestActionTimer actionTimer = new TestActionTimer();
+ TestCallback callback = new TestCallback();
+ OneTouchPlayAction action = createOneTouchPlayAction(playbackDevice, actionTimer, callback,
+ false);
+ playbackDevice.addAndStartAction(action);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage activeSource =
+ HdmiCecMessageBuilder.buildActiveSource(
+ playbackDevice.getDeviceInfo().getLogicalAddress(), mPhysicalAddress);
+ HdmiCecMessage textViewOn =
+ HdmiCecMessageBuilder.buildTextViewOn(
+ playbackDevice.getDeviceInfo().getLogicalAddress(), ADDR_TV);
+ HdmiCecMessage giveDevicePowerStatus =
+ HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
+ playbackDevice.getDeviceInfo().getLogicalAddress(), ADDR_TV);
+
+ assertThat(mNativeWrapper.getResultMessages()).contains(textViewOn);
+ assertThat(mNativeWrapper.getResultMessages()).contains(activeSource);
+ assertThat(mNativeWrapper.getResultMessages()).contains(giveDevicePowerStatus);
+ mNativeWrapper.clearResultMessages();
+ assertThat(actionTimer.getState()).isEqualTo(STATE_WAITING_FOR_REPORT_POWER_STATUS);
+
+ int counter = 0;
+ while (counter++ < LOOP_COUNTER_MAX) {
+ action.handleTimerEvent(STATE_WAITING_FOR_REPORT_POWER_STATUS);
+ mTestLooper.dispatchAll();
+
+ if (counter % 3 == 0) {
+ assertThat(mNativeWrapper.getResultMessages()).contains(textViewOn);
+ }
+ assertThat(mNativeWrapper.getResultMessages()).contains(giveDevicePowerStatus);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.dispatchAll();
+ }
+
+ action.handleTimerEvent(STATE_WAITING_FOR_REPORT_POWER_STATUS);
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TIMEOUT);
+ }
+
+ @Test
+ public void waitForReportPowerStatus_resendTextViewOn_success() throws Exception {
+ setUp(true);
+
+ HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
+ mTestLooper.dispatchAll();
+
+ mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
+ mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
+ mTestLooper.dispatchAll();
+ mNativeWrapper.clearResultMessages();
+
+ TestActionTimer actionTimer = new TestActionTimer();
+ TestCallback callback = new TestCallback();
+ OneTouchPlayAction action = createOneTouchPlayAction(playbackDevice, actionTimer, callback,
+ false);
+ playbackDevice.addAndStartAction(action);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage activeSource =
+ HdmiCecMessageBuilder.buildActiveSource(
+ playbackDevice.getDeviceInfo().getLogicalAddress(), mPhysicalAddress);
+ HdmiCecMessage textViewOn =
+ HdmiCecMessageBuilder.buildTextViewOn(
+ playbackDevice.getDeviceInfo().getLogicalAddress(), ADDR_TV);
+ HdmiCecMessage giveDevicePowerStatus =
+ HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
+ playbackDevice.getDeviceInfo().getLogicalAddress(), ADDR_TV);
+
+ assertThat(mNativeWrapper.getResultMessages()).contains(textViewOn);
+ assertThat(mNativeWrapper.getResultMessages()).contains(activeSource);
+ assertThat(mNativeWrapper.getResultMessages()).contains(giveDevicePowerStatus);
+ mNativeWrapper.clearResultMessages();
+ assertThat(actionTimer.getState()).isEqualTo(STATE_WAITING_FOR_REPORT_POWER_STATUS);
+
+ int counter = 0;
+ while (counter++ < LOOP_COUNTER_MAX) {
+ action.handleTimerEvent(STATE_WAITING_FOR_REPORT_POWER_STATUS);
+ mTestLooper.dispatchAll();
+
+ if (counter % 3 == 0) {
+ assertThat(mNativeWrapper.getResultMessages()).contains(textViewOn);
+ }
+ assertThat(mNativeWrapper.getResultMessages()).contains(giveDevicePowerStatus);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.dispatchAll();
+ }
+
+ assertThat(actionTimer.getState()).isEqualTo(STATE_WAITING_FOR_REPORT_POWER_STATUS);
+ HdmiCecMessage reportPowerStatusOn =
+ HdmiCecMessage.build(
+ ADDR_TV,
+ playbackDevice.getDeviceInfo().getLogicalAddress(),
+ Constants.MESSAGE_REPORT_POWER_STATUS,
+ POWER_ON);
+ action.processCommand(reportPowerStatusOn);
+ mTestLooper.dispatchAll();
+
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(textViewOn);
+ assertThat(mNativeWrapper.getResultMessages()).contains(activeSource);
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(giveDevicePowerStatus);
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
}
private static class TestActionTimer implements ActionTimer {
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index 316b5fa..689b241 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -39,6 +39,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -364,6 +365,39 @@
@EnableFlags(android.companion.virtualdevice.flags
.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
@Test
+ public void testReuseProjection_keyguardNotLocked_startConsentDialog()
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.start(mIMediaProjectionCallback);
+
+ doNothing().when(mContext).startActivityAsUser(any(), any());
+ doReturn(false).when(mKeyguardManager).isKeyguardLocked();
+
+ MediaProjectionManagerService.BinderService mediaProjectionBinderService =
+ mService.new BinderService(mContext);
+ mediaProjectionBinderService.requestConsentForInvalidProjection(projection);
+
+ verify(mContext).startActivityAsUser(any(), any());
+ }
+
+ @EnableFlags(android.companion.virtualdevice.flags
+ .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+ @Test
+ public void testReuseProjection_keyguardLocked_noConsentDialog() throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.start(mIMediaProjectionCallback);
+
+ doReturn(true).when(mKeyguardManager).isKeyguardLocked();
+ MediaProjectionManagerService.BinderService mediaProjectionBinderService =
+ mService.new BinderService(mContext);
+ mediaProjectionBinderService.requestConsentForInvalidProjection(projection);
+
+ verify(mContext, never()).startActivityAsUser(any(), any());
+ }
+
+ @EnableFlags(android.companion.virtualdevice.flags
+ .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+ @Test
public void testKeyguardLocked_stopsActiveProjection() throws Exception {
MediaProjectionManagerService service =
new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index 9dac23f..d7004e7 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -1746,10 +1746,6 @@
assertTrue("Tested duration=" + duration4, duration4 < 2000);
// Effect5: played normally after effect4, which may or may not have played.
-
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId5));
- verifyCallbacksTriggered(vibrationId5, Vibration.Status.FINISHED);
-
assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
fakeVibrator.getEffectSegments(vibrationId5));
}
diff --git a/services/tests/wmtests/res/xml/bookmarks.xml b/services/tests/wmtests/res/xml/bookmarks.xml
index 88419e9..1549b2d 100644
--- a/services/tests/wmtests/res/xml/bookmarks.xml
+++ b/services/tests/wmtests/res/xml/bookmarks.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2024 The Android Open Source Project
+<!-- 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.
@@ -14,6 +14,8 @@
limitations under the License.
-->
<bookmarks>
+ <!-- the key combinations for the following shortcuts must be in sync
+ with the key combinations sent by the test in ModifierShortcutTests.java -->
<bookmark
role="android.app.role.BROWSER"
shortcut="b" />
@@ -38,4 +40,37 @@
<bookmark
category="android.intent.category.APP_CALCULATOR"
shortcut="u" />
+
+ <!-- The following shortcuts will not be invoked by tests but are here to
+ provide test coverage of parsing the different types of shortcut. -->
+ <bookmark
+ package="com.test"
+ class="com.test.BookmarkTest"
+ shortcut="a" />
+ <bookmark
+ package="com.test2"
+ class="com.test.BookmarkTest"
+ shortcut="d" />
+
+ <bookmark
+ role="android.app.role.BROWSER"
+ shortcut="b"
+ shift="true" />
+ <bookmark
+ category="android.intent.category.APP_CONTACTS"
+ shortcut="c"
+ shift="true" />
+ <bookmark
+ package="com.test"
+ class="com.test.BookmarkTest"
+ shortcut="a"
+ shift="true" />
+
+ <!-- It's intended that this package/class will NOT resolve so we test the resolution
+ failure case. -->
+ <bookmark
+ package="com.test3"
+ class="com.test.BookmarkTest"
+ shortcut="f" />
+
</bookmarks>
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
index 8c375d4..5533ff9 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * 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.
@@ -19,15 +19,22 @@
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assert.assertEquals;
+import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyObject;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.os.Handler;
@@ -58,27 +65,56 @@
private Handler mHandler;
private Context mContext;
private Resources mResources;
+ private PackageManager mPackageManager;
@Before
public void setUp() {
mHandler = new Handler(Looper.getMainLooper());
mContext = spy(getInstrumentation().getTargetContext());
mResources = spy(mContext.getResources());
+ mPackageManager = spy(mContext.getPackageManager());
XmlResourceParser testBookmarks = mResources.getXml(
com.android.frameworks.wmtests.R.xml.bookmarks);
when(mContext.getResources()).thenReturn(mResources);
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mResources.getXml(R.xml.bookmarks)).thenReturn(testBookmarks);
+ try {
+ // Keep packageName / className in sync with
+ // services/tests/wmtests/res/xml/bookmarks.xml
+ ActivityInfo testActivityInfo = new ActivityInfo();
+ testActivityInfo.applicationInfo = new ApplicationInfo();
+ testActivityInfo.packageName =
+ testActivityInfo.applicationInfo.packageName = "com.test";
+
+ doReturn(testActivityInfo).when(mPackageManager).getActivityInfo(
+ eq(new ComponentName("com.test", "com.test.BookmarkTest")), anyInt());
+ doThrow(new PackageManager.NameNotFoundException("com.test3")).when(mPackageManager)
+ .getActivityInfo(eq(new ComponentName("com.test3", "com.test.BookmarkTest")),
+ anyInt());
+ } catch (PackageManager.NameNotFoundException ignored) { }
+ doReturn(new String[] { "com.test" }).when(mPackageManager)
+ .canonicalToCurrentPackageNames(aryEq(new String[] { "com.test2" }));
+
mModifierShortcutManager = new ModifierShortcutManager(mContext, mHandler);
}
@Test
public void test_getApplicationLaunchKeyboardShortcuts() {
+ // Expected values here determined by the number of shortcuts defined in
+ // services/tests/wmtests/res/xml/bookmarks.xml
+
+ // Total valid shortcuts.
KeyboardShortcutGroup group =
mModifierShortcutManager.getApplicationLaunchKeyboardShortcuts(-1);
- assertEquals(8, group.getItems().size());
+ assertEquals(13, group.getItems().size());
+
+ // Total valid shift shortcuts.
+ assertEquals(3, group.getItems().stream()
+ .filter(s -> s.getModifiers() == (KeyEvent.META_SHIFT_ON | KeyEvent.META_META_ON))
+ .count());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 0bf850a..a1ac02a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -390,6 +390,15 @@
rootTask.ensureActivitiesVisible(null /* starting */);
assertTrue(activity1.isVisible());
assertTrue(activity2.isVisible());
+
+ // If notifyClients is false, it should only update the state without starting the client.
+ activity1.setVisible(false);
+ activity1.setVisibleRequested(false);
+ activity1.detachFromProcess();
+ rootTask.ensureActivitiesVisible(null /* starting */, false /* notifyClients */);
+ verify(mSupervisor, never()).startSpecificActivity(eq(activity1),
+ anyBoolean() /* andResume */, anyBoolean() /* checkConfig */);
+ assertTrue(activity1.isVisibleRequested());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index e6648da..0cb22ad 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -18,6 +18,7 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED;
import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
@@ -86,6 +87,7 @@
ApplicationInfo info = mock(ApplicationInfo.class);
info.packageName = "test.package.name";
+ doReturn(true).when(info).isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED);
mWpc = new WindowProcessController(
mAtm, info, null, 0, -1, null, mMockListener);
mWpc.setThread(mock(IApplicationThread.class));
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
index e5f2f89..eda78cb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
@@ -63,9 +63,6 @@
resetCache();
}
- private static final String SYSTEM_PROPERTY_OVERRIDE_KEY =
- "sys.wmshell.desktopmode.dev_toggle_override";
-
@Test
@DisableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@@ -190,110 +187,6 @@
}
@Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
- @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isEnabled_noProperty_overrideOn_featureFlagOff_returnsTrueAndPropertyOn() {
- System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
- setOverride(OVERRIDE_ON.getSetting());
-
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
- // Store System Property if not present
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting()));
- }
-
- @Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- public void isEnabled_noProperty_overrideUnset_featureFlagOn_returnsTrueAndPropertyUnset() {
- System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
- setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
-
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
- // Store System Property if not present
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(String.valueOf(
- DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
- }
-
- @Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
- @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isEnabled_noProperty_overrideUnset_featureFlagOff_returnsFalseAndPropertyUnset() {
- System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
- setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
-
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
- // Store System Property if not present
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(String.valueOf(
- DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
- }
-
- @Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- public void isEnabled_propertyNotInt_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() {
- System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "abc");
- setOverride(OVERRIDE_OFF.getSetting());
-
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
- // Store System Property if currently invalid
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
- }
-
- @Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- public void isEnabled_propertyInvalid_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() {
- System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "-2");
- setOverride(OVERRIDE_OFF.getSetting());
-
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
- // Store System Property if currently invalid
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
- }
-
- @Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- public void isEnabled_propertyOff_overrideOn_featureFlagOn_returnsFalseAndnoPropertyUpdate() {
- System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(
- OVERRIDE_OFF.getSetting()));
- setOverride(OVERRIDE_ON.getSetting());
-
- // Have a consistent override until reboot
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
- }
-
- @Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
- @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isEnabled_propertyOn_overrideOff_featureFlagOff_returnsTrueAndnoPropertyUpdate() {
- System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(OVERRIDE_ON.getSetting()));
- setOverride(OVERRIDE_OFF.getSetting());
-
- // Have a consistent override until reboot
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting()));
- }
-
- @Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- public void isEnabled_propertyUnset_overrideOff_featureFlagOn_returnsTrueAndnoPropertyUpdate() {
- System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY,
- String.valueOf(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
- setOverride(OVERRIDE_OFF.getSetting());
-
- // Have a consistent override until reboot
- assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
- assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
- .isEqualTo(String.valueOf(
- DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
- }
-
- @Test
@EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY})
public void isEnabled_dwFlagOn_overrideUnset_featureFlagOn_returnsTrue() {
@@ -452,8 +345,5 @@
"sCachedToggleOverride");
cachedToggleOverride.setAccessible(true);
cachedToggleOverride.set(null, null);
-
- // Clear override cache stored in System property
- System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
}
}
diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp
index f367c38..06c2651 100644
--- a/tests/Input/Android.bp
+++ b/tests/Input/Android.bp
@@ -48,6 +48,7 @@
"testables",
"testng",
"truth",
+ "ui-trace-collector",
],
libs: [
"android.test.mock",
diff --git a/tests/Input/AndroidTest.xml b/tests/Input/AndroidTest.xml
index 4a99bd4..bc9322f 100644
--- a/tests/Input/AndroidTest.xml
+++ b/tests/Input/AndroidTest.xml
@@ -22,6 +22,10 @@
<option name="shell-timeout" value="660s" />
<option name="test-timeout" value="600s" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="device-listeners" value="android.tools.collectors.DefaultUITraceListener"/>
+ <!-- DefaultUITraceListener args -->
+ <option name="instrumentation-arg" key="skip_test_success_metrics" value="true"/>
+ <option name="instrumentation-arg" key="per_class" value="true"/>
</test>
<object class="com.android.tradefed.testtype.suite.module.TestFailureModuleController"
type="module_controller">
@@ -32,6 +36,8 @@
<option name="pull-pattern-keys" value="input_.*" />
<!-- Pull files created by tests, like the output of screenshot tests -->
<option name="directory-keys" value="/sdcard/Download/InputTests" />
+ <!-- Pull perfetto traces from DefaultUITraceListener -->
+ <option name="pull-pattern-keys" value="perfetto_file_path*" />
<option name="collect-on-run-ended-only" value="false" />
</metrics_collector>
</configuration>
diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
index b6672a0..fad94d4 100644
--- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -698,7 +698,7 @@
traceMonitor.start();
mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP,
- "My test null string: %s", null);
+ "My test null string: %s", (Object) null);
} finally {
traceMonitor.stop(mWriter);
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/PackageFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/PackageFilter.kt
new file mode 100644
index 0000000..c67e671
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/PackageFilter.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.hoststubgen.filters
+
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.utils.Trie
+
+/**
+ * Filter to apply a policy to classes inside a package, either directly or indirectly.
+ */
+class PackageFilter(
+ fallback: OutputFilter
+) : DelegatingFilter(fallback) {
+
+ private val mPackagePolicies = PackagePolicyTrie()
+
+ // We want to pick the most specific filter for a package name.
+ // Since any package with a matching prefix is a valid match, we can use a prefix tree
+ // to help us find the nearest matching filter.
+ private class PackagePolicyTrie : Trie<String, String, FilterPolicyWithReason>() {
+ // Split package name into individual component
+ override fun splitToComponents(key: String): Iterator<String> {
+ return key.split('.').iterator()
+ }
+ }
+
+ private fun getPackageKey(packageName: String): String {
+ return packageName.toHumanReadableClassName()
+ }
+
+ private fun getPackageKeyFromClass(className: String): String {
+ val clazz = className.toHumanReadableClassName()
+ val idx = clazz.lastIndexOf('.')
+ return if (idx >= 0) clazz.substring(0, idx) else ""
+ }
+
+ /**
+ * Add a policy to all classes inside a package, either directly or indirectly.
+ */
+ fun addPolicy(packageName: String, policy: FilterPolicyWithReason) {
+ mPackagePolicies[getPackageKey(packageName)] = policy
+ }
+
+ override fun getPolicyForClass(className: String): FilterPolicyWithReason {
+ return mPackagePolicies[getPackageKeyFromClass(className)]
+ ?: super.getPolicyForClass(className)
+ }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index c5acd81..a89824e 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -64,7 +64,8 @@
log.i("Loading offloaded annotations from $filename ...")
log.withIndent {
val subclassFilter = SubclassFilter(classes, fallback)
- val imf = InMemoryOutputFilter(classes, subclassFilter)
+ val packageFilter = PackageFilter(subclassFilter)
+ val imf = InMemoryOutputFilter(classes, packageFilter)
var lineNo = 0
@@ -78,10 +79,7 @@
var className = ""
while (true) {
- var line = reader.readLine()
- if (line == null) {
- break
- }
+ var line = reader.readLine() ?: break
lineNo++
line = normalizeTextLine(line)
@@ -95,6 +93,31 @@
val fields = line.split(whitespaceRegex).toTypedArray()
when (fields[0].lowercase()) {
+ "p", "package" -> {
+ if (fields.size < 3) {
+ throw ParseException("Package ('p') expects 2 fields.")
+ }
+ val name = fields[1]
+ val rawPolicy = fields[2]
+ if (resolveExtendingClass(name) != null) {
+ throw ParseException("Package can't be a super class type")
+ }
+ if (resolveSpecialClass(name) != SpecialClass.NotSpecial) {
+ throw ParseException("Package can't be a special class type")
+ }
+ if (rawPolicy.startsWith("!")) {
+ throw ParseException("Package can't have a substitution")
+ }
+ if (rawPolicy.startsWith("~")) {
+ throw ParseException("Package can't have a class load hook")
+ }
+ val policy = parsePolicy(rawPolicy)
+ if (!policy.isUsableWithClasses) {
+ throw ParseException("Package can't have policy '$policy'")
+ }
+ packageFilter.addPolicy(name, policy.withReason(FILTER_REASON))
+ }
+
"c", "class" -> {
if (fields.size < 3) {
throw ParseException("Class ('c') expects 2 fields.")
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/Trie.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/Trie.kt
new file mode 100644
index 0000000..1b3d79c
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/Trie.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.hoststubgen.utils
+
+abstract class Trie<Key, Component, Value> {
+
+ private val root = TrieNode<Component, Value>()
+
+ abstract fun splitToComponents(key: Key): Iterator<Component>
+
+ operator fun set(key: Key, value: Value) {
+ val node = root.getExactNode(splitToComponents(key))
+ node.value = value
+ }
+
+ operator fun get(key: Key): Value? {
+ return root.getNearestValue(null, splitToComponents(key))
+ }
+
+ private class TrieNode<Component, Value> {
+ private val children = mutableMapOf<Component, TrieNode<Component, Value>>()
+ var value: Value? = null
+
+ fun getExactNode(components: Iterator<Component>): TrieNode<Component, Value> {
+ val n = components.next()
+ val child = children.getOrPut(n) { TrieNode() }
+ return if (components.hasNext()) {
+ child.getExactNode(components)
+ } else {
+ child
+ }
+ }
+
+ fun getNearestValue(current: Value?, components: Iterator<Component>): Value? {
+ val n = components.next()
+ val child = children[n] ?: return current
+ val newValue = child.value ?: current
+ return if (components.hasNext()) {
+ child.getNearestValue(newValue, components)
+ } else {
+ newValue
+ }
+ }
+ }
+}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
index dd63892..3ef1175 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
@@ -2706,6 +2706,98 @@
RuntimeInvisibleAnnotations:
x: #x()
android.hosttest.annotation.HostSideTestWholeClassStub
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/A.class
+ Compiled from "A.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.A
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/A
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 1, attributes: 1
+ public com.android.hoststubgen.test.tinyframework.packagetest.A();
+ descriptor: ()V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=1, locals=1, args_size=1
+ x: aload_0
+ x: invokespecial #x // Method java/lang/Object."<init>":()V
+ x: return
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 5 0 this Lcom/android/hoststubgen/test/tinyframework/packagetest/A;
+}
+SourceFile: "A.java"
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/B.class
+ Compiled from "B.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.B
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/B
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 1, attributes: 1
+ public com.android.hoststubgen.test.tinyframework.packagetest.B();
+ descriptor: ()V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=1, locals=1, args_size=1
+ x: aload_0
+ x: invokespecial #x // Method java/lang/Object."<init>":()V
+ x: return
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 5 0 this Lcom/android/hoststubgen/test/tinyframework/packagetest/B;
+}
+SourceFile: "B.java"
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/sub/A.class
+ Compiled from "A.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.sub.A
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/sub/A
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 1, attributes: 1
+ public com.android.hoststubgen.test.tinyframework.packagetest.sub.A();
+ descriptor: ()V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=1, locals=1, args_size=1
+ x: aload_0
+ x: invokespecial #x // Method java/lang/Object."<init>":()V
+ x: return
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 5 0 this Lcom/android/hoststubgen/test/tinyframework/packagetest/sub/A;
+}
+SourceFile: "A.java"
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/sub/B.class
+ Compiled from "B.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.sub.B
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/sub/B
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 1, attributes: 1
+ public com.android.hoststubgen.test.tinyframework.packagetest.sub.B();
+ descriptor: ()V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=1, locals=1, args_size=1
+ x: aload_0
+ x: invokespecial #x // Method java/lang/Object."<init>":()V
+ x: return
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 5 0 this Lcom/android/hoststubgen/test/tinyframework/packagetest/sub/B;
+}
+SourceFile: "B.java"
## Class: com/android/hoststubgen/test/tinyframework/subclasstest/C1.class
Compiled from "C1.java"
public class com.android.hoststubgen.test.tinyframework.subclasstest.C1
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
index 906a81c..0bbb418 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
@@ -2177,6 +2177,38 @@
RuntimeInvisibleAnnotations:
x: #x()
android.hosttest.annotation.HostSideTestWholeClassStub
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/A.class
+ Compiled from "A.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.A
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/A
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "A.java"
+RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInStub
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/sub/A.class
+ Compiled from "A.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.sub.A
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/sub/A
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "A.java"
+RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInStub
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl
## Class: com/android/hoststubgen/test/tinyframework/subclasstest/C1.class
Compiled from "C1.java"
public class com.android.hoststubgen.test.tinyframework.subclasstest.C1
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
index 10bc91d..57f3783 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
@@ -3540,6 +3540,38 @@
RuntimeInvisibleAnnotations:
x: #x()
android.hosttest.annotation.HostSideTestWholeClassStub
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/A.class
+ Compiled from "A.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.A
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/A
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "A.java"
+RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInStub
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/sub/A.class
+ Compiled from "A.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.sub.A
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/sub/A
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "A.java"
+RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInStub
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl
## Class: com/android/hoststubgen/test/tinyframework/subclasstest/C1.class
Compiled from "C1.java"
public class com.android.hoststubgen.test.tinyframework.subclasstest.C1
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt
index 906a81c..0bbb418 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt
@@ -2177,6 +2177,38 @@
RuntimeInvisibleAnnotations:
x: #x()
android.hosttest.annotation.HostSideTestWholeClassStub
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/A.class
+ Compiled from "A.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.A
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/A
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "A.java"
+RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInStub
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/sub/A.class
+ Compiled from "A.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.sub.A
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/sub/A
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "A.java"
+RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInStub
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl
## Class: com/android/hoststubgen/test/tinyframework/subclasstest/C1.class
Compiled from "C1.java"
public class com.android.hoststubgen.test.tinyframework.subclasstest.C1
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt
index fcf9a8c..91104de 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt
@@ -4408,6 +4408,56 @@
RuntimeInvisibleAnnotations:
x: #x()
android.hosttest.annotation.HostSideTestWholeClassStub
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/A.class
+ Compiled from "A.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.A
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/A
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 1, attributes: 2
+ private static {};
+ descriptor: ()V
+ flags: (0x000a) ACC_PRIVATE, ACC_STATIC
+ Code:
+ stack=2, locals=0, args_size=0
+ x: ldc #x // class com/android/hoststubgen/test/tinyframework/packagetest/A
+ x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
+ x: return
+}
+SourceFile: "A.java"
+RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInStub
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl
+## Class: com/android/hoststubgen/test/tinyframework/packagetest/sub/A.class
+ Compiled from "A.java"
+public class com.android.hoststubgen.test.tinyframework.packagetest.sub.A
+ minor version: 0
+ major version: 61
+ flags: (0x0021) ACC_PUBLIC, ACC_SUPER
+ this_class: #x // com/android/hoststubgen/test/tinyframework/packagetest/sub/A
+ super_class: #x // java/lang/Object
+ interfaces: 0, fields: 0, methods: 1, attributes: 2
+ private static {};
+ descriptor: ()V
+ flags: (0x000a) ACC_PRIVATE, ACC_STATIC
+ Code:
+ stack=2, locals=0, args_size=0
+ x: ldc #x // class com/android/hoststubgen/test/tinyframework/packagetest/sub/A
+ x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
+ x: return
+}
+SourceFile: "A.java"
+RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInStub
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl
## Class: com/android/hoststubgen/test/tinyframework/subclasstest/C1.class
Compiled from "C1.java"
public class com.android.hoststubgen.test.tinyframework.subclasstest.C1
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/policy-override-tiny-framework.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/policy-override-tiny-framework.txt
index 696b6d0..530de43 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/policy-override-tiny-framework.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/policy-override-tiny-framework.txt
@@ -40,3 +40,11 @@
class *com.android.hoststubgen.test.tinyframework.subclasstest.I1 keep
class *com.android.hoststubgen.test.tinyframework.subclasstest.IA remove
+
+# Test package directive
+package com.android.hoststubgen.test.tinyframework.packagetest stub
+class com.android.hoststubgen.test.tinyframework.packagetest.B remove
+class com.android.hoststubgen.test.tinyframework.packagetest.sub.B remove
+# The following rules are the same as above
+# class com.android.hoststubgen.test.tinyframework.packagetest.A stub
+# class com.android.hoststubgen.test.tinyframework.packagetest.sub.A stub
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/A.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/A.java
new file mode 100644
index 0000000..6a52e44
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/A.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.hoststubgen.test.tinyframework.packagetest;
+
+public class A {
+}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/B.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/B.java
new file mode 100644
index 0000000..1374a28
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/B.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.hoststubgen.test.tinyframework.packagetest;
+
+public class B {
+}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/sub/A.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/sub/A.java
new file mode 100644
index 0000000..361a7fd
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/sub/A.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.hoststubgen.test.tinyframework.packagetest.sub;
+
+public class A {
+}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/sub/B.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/sub/B.java
new file mode 100644
index 0000000..716595a
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/packagetest/sub/B.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.hoststubgen.test.tinyframework.packagetest.sub;
+
+public class B {
+}
diff --git a/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/TrieTest.kt b/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/TrieTest.kt
new file mode 100644
index 0000000..081d039
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/TrieTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.hoststubgen.utils
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class TrieTest {
+
+ private class TestTrie : Trie<String, Char, Int>() {
+ override fun splitToComponents(key: String): Iterator<Char> {
+ return key.toCharArray().iterator()
+ }
+ }
+
+ @Test
+ fun testPrefixTree() {
+ val trie = TestTrie()
+ trie["ab"] = 1
+ trie["abc"] = 2
+ trie["ab123"] = 3
+ assertNull(trie["a"])
+ assertNull(trie["x"])
+ assertNull(trie["a1"])
+ assertEquals(1, trie["ab"])
+ assertEquals(2, trie["abc"])
+ assertEquals(2, trie["abcd"])
+ assertEquals(1, trie["ab1"])
+ assertEquals(1, trie["ab12"])
+ assertEquals(3, trie["ab123"])
+ assertEquals(1, trie["ab@"])
+ }
+}