diff options
75 files changed, 2042 insertions, 596 deletions
diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java index 292e6bdba539..50b73a9d3f66 100644 --- a/core/java/android/os/StrictMode.java +++ b/core/java/android/os/StrictMode.java @@ -126,20 +126,18 @@ import java.util.function.Consumer; * 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 final class StrictMode { 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/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index b2c39b13525f..ceaca2257af4 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -2798,9 +2798,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager 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/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java index 8906e6d3d02e..88264f383153 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.Optional; 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 859bc2cc40f3..84984a9f8c7b 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.Application; 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 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { || 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 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } } + @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 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { */ 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 fe60037483c4..63828ab2e62b 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 @@ import java.util.function.Consumer; /** * 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 de52f0969fa8..cd26efd4fdb6 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.Set; 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 @@ public abstract class BaseDataProducer<T> implements DataProducer<T>, 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 @@ public abstract class BaseDataProducer<T> implements DataProducer<T>, * @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 @@ public abstract class BaseDataProducer<T> implements DataProducer<T>, /** * 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 ec301dc34aaa..000000000000 --- 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 c2ba064ac7b6..39f6d8c46a24 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/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index fc4710f8f67b..a1ba24ca366f 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 @@ public class DesktopModeStatus { 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 @@ public class DesktopModeStatus { } /** + * 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 000000000000..56447de6bae1 --- /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/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 700742acfb43..32526ff06abb 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.RootTaskDisplayAreaOrganizer; 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 @@ public abstract class WMShellModule { 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 @@ public abstract class WMShellModule { transitions, desktopTasksController, rootTaskDisplayAreaOrganizer, - interactionJankMonitor); + interactionJankMonitor, + genericLinksParser); } return new CaptionWindowDecorViewModel( context, @@ -259,6 +262,15 @@ public abstract class WMShellModule { transitions); } + @WMSingleton + @Provides + static AppToWebGenericLinksParser provideGenericLinksParser( + Context context, + @ShellMainThread ShellExecutor mainExecutor + ) { + return new AppToWebGenericLinksParser(context, mainExecutor); + } + // // Freeform // 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 0e8fd7c7d0a1..a77a76c4188b 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.window.flags.Flags; 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { Transitions transitions, Optional<DesktopTasksController> desktopTasksController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - InteractionJankMonitor interactionJankMonitor + InteractionJankMonitor interactionJankMonitor, + AppToWebGenericLinksParser genericLinksParser ) { this( context, @@ -216,6 +219,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { syncQueue, transitions, desktopTasksController, + genericLinksParser, new DesktopModeWindowDecoration.Factory(), new InputMonitorFactory(), SurfaceControl.Transaction::new, @@ -241,6 +245,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SyncTransactionQueue syncQueue, Transitions transitions, Optional<DesktopTasksController> desktopTasksController, + AppToWebGenericLinksParser genericLinksParser, DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory, InputMonitorFactory inputMonitorFactory, Supplier<SurfaceControl.Transaction> transactionFactory, @@ -266,6 +271,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mInputMonitorFactory = inputMonitorFactory; mTransactionFactory = transactionFactory; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mGenericLinksParser = genericLinksParser; mInputManager = mContext.getSystemService(InputManager.class); mWindowDecorByTaskId = windowDecorByTaskId; mSysUIPackageName = mContext.getResources().getString( @@ -669,12 +675,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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()); @@ -1157,7 +1157,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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 529def7ca3d7..a1cc6507c280 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.getLarge 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.graphics.drawable.Drawable; 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.window.flags.Flags; 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mChoreographer = choreographer; mSyncQueue = syncQueue; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mGenericLinksParser = genericLinksParser; mMaximizeMenuFactory = maximizeMenuFactory; + mHandleMenuFactory = handleMenuFactory; } /** @@ -425,11 +435,23 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ void createHandleMenu(SplitScreenController splitScreenController) { loadAppInfoIfNeeded(); - mHandleMenu = new HandleMenu( + updateGenericLink(); + mHandleMenu = mHandleMenuFactory.create( this, mRelayoutParams.mLayoutResId, mOnCaptionButtonClickListener, @@ -924,7 +945,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mDisplayController, splitScreenController, DesktopModeStatus.canEnterDesktopMode(mContext), - browserLinkAvailable(), + getBrowserLink(), mResult.mCaptionWidth, mResult.mCaptionHeight, mResult.mCaptionX @@ -933,9 +954,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + AppToWebGenericLinksParser genericLinksParser) { return new DesktopModeWindowDecoration( context, displayController, @@ -1231,7 +1259,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin bgExecutor, choreographer, syncQueue, - rootTaskDisplayAreaOrganizer); + rootTaskDisplayAreaOrganizer, + genericLinksParser); } } 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 e174e83d11c9..32522c6411cb 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.BlendModeColorFilter 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 @@ class HandleMenu( 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 @@ class HandleMenu( // those as well. private val globalMenuPosition: Point = Point() + private val shouldShowBrowserPill: Boolean + get() = openInBrowserLink != null + init { updateHandleMenuPillPositions(captionX) } @@ -497,3 +501,57 @@ class HandleMenu( 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 76dfe37c7cc0..57d8cac2d336 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.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 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 @@ import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystem */ 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 @@ internal class AppHandleViewHolder( 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 @@ internal class AppHandleViewHolder( captionView.setOnTouchListener(onCaptionTouchListener) captionHandle.setOnTouchListener(onCaptionTouchListener) captionHandle.setOnClickListener(onCaptionButtonClickListener) - captionHandle.setOnHoverListener(onCaptionHoverListener) } override fun bindData( @@ -106,10 +106,19 @@ internal class AppHandleViewHolder( // 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 000000000000..053027fc1cf4 --- /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/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index aeae0bebb697..01c4f3ad6870 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.ShellTaskOrganizer 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { bgExecutor, mockMainChoreographer, mockSyncQueue, - mockRootTaskDisplayAreaOrganizer + mockRootTaskDisplayAreaOrganizer, + mockGenericLinksParser ) verify(decoration).close() } @@ -244,7 +249,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { bgExecutor, mockMainChoreographer, mockSyncQueue, - mockRootTaskDisplayAreaOrganizer + mockRootTaskDisplayAreaOrganizer, + mockGenericLinksParser ) task.setWindowingMode(WINDOWING_MODE_FREEFORM) @@ -261,7 +267,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { bgExecutor, mockMainChoreographer, mockSyncQueue, - mockRootTaskDisplayAreaOrganizer + mockRootTaskDisplayAreaOrganizer, + mockGenericLinksParser ) } @@ -359,7 +366,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { verify(mockDesktopModeWindowDecorFactory, never()) .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), - any()) + any(), any()) } @Test @@ -381,7 +388,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { verify(mockDesktopModeWindowDecorFactory, never()) .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), - any()) + any(), any()) } @Test @@ -416,7 +423,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { 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 412fef30d4fb..4b069f933d9f 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 com.google.common.truth.Truth.assertThat; 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.annotations.EnableFlags; 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.ShellTaskOrganizer; 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 @@ import java.util.function.Supplier; * 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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_capturedLinkExpiresAfterClick() { + public void capturedLink_capturedLinkStillUsedIfExpiredAfterHandleMenuCreation() { 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); - decor.relayout(taskInfo); - // Assert captured link is set - assertTrue(decor.browserLinkAvailable()); + // 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, 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { "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/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index e548f8f1eb2a..ed43aa3f1301 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 @@ class HandleMenuTest : ShellTestCase() { 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/packages/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING index 76d7ab13adad..b3fb1e7b3034 100644 --- a/packages/PackageInstaller/TEST_MAPPING +++ b/packages/PackageInstaller/TEST_MAPPING @@ -9,6 +9,9 @@ ] }, { + "name": "CtsUpdateOwnershipEnforcementTestCases" + }, + { "name": "CtsNoPermissionTestCases" }, { diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index cfd74d4b9f42..ce997bf15b91 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/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 c5dab3347e5f..38a347465318 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 @@ internal constructor( 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 @@ internal constructor( 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 @@ internal constructor( // 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/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt index 4ad020f94c89..bbd2f6b0174a 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 @@ class CommunalSceneStartableTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun keyguardGoesAway_whenLaunchingWidget_doNotForceBlankScene() = with(kosmos) { testScope.runTest { @@ -123,6 +126,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun keyguardGoesAway_whenNotLaunchingWidget_forceBlankScene() = with(kosmos) { testScope.runTest { @@ -143,6 +147,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun keyguardGoesAway_whenInEditMode_doesNotChangeScene() = with(kosmos) { testScope.runTest { @@ -180,6 +185,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun occluded_forceBlankScene() = with(kosmos) { testScope.runTest { @@ -199,6 +205,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun occluded_doesNotForceBlankSceneIfLaunchingActivityOverLockscreen() = with(kosmos) { testScope.runTest { @@ -218,6 +225,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun deviceDocked_doesNotForceCommunalIfTransitioningFromCommunal() = with(kosmos) { testScope.runTest { @@ -235,6 +243,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun deviceAsleep_forceBlankSceneAfterTimeout() = with(kosmos) { testScope.runTest { @@ -256,6 +265,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun deviceAsleep_wakesUpBeforeTimeout_noChangeInScene() = with(kosmos) { testScope.runTest { @@ -483,6 +493,7 @@ class CommunalSceneStartableTest : SysuiTestCase() { } @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 c707ebf0a2c4..ca81838bcdaa 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 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { CommunalWidgetContentModel.Pending( appWidgetId = 2, priority = 2, - packageName = "pk_2", + componentName = ComponentName("pk_2", "cls_2"), icon = fakeIcon, user = mainUser, ), @@ -731,7 +731,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { 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 9539c0492056..0242c2d6b89d 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 @@ package com.android.systemui.communal.domain.interactor 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.CommunalContentModel 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 @@ class CommunalInteractorTest : SysuiTestCase() { 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 @@ class CommunalInteractorTest : SysuiTestCase() { 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 @@ class CommunalInteractorTest : SysuiTestCase() { 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 @@ class CommunalInteractorTest : SysuiTestCase() { 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 @@ class CommunalInteractorTest : SysuiTestCase() { 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 @@ class CommunalInteractorTest : SysuiTestCase() { ) } - 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 f7f70c154ce6..ad7385344fac 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.flags.fakeFeatureFlagsClassic 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.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -53,6 +58,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow 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 @@ class CommunalSceneTransitionInteractorTest : SysuiTestCase() { @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 @@ class CommunalSceneTransitionInteractorTest : SysuiTestCase() { ) } + /** 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 000000000000..35df641035f7 --- /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 f8906adc33d4..61487b0a8c73 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.communalPrefsInteractor 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 @@ import org.mockito.kotlin.spy 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 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { 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 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { logcatLogBuffer("CommunalEditModeViewModelTest"), kosmos.testDispatcher, kosmos.communalPrefsInteractor, + metricsLogger, ) } @@ -142,20 +141,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { 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 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { 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 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { 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 c480aa8e4a3d..d862a21d8e99 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.communalSettingsInteracto 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 @@ import platform.test.runner.parameterized.Parameters @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 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { 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 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { 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 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { 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 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { ) // 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 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { ) // 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 3d2eabf2a07c..c9f3f1453492 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.Flags.FLAG_COMMUNAL_HUB 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.kosmos.testScope 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 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { 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 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { 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 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { ) } - 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/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 42cd5ec69466..3fd1c20c0560 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 @@ -2230,11 +2230,13 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest 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) ) - runCurrent() + advanceTimeBy(100L) // GIVEN a prior transition has run to GLANCEABLE_HUB communalSceneInteractor.changeScene(CommunalScenes.Communal) diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt index e9b23850627a..6b7712d9364e 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 android.provider.Settings 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 @@ constructor( 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 a44533555a04..ba2b7bf96a30 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.CommunalSmartspaceRepositor 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.Module import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap +import javax.inject.Named import kotlinx.coroutines.CoroutineScope @Module( @@ -74,6 +77,11 @@ interface CommunalModule { @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 @@ interface CommunalModule { ): CoreStartable companion object { + const val LOGGABLE_PREFIXES = "loggable_prefixes" + @Provides @Communal @SysUISingleton @@ -107,5 +117,14 @@ interface CommunalModule { ): 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 e65e5e5688f5..ad0bfc76ebbf 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 @@ constructor( ) } - 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 dbddc23d6146..6aaaf3d4ce9a 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 @@ constructor( 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 @@ constructor( 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 45cfe3673f3d..e45a69599a68 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.communal.shared.model.EditModeState 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 @@ -184,6 +185,10 @@ constructor( 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 8351566fcae6..6a20610da3a6 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 @@ constructor( */ 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 @@ constructor( // 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 @@ constructor( 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 122240daed52..73c6ce30b202 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 @@ package com.android.systemui.communal.domain.model 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 CommunalContentModel { 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 @@ sealed interface CommunalContentModel { 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 @@ sealed interface CommunalContentModel { 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 000000000000..12099f708b9e --- /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 b64c1955b43d..4ab56cc34628 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 @@ enum class CommunalUiEvent(private val id: Int) : UiEventEnum { @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 53aecc199c4b..7cddb7226601 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 @@ sealed interface CommunalWidgetContentModel { 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 01ed2b71bea0..623e702a85fe 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 @@ abstract class BaseCommunalViewModel( 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 @@ abstract class BaseCommunalViewModel( /** 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 830f543fd06f..5b825d80d9b1 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 @@ package com.android.systemui.communal.ui.viewmodel 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.CommunalPrefsInteractor 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 @@ constructor( @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 @@ constructor( 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 08fe42ede5d3..398576935eed 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.shared.model.EditModeState 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 @@ constructor( 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/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index d81195060071..6b1be93c988a 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 @@ -24,6 +24,7 @@ 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 @@ -36,6 +37,7 @@ 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 @@ -240,12 +242,21 @@ constructor( ), ) .filterRelevantKeyguardStateAnd { isKeyguardGoingAway -> isKeyguardGoingAway } - .collect { - communalSceneInteractor.changeScene( - newScene = CommunalScenes.Blank, - transitionKey = CommunalTransitionKeys.SimpleFade, - keyguardState = KeyguardState.GONE - ) + .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 { 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 b084824cd348..ef76f3837889 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 @@ package com.android.systemui.keyguard.domain.interactor 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 @@ constructor( @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 @@ constructor( biometricSettingsRepository.isCurrentUserInLockdown .distinctUntilChanged() .filterRelevantKeyguardStateAnd { inLockdown -> inLockdown } - .sample(communalInteractor.isIdleOnCommunal, ::Pair) + .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair) .collect { (_, isIdleOnCommunal) -> val to = if (isIdleOnCommunal) { @@ -120,7 +120,7 @@ constructor( 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/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index 6c89ce055470..9adcaa229ae2 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 @@ package com.android.systemui.keyguard.domain.interactor 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 @@ constructor( @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 @@ constructor( .distinctUntilChanged() fun dismissPrimaryBouncer() { - scope.launch { startTransitionTo(KeyguardState.GONE) } + scope.launch { + startTransitionTo(KeyguardState.GONE) + closeHubImmediatelyIfNeeded() + } } private fun listenForPrimaryBouncerToLockscreenHubOrOccluded() { @@ -106,7 +111,7 @@ constructor( .sample( powerInteractor.isAwake, keyguardInteractor.isActiveDreamLockscreenHosted, - communalInteractor.isIdleOnCommunal + communalSceneInteractor.isIdleOnCommunal ) .filterRelevantKeyguardState() .collect { @@ -135,7 +140,7 @@ constructor( keyguardInteractor.isKeyguardOccluded, keyguardInteractor.isDreaming, keyguardInteractor.isActiveDreamLockscreenHosted, - communalInteractor.isIdleOnCommunal, + communalSceneInteractor.isIdleOnCommunal, ) .filterRelevantKeyguardStateAnd { (isBouncerShowing, isAwake, _, _, isActiveDreamLockscreenHosted, _) -> @@ -158,6 +163,19 @@ constructor( } } + 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 @@ -212,6 +230,7 @@ constructor( }, modeOnCanceled = TransitionModeOnCanceled.RESET, ) + closeHubImmediatelyIfNeeded() } } } 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 696298e82db9..0f6f03ae8334 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 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { 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 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { } final boolean isShowing = value > 0.0f; + if (isShowing == mLockscreenShowing) { + return; + } + mLockscreenShowing = isShowing; updateAllowedStates("lockscreenShowing", isShowing); } 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 f7ce367ebb18..c00454f3bc48 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 @@ package com.android.systemui.communal.data.repository 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 @@ import kotlinx.coroutines.launch /** 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 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : } } - 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/FromGoneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt index 317294f3c884..c694114a0f47 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 @@ val Kosmos.fromGoneTransitionInteractor by 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/FromPrimaryBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt index 42ee15216590..3c369d7d954f 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 @@ var Kosmos.fromPrimaryBouncerTransitionInteractor by bgDispatcher = testDispatcher, mainDispatcher = testDispatcher, keyguardInteractor = keyguardInteractor, - communalInteractor = communalInteractor, + communalSceneInteractor = communalSceneInteractor, keyguardSecurityModel = keyguardSecurityModel, selectedUserInteractor = selectedUserInteractor, powerInteractor = powerInteractor, diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 8447cca26632..3d41f05de0b8 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -288,7 +288,6 @@ import java.util.concurrent.Executors; 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; diff --git a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java index 3c3cfe61c126..256905d50dc1 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 @@ final class OneTouchPlayAction extends HdmiCecFeatureAction { // 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 @@ final class OneTouchPlayAction extends HdmiCecFeatureAction { 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 @@ final class OneTouchPlayAction extends HdmiCecFeatureAction { 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 c82e5be7c643..13209d861e8b 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.inputmethodservice.InputMethodService; 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 @@ public abstract class InputMethodManagerInternal { * @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 @@ public abstract class InputMethodManagerInternal { * @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 @@ public abstract class InputMethodManagerInternal { 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 cbe202b5adc8..a9723ccc2576 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 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. 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 @@ -1486,15 +1485,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (!mUserManagerInternal.exists(userId)) { return InputMethodInfoSafeList.empty(); } - synchronized (ImfLock.class) { - final int callingUid = Binder.getCallingUid(); - final long ident = Binder.clearCallingIdentity(); - try { - return InputMethodInfoSafeList.create(getInputMethodListLocked( - userId, directBootAwareness, callingUid)); - } finally { - Binder.restoreCallingIdentity(ident); - } + final int callingUid = Binder.getCallingUid(); + final long ident = Binder.clearCallingIdentity(); + try { + return InputMethodInfoSafeList.create(getInputMethodListInternal( + userId, directBootAwareness, callingUid)); + } finally { + Binder.restoreCallingIdentity(ident); } } @@ -1509,15 +1506,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (!mUserManagerInternal.exists(userId)) { return InputMethodInfoSafeList.empty(); } - synchronized (ImfLock.class) { - final int callingUid = Binder.getCallingUid(); - final long ident = Binder.clearCallingIdentity(); - try { - return InputMethodInfoSafeList.create( - getEnabledInputMethodListLocked(userId, callingUid)); - } finally { - Binder.restoreCallingIdentity(ident); - } + final int callingUid = Binder.getCallingUid(); + final long ident = Binder.clearCallingIdentity(); + try { + return InputMethodInfoSafeList.create( + getEnabledInputMethodListInternal(userId, callingUid)); + } finally { + Binder.restoreCallingIdentity(ident); } } @@ -1533,14 +1528,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (!mUserManagerInternal.exists(userId)) { return Collections.emptyList(); } - synchronized (ImfLock.class) { - final int callingUid = Binder.getCallingUid(); - final long ident = Binder.clearCallingIdentity(); - try { - return getInputMethodListLocked(userId, directBootAwareness, callingUid); - } finally { - Binder.restoreCallingIdentity(ident); - } + final int callingUid = Binder.getCallingUid(); + final long ident = Binder.clearCallingIdentity(); + try { + return getInputMethodListInternal(userId, directBootAwareness, callingUid); + } finally { + Binder.restoreCallingIdentity(ident); } } @@ -1555,14 +1548,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (!mUserManagerInternal.exists(userId)) { return Collections.emptyList(); } - synchronized (ImfLock.class) { - final int callingUid = Binder.getCallingUid(); - final long ident = Binder.clearCallingIdentity(); - try { - return getEnabledInputMethodListLocked(userId, callingUid); - } finally { - Binder.restoreCallingIdentity(ident); - } + final int callingUid = Binder.getCallingUid(); + final long ident = Binder.clearCallingIdentity(); + try { + return getEnabledInputMethodListInternal(userId, callingUid); + } finally { + Binder.restoreCallingIdentity(ident); } } @@ -1606,8 +1597,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. 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) { @@ -1626,8 +1616,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. 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(); @@ -1654,20 +1643,17 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. 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); @@ -5703,17 +5689,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - /** - * 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) { @@ -5850,19 +5825,27 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. 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 @@ -6608,28 +6591,29 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. 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/location/contexthub/ContextHubTestModeManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTestModeManager.java index f2714dbd7e5f..2bb3be6a3332 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 @@ import java.util.Random; 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 @@ public class ContextHubTestModeManager { } 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 @@ public class ContextHubTestModeManager { * @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/IContextHubWrapper.java b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java index 4fc3d8715a88..a8ad41853d34 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.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.Callable; /** * @hide @@ -659,32 +660,40 @@ public abstract class IContextHubWrapper { @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; + } + }; + + // 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; - } catch (RemoteException | ServiceSpecificException e) { - return ContextHubTransaction.RESULT_FAILED_UNKNOWN; - } catch (IllegalArgumentException e) { - return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS; } + return ContextHubTransaction.RESULT_SUCCESS; } @ContextHubTransaction.Result diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING index e12b70f033c3..c40608d12a0e 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 8be20b0e4904..aaa38a3a1331 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 @@ final class DefaultPermissionGrantPolicy { 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 @@ final class DefaultPermissionGrantPolicy { 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/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt b/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt index 996daf5a5f68..95ee958f3ce4 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 @@ package com.android.server.permission.access.util 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.readWithReserveCopy(block: (FileInputStream) -> Unit) { 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 @@ inline fun AtomicFile.writeWithReserveCopy(block: (FileOutputStream) -> Unit) { } 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/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java index 2f4a6604441f..a8856dd24165 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.SystemService.PHASE_SYSTEM_SERVICES_READY; 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 @@ public class OneTouchPlayActionTest { 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 @@ public class OneTouchPlayActionTest { 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/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 000000000000..c67e6714d4c2 --- /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 c5acd81f1cf2..a89824eaf0b0 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 @@ fun createFilterFromTextPolicyFile( 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 @@ fun createFilterFromTextPolicyFile( 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 @@ fun createFilterFromTextPolicyFile( 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 000000000000..1b3d79cddb8e --- /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 dd638925a5bc..3ef117567482 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 @@ SourceFile: "TinyFrameworkPackageRedirect.java" 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 906a81cf45e3..0bbb4182859b 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 @@ RuntimeVisibleAnnotations: 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 10bc91da2544..57f3783e8a73 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 @@ RuntimeVisibleAnnotations: 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 906a81cf45e3..0bbb4182859b 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 @@ RuntimeVisibleAnnotations: 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 fcf9a8c663ad..91104dea3f7b 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 @@ RuntimeVisibleAnnotations: 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 696b6d009dc2..530de431828e 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.CA remove 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 000000000000..6a52e4401b45 --- /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 000000000000..1374a288f7aa --- /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 000000000000..361a7fd04842 --- /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 000000000000..716595a44243 --- /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 000000000000..081d03909926 --- /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@"]) + } +} |