diff options
81 files changed, 2458 insertions, 401 deletions
diff --git a/Android.bp b/Android.bp index 13b170353dd6..f6a9328d2501 100644 --- a/Android.bp +++ b/Android.bp @@ -220,6 +220,7 @@ java_library { "updatable-driver-protos", "ota_metadata_proto_java", "android.hidl.base-V1.0-java", + "android.hidl.manager-V1.2-java", "android.hardware.cas-V1-java", // AIDL "android.hardware.cas-V1.0-java", "android.hardware.cas-V1.1-java", diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 18d3e5e02fbe..71698e4f4469 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -127,6 +127,7 @@ import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import android.view.inputmethod.InlineSuggestionsRequest; import android.view.inputmethod.InlineSuggestionsResponse; @@ -388,6 +389,9 @@ public class InputMethodService extends AbstractInputMethodService { private long mStylusHwSessionsTimeout = STYLUS_HANDWRITING_IDLE_TIMEOUT_MS; private Runnable mStylusWindowIdleTimeoutRunnable; private long mStylusWindowIdleTimeoutForTest; + /** Tracks last {@link MotionEvent#getToolType(int)} used for {@link MotionEvent#ACTION_DOWN}. + **/ + private int mLastUsedToolType; /** * Returns whether {@link InputMethodService} is responsible for rendering the back button and @@ -1005,7 +1009,7 @@ public class InputMethodService extends AbstractInputMethodService { */ @Override public void updateEditorToolType(@ToolType int toolType) { - onUpdateEditorToolType(toolType); + updateEditorToolTypeInternal(toolType); } /** @@ -1249,6 +1253,14 @@ public class InputMethodService extends AbstractInputMethodService { rootView.setSystemGestureExclusionRects(exclusionRects); } + private void updateEditorToolTypeInternal(int toolType) { + if (Flags.useHandwritingListenerForTooltype()) { + mLastUsedToolType = toolType; + mInputEditorInfo.setInitialToolType(toolType); + } + onUpdateEditorToolType(toolType); + } + /** * Concrete implementation of * {@link AbstractInputMethodService.AbstractInputMethodSessionImpl} that provides @@ -3110,6 +3122,9 @@ public class InputMethodService extends AbstractInputMethodService { null /* icProto */); mInputStarted = true; mStartedInputConnection = ic; + if (Flags.useHandwritingListenerForTooltype()) { + editorInfo.setInitialToolType(mLastUsedToolType); + } mInputEditorInfo = editorInfo; initialize(); mInlineSuggestionSessionController.notifyOnStartInput( @@ -3354,6 +3369,10 @@ public class InputMethodService extends AbstractInputMethodService { * had not seen the event at all. */ public boolean onKeyDown(int keyCode, KeyEvent event) { + if (Flags.useHandwritingListenerForTooltype()) { + // any KeyEvent keyDown should reset last toolType. + updateEditorToolTypeInternal(MotionEvent.TOOL_TYPE_UNKNOWN); + } if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { final ExtractEditText eet = getExtractEditTextIfVisible(); if (eet != null && eet.handleBackInTextActionModeIfNeeded(event)) { diff --git a/core/java/android/os/HwNoService.java b/core/java/android/os/HwNoService.java index 117c3ad7ee48..084031496629 100644 --- a/core/java/android/os/HwNoService.java +++ b/core/java/android/os/HwNoService.java @@ -16,37 +16,127 @@ package android.os; +import android.hidl.manager.V1_2.IServiceManager; +import android.util.Log; + +import java.util.ArrayList; + /** * A fake hwservicemanager that is used locally when HIDL isn't supported on the device. * * @hide */ -final class HwNoService implements IHwBinder, IHwInterface { +final class HwNoService extends IServiceManager.Stub implements IHwBinder, IHwInterface { + private static final String TAG = "HwNoService"; + /** @hide */ @Override - public void transact(int code, HwParcel request, HwParcel reply, int flags) {} + public String toString() { + return "[HwNoService]"; + } - /** @hide */ @Override - public IHwInterface queryLocalInterface(String descriptor) { - return new HwNoService(); + public android.hidl.base.V1_0.IBase get(String fqName, String name) + throws android.os.RemoteException { + Log.i(TAG, "get " + fqName + "/" + name + " with no hwservicemanager"); + return null; } - /** @hide */ @Override - public boolean linkToDeath(DeathRecipient recipient, long cookie) { + public boolean add(String name, android.hidl.base.V1_0.IBase service) + throws android.os.RemoteException { + Log.i(TAG, "get " + name + " with no hwservicemanager"); + return false; + } + + @Override + public byte getTransport(String fqName, String name) throws android.os.RemoteException { + Log.i(TAG, "getTransoport " + fqName + "/" + name + " with no hwservicemanager"); + return 0x0; + } + + @Override + public java.util.ArrayList<String> list() throws android.os.RemoteException { + Log.i(TAG, "list with no hwservicemanager"); + return new ArrayList<String>(); + } + + @Override + public java.util.ArrayList<String> listByInterface(String fqName) + throws android.os.RemoteException { + Log.i(TAG, "listByInterface with no hwservicemanager"); + return new ArrayList<String>(); + } + + @Override + public boolean registerForNotifications( + String fqName, String name, android.hidl.manager.V1_0.IServiceNotification callback) + throws android.os.RemoteException { + Log.i(TAG, "registerForNotifications with no hwservicemanager"); return true; } - /** @hide */ @Override - public boolean unlinkToDeath(DeathRecipient recipient) { + public ArrayList<android.hidl.manager.V1_0.IServiceManager.InstanceDebugInfo> debugDump() + throws android.os.RemoteException { + Log.i(TAG, "debugDump with no hwservicemanager"); + return new ArrayList<android.hidl.manager.V1_0.IServiceManager.InstanceDebugInfo>(); + } + + @Override + public void registerPassthroughClient(String fqName, String name) + throws android.os.RemoteException { + Log.i(TAG, "registerPassthroughClient with no hwservicemanager"); + } + + @Override + public boolean unregisterForNotifications( + String fqName, String name, android.hidl.manager.V1_0.IServiceNotification callback) + throws android.os.RemoteException { + Log.i(TAG, "unregisterForNotifications with no hwservicemanager"); return true; } - /** @hide */ @Override - public IHwBinder asBinder() { - return this; + public boolean registerClientCallback( + String fqName, + String name, + android.hidl.base.V1_0.IBase server, + android.hidl.manager.V1_2.IClientCallback cb) + throws android.os.RemoteException { + Log.i( + TAG, + "registerClientCallback for " + fqName + "/" + name + " with no hwservicemanager"); + return true; + } + + @Override + public boolean unregisterClientCallback( + android.hidl.base.V1_0.IBase server, android.hidl.manager.V1_2.IClientCallback cb) + throws android.os.RemoteException { + Log.i(TAG, "unregisterClientCallback with no hwservicemanager"); + return true; + } + + @Override + public boolean addWithChain( + String name, android.hidl.base.V1_0.IBase service, java.util.ArrayList<String> chain) + throws android.os.RemoteException { + Log.i(TAG, "addWithChain with no hwservicemanager"); + return true; + } + + @Override + public java.util.ArrayList<String> listManifestByInterface(String fqName) + throws android.os.RemoteException { + Log.i(TAG, "listManifestByInterface for " + fqName + " with no hwservicemanager"); + return new ArrayList<String>(); + } + + @Override + public boolean tryUnregister(String fqName, String name, android.hidl.base.V1_0.IBase service) + throws android.os.RemoteException { + Log.i(TAG, "tryUnregister for " + fqName + "/" + name + " with no hwservicemanager"); + return true; } } diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index ad0bf7c95c70..785055441d59 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -274,7 +274,8 @@ public class Surface implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"FRAME_RATE_CATEGORY_"}, value = {FRAME_RATE_CATEGORY_DEFAULT, FRAME_RATE_CATEGORY_NO_PREFERENCE, - FRAME_RATE_CATEGORY_LOW, FRAME_RATE_CATEGORY_NORMAL, FRAME_RATE_CATEGORY_HIGH}) + FRAME_RATE_CATEGORY_LOW, FRAME_RATE_CATEGORY_NORMAL, + FRAME_RATE_CATEGORY_HIGH_HINT, FRAME_RATE_CATEGORY_HIGH}) public @interface FrameRateCategory {} // From native_window.h or window.h. Keep these in sync. @@ -308,11 +309,21 @@ public class Surface implements Parcelable { public static final int FRAME_RATE_CATEGORY_NORMAL = 3; /** + * Hints that, as a result of a user interaction, an animation is likely to start. + * This category is a signal that a user interaction heuristic determined the need of a + * high refresh rate, and is not an explicit request from the app. + * As opposed to {@link #FRAME_RATE_CATEGORY_HIGH}, this vote may be ignored in favor of + * more explicit votes. + * @hide + */ + public static final int FRAME_RATE_CATEGORY_HIGH_HINT = 4; + + /** * Indicates a frame rate suitable for animations that require a high frame rate, which may * increase smoothness but may also increase power usage. * @hide */ - public static final int FRAME_RATE_CATEGORY_HIGH = 4; + public static final int FRAME_RATE_CATEGORY_HIGH = 5; /** * Create an empty surface, which will later be filled in by readFromParcel(). diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index dc6aa6cdc048..bb7677d6a571 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -38,3 +38,12 @@ flag { description: "Feature flag for supporting stylus handwriting delegation from RemoteViews on the home screen" bug: "279959705" } + +flag { + name: "use_handwriting_listener_for_tooltype" + namespace: "input_method" + description: "Feature flag for using handwriting spy for determining pointer toolType." + bug: "309554999" + is_fixed_read_only: true +} + diff --git a/core/jni/android_os_HwBinder.cpp b/core/jni/android_os_HwBinder.cpp index 781895eeeaba..477bd096b11a 100644 --- a/core/jni/android_os_HwBinder.cpp +++ b/core/jni/android_os_HwBinder.cpp @@ -258,14 +258,59 @@ static void JHwBinder_native_setup(JNIEnv *env, jobject thiz) { JHwBinder::SetNativeContext(env, thiz, context); } -static void JHwBinder_native_transact( - JNIEnv * /* env */, - jobject /* thiz */, - jint /* code */, - jobject /* requestObj */, - jobject /* replyObj */, - jint /* flags */) { - CHECK(!"Should not be here"); +static void JHwBinder_native_transact(JNIEnv *env, jobject thiz, jint code, jobject requestObj, + jobject replyObj, jint flags) { + if (requestObj == NULL) { + jniThrowException(env, "java/lang/NullPointerException", NULL); + return; + } + sp<hardware::IBinder> binder = JHwBinder::GetNativeBinder(env, thiz); + sp<android::hidl::base::V1_0::IBase> base = new android::hidl::base::V1_0::BpHwBase(binder); + hidl_string desc; + auto ret = base->interfaceDescriptor( + [&desc](const hidl_string &descriptor) { desc = descriptor; }); + ret.assertOk(); + // Only the fake hwservicemanager is allowed to be used locally like this. + if (desc != "android.hidl.manager@1.2::IServiceManager" && + desc != "android.hidl.manager@1.1::IServiceManager" && + desc != "android.hidl.manager@1.0::IServiceManager") { + LOG(FATAL) << "Local binders are not supported!"; + } + if (replyObj == nullptr) { + LOG(FATAL) << "Unexpected null replyObj. code: " << code; + return; + } + const hardware::Parcel *request = JHwParcel::GetNativeContext(env, requestObj)->getParcel(); + sp<JHwParcel> replyContext = JHwParcel::GetNativeContext(env, replyObj); + hardware::Parcel *reply = replyContext->getParcel(); + + request->setDataPosition(0); + + bool isOneway = (flags & IBinder::FLAG_ONEWAY) != 0; + if (!isOneway) { + replyContext->setTransactCallback([](auto &replyParcel) {}); + } + + env->CallVoidMethod(thiz, gFields.onTransactID, code, requestObj, replyObj, flags); + + if (env->ExceptionCheck()) { + jthrowable excep = env->ExceptionOccurred(); + env->ExceptionDescribe(); + env->ExceptionClear(); + + binder_report_exception(env, excep, "Uncaught error or exception in hwbinder!"); + + env->DeleteLocalRef(excep); + } + + if (!isOneway) { + if (!replyContext->wasSent()) { + // The implementation never finished the transaction. + LOG(ERROR) << "The reply failed to send!"; + } + } + + reply->setDataPosition(0); } static void JHwBinder_native_registerService( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 48a0a46dccc1..3b0e7c139bed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -19,6 +19,7 @@ package com.android.wm.shell.pip2.phone; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; @@ -54,6 +55,8 @@ public class PipTransition extends PipTransitionController { @Nullable private WindowContainerToken mPipTaskToken; @Nullable + private IBinder mEnterTransition; + @Nullable private IBinder mAutoEnterButtonNavTransition; @Nullable private IBinder mExitViaExpandTransition; @@ -98,11 +101,8 @@ public class PipTransition extends PipTransitionController { @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { - if (isAutoEnterInButtonNavigation(request)) { - mAutoEnterButtonNavTransition = transition; - return getEnterPipTransaction(transition, request); - } else if (isLegacyEnter(request)) { - mLegacyEnterTransition = transition; + if (isAutoEnterInButtonNavigation(request) || isEnterPictureInPictureModeRequest(request)) { + mEnterTransition = transition; return getEnterPipTransaction(transition, request); } return null; @@ -111,12 +111,9 @@ public class PipTransition extends PipTransitionController { @Override public void augmentRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request, @NonNull WindowContainerTransaction outWct) { - if (isAutoEnterInButtonNavigation(request)) { + if (isAutoEnterInButtonNavigation(request) || isEnterPictureInPictureModeRequest(request)) { outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */); - mAutoEnterButtonNavTransition = transition; - } else if (isLegacyEnter(request)) { - outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */); - mLegacyEnterTransition = transition; + mEnterTransition = transition; } } @@ -162,7 +159,7 @@ public class PipTransition extends PipTransitionController { && pipTask.pictureInPictureParams.isAutoEnterEnabled(); } - private boolean isLegacyEnter(@NonNull TransitionRequestInfo requestInfo) { + private boolean isEnterPictureInPictureModeRequest(@NonNull TransitionRequestInfo requestInfo) { return requestInfo.getType() == TRANSIT_PIP; } @@ -172,13 +169,15 @@ public class PipTransition extends PipTransitionController { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (transition == mAutoEnterButtonNavTransition) { - mAutoEnterButtonNavTransition = null; - return startAutoEnterButtonNavAnimation(info, startTransaction, finishTransaction, - finishCallback); - } else if (transition == mLegacyEnterTransition) { - mLegacyEnterTransition = null; - return startLegacyEnterAnimation(info, startTransaction, finishTransaction, + if (transition == mEnterTransition) { + mEnterTransition = null; + if (isLegacyEnter(info)) { + // If this is a legacy-enter-pip (auto-enter is off and PiP activity went to pause), + // then we should run an ALPHA type (cross-fade) animation. + return startAlphaTypeEnterAnimation(info, startTransaction, finishTransaction, + finishCallback); + } + return startBoundsTypeEnterAnimation(info, startTransaction, finishTransaction, finishCallback); } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; @@ -187,7 +186,15 @@ public class PipTransition extends PipTransitionController { return false; } - private boolean startAutoEnterButtonNavAnimation(@NonNull TransitionInfo info, + private boolean isLegacyEnter(@NonNull TransitionInfo info) { + TransitionInfo.Change pipChange = getPipChange(info); + // If the only change in the changes list is a TO_FRONT mode PiP task, + // then this is legacy-enter PiP. + return pipChange != null && pipChange.getMode() == TRANSIT_TO_FRONT + && info.getChanges().size() == 1; + } + + private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { @@ -205,7 +212,7 @@ public class PipTransition extends PipTransitionController { return true; } - private boolean startLegacyEnterAnimation(@NonNull TransitionInfo info, + private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 47411701e5ab..eebf8aabd89c 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -38,6 +38,7 @@ aconfig_declarations { cc_aconfig_library { name: "hwui_flags_cc_lib", + host_supported: true, aconfig_declarations: "hwui_flags", } @@ -109,12 +110,15 @@ cc_defaults { "libbase", "libharfbuzz_ng", "libminikin", + "server_configurable_flags", ], static_libs: [ "libui-types", ], + whole_static_libs: ["hwui_flags_cc_lib"], + target: { android: { shared_libs: [ @@ -146,7 +150,6 @@ cc_defaults { "libstatspull_lazy", "libstatssocket_lazy", "libtonemap", - "hwui_flags_cc_lib", ], }, host: { diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index ca119757e816..c156c46a5a9b 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -15,6 +15,13 @@ flag { } flag { + name: "high_contrast_text_luminance" + namespace: "accessibility" + description: "Use luminance to determine how to make text more high contrast, instead of RGB heuristic" + bug: "186567103" +} + +flag { name: "hdr_10bit_plus" namespace: "core_graphics" description: "Use 10101010 and FP16 formats for HDR-UI when available" diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index 2e6e97634aec..8f999904a8ab 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -16,7 +16,9 @@ #include <SkFontMetrics.h> #include <SkRRect.h> +#include <com_android_graphics_hwui_flags.h> +#include "../utils/Color.h" #include "Canvas.h" #include "FeatureFlags.h" #include "MinikinUtils.h" @@ -27,6 +29,8 @@ #include "hwui/PaintFilter.h" #include "pipeline/skia/SkiaRecordingCanvas.h" +namespace flags = com::android::graphics::hwui::flags; + namespace android { static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkScalar thickness, @@ -73,8 +77,14 @@ public: if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) { // high contrast draw path int color = paint.getColor(); - int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color); - bool darken = channelSum < (128 * 3); + bool darken; + if (flags::high_contrast_text_luminance()) { + uirenderer::Lab lab = uirenderer::sRGBToLab(color); + darken = lab.L <= 50; + } else { + int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color); + darken = channelSum < (128 * 3); + } // outline gDrawTextBlobMode = DrawTextBlobMode::HctOutline; diff --git a/libs/hwui/utils/ForceDark.h b/libs/hwui/utils/ForceDark.h index 28538c4b7a7b..ecfe41f39ecb 100644 --- a/libs/hwui/utils/ForceDark.h +++ b/libs/hwui/utils/ForceDark.h @@ -17,6 +17,8 @@ #ifndef FORCEDARKUTILS_H #define FORCEDARKUTILS_H +#include <stdint.h> + namespace android { namespace uirenderer { @@ -26,9 +28,9 @@ namespace uirenderer { * This should stay in sync with the java @IntDef in * frameworks/base/graphics/java/android/graphics/ForceDarkType.java */ -enum class ForceDarkType : __uint8_t { NONE = 0, FORCE_DARK = 1, FORCE_INVERT_COLOR_DARK = 2 }; +enum class ForceDarkType : uint8_t { NONE = 0, FORCE_DARK = 1, FORCE_INVERT_COLOR_DARK = 2 }; } /* namespace uirenderer */ } /* namespace android */ -#endif // FORCEDARKUTILS_H
\ No newline at end of file +#endif // FORCEDARKUTILS_H diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 17c4e022ba82..5a4e0a9cd5ce 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.ui.compose +import android.appwidget.AppWidgetHostView import android.os.Bundle import android.util.SizeF import android.widget.FrameLayout @@ -376,7 +377,7 @@ private fun SmartspaceContent( AndroidView( modifier = modifier, factory = { context -> - FrameLayout(context).apply { addView(model.remoteViews.apply(context, this)) } + AppWidgetHostView(context).apply { updateAppWidget(model.remoteViews) } }, // For reusing composition in lazy lists. onReset = {}, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt index 477f4555ea65..032979447861 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,20 +12,19 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ package com.android.systemui.keyguard.data.quickaffordance import android.app.Activity import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.res.R import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -37,17 +36,17 @@ import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import org.junit.runners.Parameterized.Parameter -import org.junit.runners.Parameterized.Parameters import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import platform.test.runner.parameterized.Parameter +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(Parameterized::class) +@RunWith(ParameterizedAndroidJunit4::class) class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTestCase() { companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt index 9daf1860ebb8..e7037a682cca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt @@ -94,7 +94,7 @@ class AlternateBouncerToAodTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(6) + assertThat(values.size).isEqualTo(5) values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt index 53bca483f73f..e141c2b3107f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt @@ -55,6 +55,28 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { private val underTest = kosmos.dreamingToLockscreenTransitionViewModel @Test + fun shortcutsAlpha_bothShortcutsReceiveLastValue() = + testScope.runTest { + val valuesLeft by collectValues(underTest.shortcutsAlpha) + val valuesRight by collectValues(underTest.shortcutsAlpha) + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.3f), + step(0.5f), + step(0.6f), + step(0.8f), + step(1f), + ), + testScope, + ) + + assertThat(valuesLeft.last()).isEqualTo(1f) + assertThat(valuesRight.last()).isEqualTo(1f) + } + + @Test fun dreamOverlayTranslationY() = testScope.runTest { val pixels = 100 @@ -73,7 +95,7 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(7) + assertThat(values.size).isEqualTo(6) values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } } @@ -95,7 +117,7 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(4) + assertThat(values.size).isEqualTo(3) values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } } @@ -210,7 +232,7 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt index 3c07034f0e12..897ce6d305b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt @@ -61,7 +61,7 @@ class GoneToDreamingTransitionViewModelTest : SysuiTestCase() { // Only three values should be present, since the dream overlay runs for a small // fraction of the overall animation time - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } } @@ -84,7 +84,7 @@ class GoneToDreamingTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt index a346e8b45795..4843f8ba4249 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt @@ -75,7 +75,7 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { // Only three values should be present, since the dream overlay runs for a small // fraction of the overall animation time - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } } @@ -98,10 +98,10 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { testScope = testScope, ) - assertThat(values.size).isEqualTo(6) + assertThat(values.size).isEqualTo(5) values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } // Validate finished value - assertThat(values[5]).isEqualTo(0f) + assertThat(values[4]).isEqualTo(0f) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt index 274bde1ccfdf..a1b8aab402a7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt @@ -76,7 +76,7 @@ class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() { ) // Only 3 values should be present, since the dream overlay runs for a small fraction // of the overall animation time - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } } @@ -99,7 +99,7 @@ class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() { ), testScope = testScope, ) - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } } @@ -121,11 +121,11 @@ class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() { ), testScope = testScope, ) - assertThat(values.size).isEqualTo(4) + assertThat(values.size).isEqualTo(3) values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } // Cancel will reset the translation - assertThat(values[3]).isEqualTo(0) + assertThat(values[2]).isEqualTo(0) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt index d419d4a2534c..2111ad5d975e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt @@ -95,7 +95,7 @@ class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(5) + assertThat(values.size).isEqualTo(4) values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt index f027bc849e51..90b83620084c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt @@ -95,7 +95,7 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(3) + assertThat(values.size).isEqualTo(1) values.forEach { assertThat(it).isEqualTo(0f) } } @@ -107,7 +107,7 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED)) keyguardTransitionRepository.sendTransitionStep(step(1f)) - assertThat(values.size).isEqualTo(2) + assertThat(values.size).isEqualTo(1) values.forEach { assertThat(it).isEqualTo(0f) } } @@ -121,7 +121,7 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED)) keyguardTransitionRepository.sendTransitionStep(step(1f)) - assertThat(values.size).isEqualTo(2) + assertThat(values.size).isEqualTo(1) values.forEach { assertThat(it).isEqualTo(1f) } } @@ -135,7 +135,7 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED)) keyguardTransitionRepository.sendTransitionStep(step(1f)) - assertThat(values.size).isEqualTo(2) + assertThat(values.size).isEqualTo(1) values.forEach { assertThat(it).isEqualTo(1f) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java index 53cb8a7eb81b..7a78b366dd7f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java @@ -25,15 +25,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.graphics.Point; import android.os.PowerManager; -import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.view.View; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; @@ -68,7 +67,7 @@ import java.util.Collections; import java.util.HashSet; @SmallTest -@RunWith(AndroidTestingRunner.class) +@RunWith(AndroidJUnit4.class) @RunWithLooper(setAsMainLooper = true) public class DozeServiceHostTest extends SysuiTestCase { @@ -181,6 +180,7 @@ public class DozeServiceHostTest extends SysuiTestCase { DozeLog.PULSE_REASON_DOCKING, DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE, DozeLog.REASON_SENSOR_QUICK_PICKUP, + DozeLog.PULSE_REASON_FINGERPRINT_ACTIVATED, DozeLog.REASON_SENSOR_TAP)); HashSet<Integer> reasonsThatDontPulse = new HashSet<>( Arrays.asList(DozeLog.REASON_SENSOR_PICKUP, @@ -232,7 +232,7 @@ public class DozeServiceHostTest extends SysuiTestCase { public void onSlpiTap_doesnt_pass_negative_values() { mDozeServiceHost.onSlpiTap(-1, 200); mDozeServiceHost.onSlpiTap(100, -2); - verifyZeroInteractions(mDozeInteractor); + verify(mDozeInteractor, never()).setLastTapToWakePosition(any()); } @Test public void dozeTimeTickSentToDozeInteractor() { diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index e01a2aa674b3..5c362b203e09 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -963,10 +963,16 @@ <bool name="config_edgeToEdgeBottomSheetDialog">true</bool> <!-- + Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate when + the screen is turned off with AOD not enabled. + TODO(b/302332976) Get this value from the HAL if they can provide an API for it. + --> + <integer name="config_restToUnlockDurationScreenOff">500</integer> + <!-- Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate TODO(b/302332976) Get this value from the HAL if they can provide an API for it. --> - <integer name="config_restToUnlockDuration">300</integer> + <integer name="config_restToUnlockDurationDefault">300</integer> <!-- Width in pixels of the Side FPS sensor. diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt index f4231ac01fee..c320350e69cd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt @@ -26,6 +26,8 @@ import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.isDefaultOrientation import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.log.SideFpsLogger import com.android.systemui.res.R import java.util.Optional @@ -47,6 +49,7 @@ constructor( windowManager: WindowManager, displayStateInteractor: DisplayStateInteractor, fingerprintInteractiveToAuthProvider: Optional<FingerprintInteractiveToAuthProvider>, + keyguardTransitionInteractor: KeyguardTransitionInteractor, private val logger: SideFpsLogger, ) { @@ -62,8 +65,21 @@ constructor( val isAvailable: Flow<Boolean> = fingerprintPropertyRepository.sensorType.map { it == FingerprintSensorType.POWER_BUTTON } - val authenticationDuration: Long = - context.resources?.getInteger(R.integer.config_restToUnlockDuration)?.toLong() ?: 0L + val authenticationDuration: Flow<Long> = + keyguardTransitionInteractor + .isFinishedInStateWhere { it == KeyguardState.OFF || it == KeyguardState.DOZING } + .map { + if (it) + context.resources + ?.getInteger(R.integer.config_restToUnlockDurationScreenOff) + ?.toLong() + else + context.resources + ?.getInteger(R.integer.config_restToUnlockDurationDefault) + ?.toLong() + } + .map { it ?: 0L } + .onEach { logger.authDurationChanged(it) } val isProlongedTouchRequiredForAuthentication: Flow<Boolean> = if (fingerprintInteractiveToAuthProvider.isEmpty) { diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt index 5df26b3176ff..a6b432019486 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/PanelConfirmationDialogFactory.kt @@ -28,27 +28,26 @@ import javax.inject.Inject /** * Factory to create dialogs for consenting to show app panels for specific apps. * - * [internalDialogFactory] is for facilitating testing. + * [dialogFactory] is for facilitating testing. */ -class PanelConfirmationDialogFactory( - private val internalDialogFactory: (Context) -> SystemUIDialog +class PanelConfirmationDialogFactory @Inject constructor( + private val dialogFactory: SystemUIDialog.Factory ) { - @Inject constructor() : this({ SystemUIDialog(it) }) /** * Creates a dialog to show to the user. [response] will be true if an only if the user responds * affirmatively. */ fun createConfirmationDialog( - context: Context, - appName: CharSequence, - response: Consumer<Boolean> + context: Context, + appName: CharSequence, + response: Consumer<Boolean> ): Dialog { val listener = DialogInterface.OnClickListener { _, which -> response.accept(which == DialogInterface.BUTTON_POSITIVE) } - return internalDialogFactory(context).apply { + return dialogFactory.create(context).apply { setTitle(this.context.getString(R.string.controls_panel_authorization_title, appName)) setMessage(this.context.getString(R.string.controls_panel_authorization, appName)) setCanceledOnTouchOutside(true) diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt index 2ad6014fd7cd..e42a4a6af0de 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsDialogsFactory.kt @@ -25,20 +25,21 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import java.util.function.Consumer import javax.inject.Inject -class ControlsDialogsFactory(private val internalDialogFactory: (Context) -> SystemUIDialog) { +class ControlsDialogsFactory @Inject constructor( + private val dialogFactory: SystemUIDialog.Factory +) { - @Inject constructor() : this({ SystemUIDialog(it) }) fun createRemoveAppDialog( - context: Context, - appName: CharSequence, - response: Consumer<Boolean> + context: Context, + appName: CharSequence, + response: Consumer<Boolean> ): Dialog { val listener = DialogInterface.OnClickListener { _, which -> response.accept(which == DialogInterface.BUTTON_POSITIVE) } - return internalDialogFactory(context).apply { + return dialogFactory.create(context).apply { setTitle(context.getString(R.string.controls_panel_remove_app_authorization, appName)) setCanceledOnTouchOutside(true) setOnCancelListener { response.accept(false) } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt index efa1c0a07490..684627ba27bf 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance @@ -31,4 +32,11 @@ constructor( ) { val fingerprintFailure: Flow<FailFingerprintAuthenticationStatus> = repository.authenticationStatus.filterIsInstance<FailFingerprintAuthenticationStatus>() + + /** Whether fingerprint authentication is currently running or not */ + val isRunning: Flow<Boolean> = repository.isRunning + + /** Provide the current status of fingerprint authentication. */ + val authenticationStatus: Flow<FingerprintAuthenticationStatus> = + repository.authenticationStatus } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java index 4c4aa5ce1911..8776ec5496c8 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java @@ -118,6 +118,11 @@ public interface DozeHost { * Called when the dozing state may have been updated. */ default void onDozingChanged(boolean isDozing) {} + + /** + * Called when fingerprint acquisition has started and screen state might need updating. + */ + default void onSideFingerprintAcquisitionStarted() {} } interface PulseCallback { diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index 5b90ef2bb806..424bd0a3e23b 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -514,6 +514,7 @@ public class DozeLog implements Dumpable { case REASON_SENSOR_TAP: return "tap"; case REASON_SENSOR_UDFPS_LONG_PRESS: return "udfps"; case REASON_SENSOR_QUICK_PICKUP: return "quickPickup"; + case PULSE_REASON_FINGERPRINT_ACTIVATED: return "fingerprint-triggered"; default: throw new IllegalArgumentException("invalid reason: " + pulseReason); } } @@ -542,7 +543,9 @@ public class DozeLog implements Dumpable { PULSE_REASON_SENSOR_SIGMOTION, REASON_SENSOR_PICKUP, REASON_SENSOR_DOUBLE_TAP, PULSE_REASON_SENSOR_LONG_PRESS, PULSE_REASON_DOCKING, REASON_SENSOR_WAKE_UP_PRESENCE, PULSE_REASON_SENSOR_WAKE_REACH, REASON_SENSOR_TAP, - REASON_SENSOR_UDFPS_LONG_PRESS, REASON_SENSOR_QUICK_PICKUP}) + REASON_SENSOR_UDFPS_LONG_PRESS, REASON_SENSOR_QUICK_PICKUP, + PULSE_REASON_FINGERPRINT_ACTIVATED + }) public @interface Reason {} public static final int PULSE_REASON_NONE = -1; public static final int PULSE_REASON_INTENT = 0; @@ -557,6 +560,7 @@ public class DozeLog implements Dumpable { public static final int REASON_SENSOR_TAP = 9; public static final int REASON_SENSOR_UDFPS_LONG_PRESS = 10; public static final int REASON_SENSOR_QUICK_PICKUP = 11; + public static final int PULSE_REASON_FINGERPRINT_ACTIVATED = 12; - public static final int TOTAL_REASONS = 12; + public static final int TOTAL_REASONS = 13; } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index 795c3d4528c5..93111874c69b 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -265,6 +265,10 @@ public class DozeTriggers implements DozeMachine.Part { mDozeLog.traceNotificationPulse(); } + private void onSideFingerprintAcquisitionStarted() { + requestPulse(DozeLog.PULSE_REASON_FINGERPRINT_ACTIVATED, false, null); + } + private static void runIfNotNull(Runnable runnable) { if (runnable != null) { runnable.run(); @@ -690,5 +694,10 @@ public class DozeTriggers implements DozeMachine.Part { public void onNotificationAlerted(Runnable onPulseSuppressedListener) { onNotification(onPulseSuppressedListener); } + + @Override + public void onSideFingerprintAcquisitionStarted() { + DozeTriggers.this.onSideFingerprintAcquisitionStarted(); + } }; } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt index 6cb68bade9a9..89bfd96d2408 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt @@ -16,6 +16,7 @@ package com.android.systemui.haptics.slider +import android.view.MotionEvent import androidx.annotation.FloatRange /** Configuration parameters of a [SliderHapticFeedbackProvider] */ @@ -38,6 +39,8 @@ data class SliderHapticFeedbackConfig( val numberOfLowTicks: Int = 5, /** Maximum velocity allowed for vibration scaling. This is not expected to change. */ val maxVelocityToScale: Float = 2000f, /* In pixels/sec */ + /** Axis to use when computing velocity. Must be the same as the slider's axis of movement */ + val velocityAxis: Int = MotionEvent.AXIS_X, /** Vibration scale at the upper bookend of the slider */ @FloatRange(from = 0.0, to = 1.0) val upperBookendScale: Float = 1f, /** Vibration scale at the lower bookend of the slider */ diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt index 9e6245ae7f21..6f28ab7f414c 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt @@ -162,27 +162,33 @@ class SliderHapticFeedbackProvider( override fun onLowerBookend() { if (!hasVibratedAtLowerBookend) { - velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale) - vibrateOnEdgeCollision(abs(velocityTracker.xVelocity)) + vibrateOnEdgeCollision(abs(getTrackedVelocity())) hasVibratedAtLowerBookend = true } } override fun onUpperBookend() { if (!hasVibratedAtUpperBookend) { - velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale) - vibrateOnEdgeCollision(abs(velocityTracker.xVelocity)) + vibrateOnEdgeCollision(abs(getTrackedVelocity())) hasVibratedAtUpperBookend = true } } override fun onProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) { - velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale) - vibrateDragTexture(abs(velocityTracker.xVelocity), progress) + vibrateDragTexture(abs(getTrackedVelocity()), progress) hasVibratedAtUpperBookend = false hasVibratedAtLowerBookend = false } + private fun getTrackedVelocity(): Float { + velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale) + return if (velocityTracker.isAxisSupported(config.velocityAxis)) { + velocityTracker.getAxisVelocity(config.velocityAxis) + } else { + 0f + } + } + override fun onProgressJump(@FloatRange(from = 0.0, to = 1.0) progress: Float) {} override fun onSelectAndArrow(@FloatRange(from = 0.0, to = 1.0) progress: Float) {} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt index 12775854c737..cf1d2477c9af 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt @@ -32,6 +32,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map @@ -89,7 +90,6 @@ constructor( val start = (startTime / transitionDuration).toFloat() val chunks = (transitionDuration / duration).toFloat() logger.logCreate(name, start) - var isComplete = true fun stepToValue(step: TransitionStep): Float? { val value = (step.value - start) * chunks @@ -98,17 +98,13 @@ constructor( // middle, it is possible this animation is being skipped but we need to inform // the ViewModels of the last update STARTED -> { - isComplete = false onStart?.invoke() max(0f, min(1f, value)) } // Always send a final value of 1. Because of rounding, [value] may never be // exactly 1. RUNNING -> - if (isComplete) { - null - } else if (value >= 1f) { - isComplete = true + if (value >= 1f) { 1f } else if (value >= 0f) { value @@ -132,6 +128,7 @@ constructor( value } .filterNotNull() + .distinctUntilChanged() } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt index 1dbf1f14b569..693e3b7506fc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt @@ -28,13 +28,16 @@ import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.isDefaultOrientation import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.DozeServiceHost import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow @@ -43,6 +46,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion @@ -54,9 +58,13 @@ class SideFpsProgressBarViewModel @Inject constructor( private val context: Context, - private val fpAuthRepository: DeviceEntryFingerprintAuthRepository, + private val fpAuthRepository: DeviceEntryFingerprintAuthInteractor, private val sfpsSensorInteractor: SideFpsSensorInteractor, + // todo (b/317432075) Injecting DozeServiceHost directly instead of using it through + // DozeInteractor as DozeServiceHost already depends on DozeInteractor. + private val dozeServiceHost: DozeServiceHost, displayStateInteractor: DisplayStateInteractor, + @Main private val mainDispatcher: CoroutineDispatcher, @Application private val applicationScope: CoroutineScope, ) { private val _progress = MutableStateFlow(0.0f) @@ -168,18 +176,21 @@ constructor( return@collectLatest } animatorJob = - fpAuthRepository.authenticationStatus - .onEach { authStatus -> + combine( + sfpsSensorInteractor.authenticationDuration, + fpAuthRepository.authenticationStatus, + ::Pair + ) + .onEach { (authDuration, authStatus) -> when (authStatus) { is AcquiredFingerprintAuthenticationStatus -> { if (authStatus.fingerprintCaptureStarted) { _visible.value = true + dozeServiceHost.fireSideFpsAcquisitionStarted() _animator?.cancel() _animator = ValueAnimator.ofFloat(0.0f, 1.0f) - .setDuration( - sfpsSensorInteractor.authenticationDuration - ) + .setDuration(authDuration) .apply { addUpdateListener { _progress.value = it.animatedValue as Float @@ -209,6 +220,7 @@ constructor( else -> Unit } } + .flowOn(mainDispatcher) .onCompletion { _animator?.cancel() } .launchIn(applicationScope) } diff --git a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt index 919072a63220..171656a48e58 100644 --- a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt @@ -108,4 +108,13 @@ class SideFpsLogger @Inject constructor(@BouncerLog private val buffer: LogBuffe } ) } + + fun authDurationChanged(duration: Long) { + buffer.log( + TAG, + LogLevel.DEBUG, + { long1 = duration }, + { "SideFpsSensor auth duration changed: $long1" } + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index a6c623391bb0..7e06f5a21113 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -87,6 +87,7 @@ import java.util.Locale; import java.util.Objects; import javax.inject.Inject; +import javax.inject.Provider; /** */ @@ -149,6 +150,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { public static final String EXTRA_CONFIRM_ONLY = "extra_confirm_only"; private final Context mContext; + private final SystemUIDialog.Factory mSystemUIDialogFactory; private final NotificationManager mNoMan; private final PowerManager mPowerMan; private final KeyguardManager mKeyguard; @@ -186,11 +188,17 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { /** */ @Inject - public PowerNotificationWarnings(Context context, ActivityStarter activityStarter, - BroadcastSender broadcastSender, Lazy<BatteryController> batteryControllerLazy, - DialogLaunchAnimator dialogLaunchAnimator, UiEventLogger uiEventLogger, - GlobalSettings globalSettings, UserTracker userTracker) { + public PowerNotificationWarnings( + Context context, + ActivityStarter activityStarter, + BroadcastSender broadcastSender, + Lazy<BatteryController> batteryControllerLazy, + DialogLaunchAnimator dialogLaunchAnimator, + UiEventLogger uiEventLogger, + UserTracker userTracker, + SystemUIDialog.Factory systemUIDialogFactory) { mContext = context; + mSystemUIDialogFactory = systemUIDialogFactory; mNoMan = mContext.getSystemService(NotificationManager.class); mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mKeyguard = mContext.getSystemService(KeyguardManager.class); @@ -444,7 +452,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private void showHighTemperatureDialog() { if (mHighTempDialog != null) return; - final SystemUIDialog d = new SystemUIDialog(mContext); + final SystemUIDialog d = mSystemUIDialogFactory.create(); d.setIconAttribute(android.R.attr.alertDialogIcon); d.setTitle(R.string.high_temp_title); d.setMessage(R.string.high_temp_dialog_message); @@ -479,7 +487,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private void showThermalShutdownDialog() { if (mThermalShutdownDialog != null) return; - final SystemUIDialog d = new SystemUIDialog(mContext); + final SystemUIDialog d = mSystemUIDialogFactory.create(); d.setIconAttribute(android.R.attr.alertDialogIcon); d.setTitle(R.string.thermal_shutdown_title); d.setMessage(R.string.thermal_shutdown_dialog_message); @@ -643,7 +651,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private void showStartSaverConfirmation(Bundle extras) { if (mSaverConfirmation != null || mUseExtraSaverConfirmation) return; - final SystemUIDialog d = new SystemUIDialog(mContext); + final SystemUIDialog d = mSystemUIDialogFactory.create(); final boolean confirmOnly = extras.getBoolean(BatterySaverUtils.EXTRA_CONFIRM_TEXT_ONLY); final int batterySaverTriggerMode = extras.getInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER, diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 6f35cfbfb4a5..b5def41fb3c7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -148,7 +148,8 @@ class FgsManagerControllerImpl @Inject constructor( private val deviceConfigProxy: DeviceConfigProxy, private val dialogLaunchAnimator: DialogLaunchAnimator, private val broadcastDispatcher: BroadcastDispatcher, - private val dumpManager: DumpManager + private val dumpManager: DumpManager, + private val systemUIDialogFactory: SystemUIDialog.Factory, ) : Dumpable, FgsManagerController { companion object { @@ -375,7 +376,7 @@ class FgsManagerControllerImpl @Inject constructor( override fun showDialog(expandable: Expandable?) { synchronized(lock) { if (dialog == null) { - val dialog = SystemUIDialog(context) + val dialog = systemUIDialogFactory.create() dialog.setTitle(R.string.fgs_manager_dialog_title) dialog.setMessage(R.string.fgs_manager_dialog_message) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java index ccf7afbe7016..c9b002209fa8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java @@ -55,6 +55,7 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements private final DataSaverController mDataSaverController; private final DialogLaunchAnimator mDialogLaunchAnimator; + private final SystemUIDialog.Factory mSystemUIDialogFactory; @Inject public DataSaverTile( @@ -68,12 +69,14 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements ActivityStarter activityStarter, QSLogger qsLogger, DataSaverController dataSaverController, - DialogLaunchAnimator dialogLaunchAnimator + DialogLaunchAnimator dialogLaunchAnimator, + SystemUIDialog.Factory systemUIDialogFactory ) { super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); mDataSaverController = dataSaverController; mDialogLaunchAnimator = dialogLaunchAnimator; + mSystemUIDialogFactory = systemUIDialogFactory; mDataSaverController.observe(getLifecycle(), this); } @@ -98,7 +101,7 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements // Show a dialog to confirm first. Dialogs shown by the DialogLaunchAnimator must be created // and shown on the main thread, so we post it to the UI handler. mUiHandler.post(() -> { - SystemUIDialog dialog = new SystemUIDialog(mContext); + SystemUIDialog dialog = mSystemUIDialogFactory.create(); dialog.setTitle(com.android.internal.R.string.data_saver_enable_title); dialog.setMessage(com.android.internal.R.string.data_saver_description); dialog.setPositiveButton(com.android.internal.R.string.data_saver_enable_button, diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt index acd7510a6c2a..41cd221186fe 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt @@ -23,7 +23,6 @@ import android.content.DialogInterface.BUTTON_NEUTRAL import android.content.Intent import android.provider.Settings import android.view.LayoutInflater -import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger import com.android.systemui.res.R @@ -44,31 +43,15 @@ import javax.inject.Provider * Controller for [UserDialog]. */ @SysUISingleton -class UserSwitchDialogController @VisibleForTesting constructor( - private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>, - private val activityStarter: ActivityStarter, - private val falsingManager: FalsingManager, - private val dialogLaunchAnimator: DialogLaunchAnimator, - private val uiEventLogger: UiEventLogger, - private val dialogFactory: (Context) -> SystemUIDialog +class UserSwitchDialogController @Inject constructor( + private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>, + private val activityStarter: ActivityStarter, + private val falsingManager: FalsingManager, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val uiEventLogger: UiEventLogger, + private val dialogFactory: SystemUIDialog.Factory ) { - @Inject - constructor( - userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>, - activityStarter: ActivityStarter, - falsingManager: FalsingManager, - dialogLaunchAnimator: DialogLaunchAnimator, - uiEventLogger: UiEventLogger - ) : this( - userDetailViewAdapterProvider, - activityStarter, - falsingManager, - dialogLaunchAnimator, - uiEventLogger, - { SystemUIDialog(it) } - ) - companion object { private const val INTERACTION_JANK_TAG = "switch_user" private val USER_SETTINGS_INTENT = Intent(Settings.ACTION_USER_SETTINGS) @@ -81,7 +64,7 @@ class UserSwitchDialogController @VisibleForTesting constructor( * [userDetailViewAdapterProvider] and show it as launched from [expandable]. */ fun showDialog(context: Context, expandable: Expandable) { - with(dialogFactory(context)) { + with(dialogFactory.create()) { setShowForAllUsers(true) setCanceledOnTouchOutside(true) diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java index f07162377358..9076182def70 100644 --- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayDialogController.java @@ -21,8 +21,10 @@ import android.annotation.SuppressLint; import android.annotation.TestApi; import android.content.Context; import android.content.res.Configuration; +import android.content.res.Resources; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManagerGlobal; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.widget.LinearLayout; @@ -72,20 +74,27 @@ public class RearDisplayDialogController implements private DeviceStateManager.DeviceStateCallback mDeviceStateManagerCallback = new DeviceStateManagerCallback(); - private final Context mContext; private final CommandQueue mCommandQueue; private final Executor mExecutor; + private final Resources mResources; + private final LayoutInflater mLayoutInflater; + private final SystemUIDialog.Factory mSystemUIDialogFactory; - @VisibleForTesting - SystemUIDialog mRearDisplayEducationDialog; + private SystemUIDialog mRearDisplayEducationDialog; @Nullable LinearLayout mDialogViewContainer; @Inject - public RearDisplayDialogController(Context context, CommandQueue commandQueue, - @Main Executor executor) { - mContext = context; + public RearDisplayDialogController( + CommandQueue commandQueue, + @Main Executor executor, + @Main Resources resources, + LayoutInflater layoutInflater, + SystemUIDialog.Factory systemUIDialogFactory) { mCommandQueue = commandQueue; mExecutor = executor; + mResources = resources; + mLayoutInflater = layoutInflater; + mSystemUIDialogFactory = systemUIDialogFactory; } @Override @@ -104,8 +113,7 @@ public class RearDisplayDialogController implements if (mRearDisplayEducationDialog != null && mRearDisplayEducationDialog.isShowing() && mDialogViewContainer != null) { // Refresh the dialog view when configuration is changed. - Context dialogContext = mRearDisplayEducationDialog.getContext(); - View dialogView = createDialogView(dialogContext); + View dialogView = createDialogView(mRearDisplayEducationDialog.getContext()); mDialogViewContainer.removeAllViews(); mDialogViewContainer.addView(dialogView); } @@ -114,9 +122,7 @@ public class RearDisplayDialogController implements private void createAndShowDialog() { mServiceNotified = false; Context dialogContext = mRearDisplayEducationDialog.getContext(); - View dialogView = createDialogView(dialogContext); - mDialogViewContainer = new LinearLayout(dialogContext); mDialogViewContainer.setLayoutParams( new LinearLayout.LayoutParams( @@ -133,11 +139,11 @@ public class RearDisplayDialogController implements private View createDialogView(Context context) { View dialogView; + LayoutInflater inflater = mLayoutInflater.cloneInContext(context); if (mStartedFolded) { - dialogView = View.inflate(context, - R.layout.activity_rear_display_education, null); + dialogView = inflater.inflate(R.layout.activity_rear_display_education, null); } else { - dialogView = View.inflate(context, + dialogView = inflater.inflate( R.layout.activity_rear_display_education_opened, null); } LottieAnimationView animationView = dialogView.findViewById( @@ -172,9 +178,9 @@ public class RearDisplayDialogController implements * Ensures we're not using old values from when the dialog may have been shown previously. */ private void initializeValues(int startingBaseState) { - mRearDisplayEducationDialog = new SystemUIDialog(mContext); + mRearDisplayEducationDialog = mSystemUIDialogFactory.create(); if (mFoldedStates == null) { - mFoldedStates = mContext.getResources().getIntArray( + mFoldedStates = mResources.getIntArray( com.android.internal.R.array.config_foldedDeviceStates); } mStartedFolded = isFoldedState(startingBaseState); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index 600d4afde935..45005cbc28a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -55,11 +55,12 @@ import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import com.android.systemui.util.Assert; +import dagger.Lazy; + import java.util.ArrayList; import javax.inject.Inject; -import dagger.Lazy; import kotlinx.coroutines.ExperimentalCoroutinesApi; /** @@ -175,6 +176,16 @@ public final class DozeServiceHost implements DozeHost { } } + /** + * Notify the registered callback about SPFS fingerprint acquisition started event. + */ + public void fireSideFpsAcquisitionStarted() { + Assert.isMainThread(); + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).onSideFingerprintAcquisitionStarted(); + } + } + void fireNotificationPulse(NotificationEntry entry) { Runnable pulseSuppressedListener = () -> { if (NotificationIconContainerRefactor.isEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index af6da3fb6e51..3394eacddbd8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -149,6 +149,14 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh return create(new DialogDelegate<>(){}, mContext); } + /** Creates a new instance of {@link SystemUIDialog} with no customized behavior. + * + * When you just need a dialog created with a specific {@link Context}, call this. + */ + public SystemUIDialog create(Context context) { + return create(new DialogDelegate<>(){}, context); + } + /** * Creates a new instance of {@link SystemUIDialog} with {@code delegate} as the {@link * Delegate}. diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java index 8087a8755a6e..550a65c01bfc 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java @@ -48,6 +48,8 @@ import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.util.leak.LeakDetector; +import dagger.Lazy; + import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -87,6 +89,7 @@ public class TunerServiceImpl extends TunerService { // Set of all tunables, used for leak detection. private final HashSet<Tunable> mTunables = LeakDetector.ENABLED ? new HashSet<>() : null; private final Context mContext; + private final Lazy<SystemUIDialog.Factory> mSystemUIDialogFactoryLazy; private final LeakDetector mLeakDetector; private final DemoModeController mDemoModeController; @@ -104,9 +107,11 @@ public class TunerServiceImpl extends TunerService { @Main Handler mainHandler, LeakDetector leakDetector, DemoModeController demoModeController, - UserTracker userTracker) { + UserTracker userTracker, + Lazy<SystemUIDialog.Factory> systemUIDialogFactoryLazy) { super(context); mContext = context; + mSystemUIDialogFactoryLazy = systemUIDialogFactoryLazy; mContentResolver = mContext.getContentResolver(); mLeakDetector = leakDetector; mDemoModeController = demoModeController; @@ -301,7 +306,7 @@ public class TunerServiceImpl extends TunerService { @Override public void showResetRequest(Runnable onDisabled) { - SystemUIDialog dialog = new SystemUIDialog(mContext); + SystemUIDialog dialog = mSystemUIDialogFactoryLazy.get().create(); dialog.setShowForAllUsers(true); dialog.setMessage(R.string.remove_from_settings_prompt); dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mContext.getString(R.string.cancel), diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt index bfb5485e47b7..c52571188256 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt @@ -120,7 +120,7 @@ class FontScalingDialogDelegateTest : SysuiTestCase() { fontScalingDialogDelegate ) - whenever(dialogFactory.create(any())).thenReturn(dialog) + whenever(dialogFactory.create(any(), any())).thenReturn(dialog) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt index 640807b110d2..8adee8d81ee4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt @@ -36,15 +36,24 @@ import com.android.systemui.biometrics.shared.model.DisplayRotation.ROTATION_90 import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OFF +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope import com.android.systemui.log.SideFpsLogger import com.android.systemui.log.logcatLogBuffer import com.android.systemui.res.R +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -62,9 +71,10 @@ import org.mockito.junit.MockitoJUnit @SmallTest @RunWith(JUnit4::class) class SideFpsSensorInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() @JvmField @Rule var mockitoRule = MockitoJUnit.rule() - private val testScope = TestScope(StandardTestDispatcher()) + private val testScope = kosmos.testScope private val fingerprintRepository = FakeFingerprintPropertyRepository() @@ -101,6 +111,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { windowManager, displayStateInteractor, Optional.of(fingerprintInteractiveToAuthProvider), + kosmos.keyguardTransitionInteractor, SideFpsLogger(logcatLogBuffer("SfpsLogger")) ) } @@ -129,11 +140,62 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { assertThat(isAvailable).isFalse() } + private suspend fun sendTransition(from: KeyguardState, to: KeyguardState) { + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + listOf( + TransitionStep( + from = from, + to = to, + transitionState = TransitionState.STARTED, + ), + TransitionStep( + from = from, + to = to, + transitionState = TransitionState.FINISHED, + value = 1.0f + ) + ), + testScope + ) + } + @Test - fun authenticationDurationIsAvailableWhenSFPSSensorIsAvailable() = + fun authenticationDurationIsLongerIfScreenIsOff() = testScope.runTest { - assertThat(underTest.authenticationDuration) - .isEqualTo(context.resources.getInteger(R.integer.config_restToUnlockDuration)) + val authenticationDuration by collectLastValue(underTest.authenticationDuration) + val longDuration = + context.resources.getInteger(R.integer.config_restToUnlockDurationScreenOff) + sendTransition(LOCKSCREEN, OFF) + + runCurrent() + assertThat(authenticationDuration).isEqualTo(longDuration) + } + + @Test + fun authenticationDurationIsLongerIfScreenIsDozing() = + testScope.runTest { + val authenticationDuration by collectLastValue(underTest.authenticationDuration) + val longDuration = + context.resources.getInteger(R.integer.config_restToUnlockDurationScreenOff) + sendTransition(LOCKSCREEN, DOZING) + runCurrent() + assertThat(authenticationDuration).isEqualTo(longDuration) + } + + @Test + fun authenticationDurationIsShorterIfScreenIsNotDozingOrOff() = + testScope.runTest { + val authenticationDuration by collectLastValue(underTest.authenticationDuration) + val shortDuration = + context.resources.getInteger(R.integer.config_restToUnlockDurationDefault) + val allOtherKeyguardStates = KeyguardState.entries.filter { it != OFF && it != DOZING } + + allOtherKeyguardStates.forEach { destinationState -> + sendTransition(OFF, destinationState) + + runCurrent() + assertThat(authenticationDuration).isEqualTo(shortDuration) + } } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt index cb261789d7bf..755fa021b07c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt @@ -75,6 +75,7 @@ import com.android.systemui.unfold.compat.ScreenSizeFoldProvider import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import java.util.Optional @@ -82,6 +83,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -235,15 +237,18 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { windowManager, displayStateInteractor, Optional.of(fingerprintInteractiveToAuthProvider), + mock(), SideFpsLogger(logcatLogBuffer("SfpsLogger")) ) sideFpsProgressBarViewModel = SideFpsProgressBarViewModel( mContext, - deviceEntryFingerprintAuthRepository, + mock(), sfpsSensorInteractor, + mock(), displayStateInteractor, + UnconfinedTestDispatcher(), testScope.backgroundScope, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt index 823b952d9888..bdca948da6e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt @@ -72,6 +72,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.unfold.compat.ScreenSizeFoldProvider import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -238,15 +239,18 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { windowManager, displayStateInteractor, Optional.of(fingerprintInteractiveToAuthProvider), + mock(), SideFpsLogger(logcatLogBuffer("SfpsLogger")) ) sideFpsProgressBarViewModel = SideFpsProgressBarViewModel( mContext, - deviceEntryFingerprintAuthRepository, + mock(), sfpsSensorInteractor, + mock(), displayStateInteractor, + StandardTestDispatcher(), testScope.backgroundScope, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java index 4022d4388ab1..3ff43c6a3787 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/BroadcastDialogDelegateTest.java @@ -28,8 +28,6 @@ import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; -import android.view.LayoutInflater; -import android.view.View; import android.widget.Button; import android.widget.TextView; @@ -95,7 +93,7 @@ public class BroadcastDialogDelegateTest extends SysuiTestCase { mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true); when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); - when(mSystemUIDialogFactory.create(any())).thenReturn(mDialog); + when(mSystemUIDialogFactory.create(any(), any())).thenReturn(mDialog); mBroadcastDialogDelegate = new BroadcastDialogDelegate( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt index 65f68f9df3e1..35ac2ae4ed44 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogDelegateTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.FeatureFlags import com.android.systemui.model.SysUiState import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.phone.DialogDelegate import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -69,7 +70,8 @@ class ContrastDialogDelegateTest : SysuiTestCase() { mDependency.injectTestDependency(SysUiState::class.java, sysuiState) mDependency.injectMockDependency(DialogLaunchAnimator::class.java) whenever(sysuiState.setFlag(any(), any())).thenReturn(sysuiState) - whenever(sysuiDialogFactory.create(any())).thenReturn(sysuiDialog) + whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java))) + .thenReturn(sysuiDialog) whenever(sysuiDialog.layoutInflater).thenReturn(LayoutInflater.from(mContext)) whenever(mockUserTracker.userId).thenReturn(context.userId) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt index 4e8f86615522..7f0ea9a7a6d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt @@ -17,34 +17,48 @@ package com.android.systemui.controls.management +import android.content.Context import android.content.DialogInterface import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidTestingRunner::class) class PanelConfirmationDialogFactoryTest : SysuiTestCase() { + @Mock private lateinit var mockDialog : SystemUIDialog + @Mock private lateinit var mockDialogFactory : SystemUIDialog.Factory + private lateinit var factory : PanelConfirmationDialogFactory + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + whenever(mockDialogFactory.create(any(Context::class.java))).thenReturn(mockDialog) + whenever(mockDialog.context).thenReturn(mContext) + factory = PanelConfirmationDialogFactory(mockDialogFactory) + } + @Test fun testDialogHasCorrectInfo() { - val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } - val factory = PanelConfirmationDialogFactory { mockDialog } val appName = "appName" - factory.createConfirmationDialog(context, appName) {} + factory.createConfirmationDialog(mContext, appName) {} verify(mockDialog).setCanceledOnTouchOutside(true) verify(mockDialog) @@ -55,12 +69,9 @@ class PanelConfirmationDialogFactoryTest : SysuiTestCase() { @Test fun testDialogPositiveButton() { - val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } - val factory = PanelConfirmationDialogFactory { mockDialog } - var response: Boolean? = null - factory.createConfirmationDialog(context, "") { response = it } + factory.createConfirmationDialog(mContext,"") { response = it } val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor() verify(mockDialog).setPositiveButton(eq(R.string.controls_dialog_ok), capture(captor)) @@ -72,12 +83,9 @@ class PanelConfirmationDialogFactoryTest : SysuiTestCase() { @Test fun testDialogNeutralButton() { - val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } - val factory = PanelConfirmationDialogFactory { mockDialog } - var response: Boolean? = null - factory.createConfirmationDialog(context, "") { response = it } + factory.createConfirmationDialog(mContext, "") { response = it } val captor: ArgumentCaptor<DialogInterface.OnClickListener> = argumentCaptor() verify(mockDialog).setNeutralButton(eq(R.string.cancel), capture(captor)) @@ -89,12 +97,9 @@ class PanelConfirmationDialogFactoryTest : SysuiTestCase() { @Test fun testDialogCancel() { - val mockDialog: SystemUIDialog = mock() { `when`(context).thenReturn(mContext) } - val factory = PanelConfirmationDialogFactory { mockDialog } - var response: Boolean? = null - factory.createConfirmationDialog(context, "") { response = it } + factory.createConfirmationDialog(mContext, "") { response = it } val captor: ArgumentCaptor<DialogInterface.OnCancelListener> = argumentCaptor() verify(mockDialog).setOnCancelListener(capture(captor)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt index 8eebceebe874..38c6a0e236ed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt @@ -17,17 +17,23 @@ package com.android.systemui.controls.ui +import android.content.Context import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.res.R import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.FakeSystemUIDialogController +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any import org.mockito.Mockito.eq import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @@ -37,18 +43,24 @@ class ControlsDialogsFactoryTest : SysuiTestCase() { const val APP_NAME = "Test App" } - private val fakeDialogController = FakeSystemUIDialogController() + @Mock + private lateinit var mockDialogFactory : SystemUIDialog.Factory + + private val fakeDialogController = FakeSystemUIDialogController(mContext) private lateinit var underTest: ControlsDialogsFactory @Before fun setup() { - underTest = ControlsDialogsFactory { fakeDialogController.dialog } + MockitoAnnotations.initMocks(this) + whenever(mockDialogFactory.create(any(Context::class.java))) + .thenReturn(fakeDialogController.dialog) + underTest = ControlsDialogsFactory(mockDialogFactory) } @Test fun testCreatesRemoveAppDialog() { - val dialog = underTest.createRemoveAppDialog(context, APP_NAME) {} + val dialog = underTest.createRemoveAppDialog(mContext, APP_NAME) {} verify(dialog) .setTitle( @@ -60,7 +72,7 @@ class ControlsDialogsFactoryTest : SysuiTestCase() { @Test fun testPositiveClickRemoveAppDialogWorks() { var dialogResult: Boolean? = null - underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it } + underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it } fakeDialogController.clickPositive() @@ -70,7 +82,7 @@ class ControlsDialogsFactoryTest : SysuiTestCase() { @Test fun testNeutralClickRemoveAppDialogWorks() { var dialogResult: Boolean? = null - underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it } + underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it } fakeDialogController.clickNeutral() @@ -80,7 +92,7 @@ class ControlsDialogsFactoryTest : SysuiTestCase() { @Test fun testCancelRemoveAppDialogWorks() { var dialogResult: Boolean? = null - underTest.createRemoveAppDialog(context, APP_NAME) { dialogResult = it } + underTest.createRemoveAppDialog(mContext, APP_NAME) { dialogResult = it } fakeDialogController.cancel() diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index 11bd9cb240a5..36ae0c740c48 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -51,6 +51,7 @@ import com.android.systemui.flags.FeatureFlags import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSystemUIDialogController import com.android.systemui.util.concurrency.FakeExecutor @@ -97,9 +98,10 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository @Mock lateinit var featureFlags: FeatureFlags @Mock lateinit var packageManager: PackageManager + @Mock lateinit var systemUIDialogFactory: SystemUIDialog.Factory private val preferredPanelRepository = FakeSelectedComponentRepository() - private val fakeDialogController = FakeSystemUIDialogController() + private lateinit var fakeDialogController: FakeSystemUIDialogController private val uiExecutor = FakeExecutor(FakeSystemClock()) private val bgExecutor = FakeExecutor(FakeSystemClock()) @@ -114,6 +116,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { fun setup() { MockitoAnnotations.initMocks(this) + fakeDialogController = FakeSystemUIDialogController(mContext) + whenever(systemUIDialogFactory.create(any(Context::class.java))) + .thenReturn(fakeDialogController.dialog) controlsSettingsRepository = FakeControlsSettingsRepository() // This way, it won't be cloned every time `LayoutInflater.fromContext` is called, but we @@ -146,10 +151,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() { authorizedPanelsRepository, preferredPanelRepository, featureFlags, - ControlsDialogsFactory { - isRemoveAppDialogCreated = true - fakeDialogController.dialog - }, + ControlsDialogsFactory(systemUIDialogFactory), dumpManager, ) `when`(userTracker.userId).thenReturn(0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt index ab6bc2ca2dda..66fdf538e284 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.haptics.slider import android.os.VibrationAttributes import android.os.VibrationEffect import android.view.VelocityTracker -import android.view.animation.AccelerateInterpolator import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -51,8 +50,6 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { private val lowTickDuration = 12 // Mocked duration of a low tick private val dragTextureThresholdMillis = lowTickDuration * config.numberOfLowTicks + config.deltaMillisForDragInterval - private val progressInterpolator = AccelerateInterpolator(config.progressInterpolatorFactor) - private val velocityInterpolator = AccelerateInterpolator(config.velocityInterpolatorFactor) private lateinit var sliderHapticFeedbackProvider: SliderHapticFeedbackProvider @Before @@ -60,7 +57,9 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(vibratorHelper.getPrimitiveDurations(any())) .thenReturn(intArrayOf(lowTickDuration)) - whenever(velocityTracker.xVelocity).thenReturn(config.maxVelocityToScale) + whenever(velocityTracker.isAxisSupported(config.velocityAxis)).thenReturn(true) + whenever(velocityTracker.getAxisVelocity(config.velocityAxis)) + .thenReturn(config.maxVelocityToScale) sliderHapticFeedbackProvider = SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, config, clock) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java index 6248bb1009dc..1a303b08b396 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java @@ -55,6 +55,7 @@ import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.util.NotificationChannels; import com.android.systemui.util.settings.FakeGlobalSettings; @@ -77,7 +78,6 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { public static final String FORMATTED_45M = "0h 45m"; public static final String FORMATTED_HOUR = "1h 0m"; private final NotificationManager mMockNotificationManager = mock(NotificationManager.class); - private final GlobalSettings mGlobalSettings = new FakeGlobalSettings(); private PowerNotificationWarnings mPowerNotificationWarnings; @Mock @@ -90,6 +90,10 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { private UserTracker mUserTracker; @Mock private View mView; + @Mock + private SystemUIDialog.Factory mSystemUIDialogFactory; + @Mock + private SystemUIDialog mSystemUIDialog; private BroadcastReceiver mReceiver; @@ -113,9 +117,16 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser()); when(mUserTracker.getUserHandle()).thenReturn( UserHandle.of(ActivityManager.getCurrentUser())); - mPowerNotificationWarnings = new PowerNotificationWarnings(wrapper, starter, - broadcastSender, () -> mBatteryController, mDialogLaunchAnimator, mUiEventLogger, - mGlobalSettings, mUserTracker); + when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog); + mPowerNotificationWarnings = new PowerNotificationWarnings( + wrapper, + starter, + broadcastSender, + () -> mBatteryController, + mDialogLaunchAnimator, + mUiEventLogger, + mUserTracker, + mSystemUIDialogFactory); BatteryStateSnapshot snapshot = new BatteryStateSnapshot(100, false, false, 1, BatteryManager.BATTERY_HEALTH_GOOD, 5, 15); mPowerNotificationWarnings.updateSnapshot(snapshot); @@ -251,7 +262,7 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { verify(mDialogLaunchAnimator, never()).showFromView(any(), any()); - assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue(); + verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show(); mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss(); } @@ -266,7 +277,7 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { verify(mDialogLaunchAnimator, never()).showFromView(any(), any()); - assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue(); + verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show(); mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java index f5a3becc7017..698868d67071 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.app.IActivityManager; import android.app.IForegroundServiceObserver; @@ -53,6 +54,7 @@ import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.util.DeviceConfigProxyFake; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -95,6 +97,10 @@ public class FgsManagerControllerTest extends SysuiTestCase { BroadcastDispatcher mBroadcastDispatcher; @Mock DumpManager mDumpManager; + @Mock + SystemUIDialog.Factory mSystemUIDialogFactory; + @Mock + SystemUIDialog mSystemUIDialog; private FgsManagerController mFmc; @@ -114,6 +120,7 @@ public class FgsManagerControllerTest extends SysuiTestCase { mSystemClock = new FakeSystemClock(); mMainExecutor = new FakeExecutor(mSystemClock); mBackgroundExecutor = new FakeExecutor(mSystemClock); + when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog); mUserProfiles = new ArrayList<>(); Mockito.doReturn(mUserProfiles).when(mUserTracker).getUserProfiles(); @@ -325,7 +332,8 @@ public class FgsManagerControllerTest extends SysuiTestCase { mDeviceConfigProxyFake, mDialogLaunchAnimator, mBroadcastDispatcher, - mDumpManager + mDumpManager, + mSystemUIDialogFactory ); fmc.init(); Assert.assertTrue(fmc.getIncludesUserVisibleJobs()); @@ -351,7 +359,8 @@ public class FgsManagerControllerTest extends SysuiTestCase { mDeviceConfigProxyFake, mDialogLaunchAnimator, mBroadcastDispatcher, - mDumpManager + mDumpManager, + mSystemUIDialogFactory ); fmc.init(); Assert.assertFalse(fmc.getIncludesUserVisibleJobs()); @@ -457,7 +466,8 @@ public class FgsManagerControllerTest extends SysuiTestCase { mDeviceConfigProxyFake, mDialogLaunchAnimator, mBroadcastDispatcher, - mDumpManager + mDumpManager, + mSystemUIDialogFactory ); result.init(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt index 51e95be3611b..c109a1e95f66 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt @@ -32,7 +32,9 @@ import com.android.systemui.qs.QSHost import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.DataSaverController +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before @@ -49,8 +51,6 @@ class DataSaverTileTest : SysuiTestCase() { @Mock private lateinit var mHost: QSHost @Mock private lateinit var mMetricsLogger: MetricsLogger - @Mock private lateinit var mStatusBarStateController: StatusBarStateController - @Mock private lateinit var mActivityStarter: ActivityStarter @Mock private lateinit var mQsLogger: QSLogger private val falsingManager = FalsingManagerFake() @Mock private lateinit var statusBarStateController: StatusBarStateController @@ -58,6 +58,8 @@ class DataSaverTileTest : SysuiTestCase() { @Mock private lateinit var dataSaverController: DataSaverController @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator @Mock private lateinit var uiEventLogger: QsEventLogger + @Mock private lateinit var systemUIDialogFactory: SystemUIDialog.Factory + @Mock private lateinit var systemUIDialog: SystemUIDialog private lateinit var testableLooper: TestableLooper private lateinit var tile: DataSaverTile @@ -67,7 +69,8 @@ class DataSaverTileTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) - Mockito.`when`(mHost.context).thenReturn(mContext) + whenever(mHost.context).thenReturn(mContext) + whenever(systemUIDialogFactory.create()).thenReturn(systemUIDialog) tile = DataSaverTile( @@ -81,7 +84,8 @@ class DataSaverTileTest : SysuiTestCase() { activityStarter, mQsLogger, dataSaverController, - dialogLaunchAnimator + dialogLaunchAnimator, + systemUIDialogFactory ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt index 0a34810f4d3f..945490f1983d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -56,6 +57,8 @@ import org.mockito.MockitoAnnotations class UserSwitchDialogControllerTest : SysuiTestCase() { @Mock + private lateinit var dialogFactory: SystemUIDialog.Factory + @Mock private lateinit var dialog: SystemUIDialog @Mock private lateinit var falsingManager: FalsingManager @@ -80,7 +83,8 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - `when`(dialog.context).thenReturn(mContext) + whenever(dialog.context).thenReturn(mContext) + whenever(dialogFactory.create()).thenReturn(dialog) controller = UserSwitchDialogController( { userDetailViewAdapter }, @@ -88,7 +92,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { falsingManager, dialogLaunchAnimator, uiEventLogger, - { dialog } + dialogFactory ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java index 273ce85f89f5..35bf7753358e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayDialogControllerTest.java @@ -18,25 +18,42 @@ package com.android.systemui.reardisplay; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotSame; -import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.res.Configuration; +import android.content.res.Resources; import android.hardware.devicestate.DeviceStateManager; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.LayoutInflater; +import android.view.View; import android.widget.TextView; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.res.R; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.model.SysUiState; +import com.android.systemui.res.R; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -45,24 +62,49 @@ public class RearDisplayDialogControllerTest extends SysuiTestCase { @Mock private CommandQueue mCommandQueue; + @Mock + private SystemUIDialog.Factory mSystemUIDialogFactory; + @Mock + private SystemUIDialog mSystemUIDialog; + private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); + @Mock + private SysUiState mSysUiState; + @Mock + private Resources mResources; - private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); + LayoutInflater mLayoutInflater = LayoutInflater.from(mContext); + private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); private static final int CLOSED_BASE_STATE = 0; private static final int OPEN_BASE_STATE = 1; + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM, true); + when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); + when(mSystemUIDialogFactory.create()).thenReturn(mSystemUIDialog); + when(mSystemUIDialog.getContext()).thenReturn(mContext); + } @Test public void testClosedDialogIsShown() { - RearDisplayDialogController controller = new RearDisplayDialogController(mContext, - mCommandQueue, mFakeExecutor); + RearDisplayDialogController controller = new RearDisplayDialogController( + mCommandQueue, + mFakeExecutor, + mResources, + mLayoutInflater, + mSystemUIDialogFactory); controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback()); controller.setFoldedStates(new int[]{0}); controller.setAnimationRepeatCount(0); controller.showRearDisplayDialog(CLOSED_BASE_STATE); - assertTrue(controller.mRearDisplayEducationDialog.isShowing()); - TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById( + verify(mSystemUIDialog).show(); + + View container = getDialogViewContainer(); + TextView deviceClosedTitleTextView = container.findViewById( R.id.rear_display_title_text_view); assertEquals(deviceClosedTitleTextView.getText().toString(), getContext().getResources().getString( @@ -71,20 +113,28 @@ public class RearDisplayDialogControllerTest extends SysuiTestCase { @Test public void testClosedDialogIsRefreshedOnConfigurationChange() { - RearDisplayDialogController controller = new RearDisplayDialogController(mContext, - mCommandQueue, mFakeExecutor); + RearDisplayDialogController controller = new RearDisplayDialogController( + mCommandQueue, + mFakeExecutor, + mResources, + mLayoutInflater, + mSystemUIDialogFactory); controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback()); controller.setFoldedStates(new int[]{0}); controller.setAnimationRepeatCount(0); controller.showRearDisplayDialog(CLOSED_BASE_STATE); - assertTrue(controller.mRearDisplayEducationDialog.isShowing()); - TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById( + verify(mSystemUIDialog).show(); + View container = getDialogViewContainer(); + TextView deviceClosedTitleTextView = container.findViewById( R.id.rear_display_title_text_view); + reset(mSystemUIDialog); + when(mSystemUIDialog.isShowing()).thenReturn(true); + when(mSystemUIDialog.getContext()).thenReturn(mContext); + controller.onConfigChanged(new Configuration()); - assertTrue(controller.mRearDisplayEducationDialog.isShowing()); - TextView deviceClosedTitleTextView2 = controller.mRearDisplayEducationDialog.findViewById( + TextView deviceClosedTitleTextView2 = container.findViewById( R.id.rear_display_title_text_view); assertNotSame(deviceClosedTitleTextView, deviceClosedTitleTextView2); @@ -92,22 +142,33 @@ public class RearDisplayDialogControllerTest extends SysuiTestCase { @Test public void testOpenDialogIsShown() { - RearDisplayDialogController controller = new RearDisplayDialogController(mContext, - mCommandQueue, mFakeExecutor); + RearDisplayDialogController controller = new RearDisplayDialogController( + mCommandQueue, + mFakeExecutor, + mResources, + mLayoutInflater, + mSystemUIDialogFactory); controller.setDeviceStateManagerCallback(new TestDeviceStateManagerCallback()); controller.setFoldedStates(new int[]{0}); controller.setAnimationRepeatCount(0); controller.showRearDisplayDialog(OPEN_BASE_STATE); - assertTrue(controller.mRearDisplayEducationDialog.isShowing()); - TextView deviceClosedTitleTextView = controller.mRearDisplayEducationDialog.findViewById( + verify(mSystemUIDialog).show(); + View container = getDialogViewContainer(); + TextView deviceClosedTitleTextView = container.findViewById( R.id.rear_display_title_text_view); assertEquals(deviceClosedTitleTextView.getText().toString(), getContext().getResources().getString( R.string.rear_display_unfolded_bottom_sheet_title)); } + private View getDialogViewContainer() { + ArgumentCaptor<View> viewCaptor = ArgumentCaptor.forClass(View.class); + verify(mSystemUIDialog).setView(viewCaptor.capture()); + + return viewCaptor.getValue(); + } /** * Empty device state manager callbacks, so we can verify that the correct * dialogs are being created regardless of device state of the test device. diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt index 0c9ce0f145f1..697b5087a865 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSystemUIDialogController.kt @@ -17,6 +17,7 @@ package com.android.systemui.util +import android.content.Context import android.content.DialogInterface import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.mockito.any @@ -27,13 +28,15 @@ import org.mockito.Mockito.doAnswer import org.mockito.Mockito.verify import org.mockito.stubbing.Stubber -class FakeSystemUIDialogController { +class FakeSystemUIDialogController(context: Context) { val dialog: SystemUIDialog = mock() + private val clickListeners: MutableMap<Int, DialogInterface.OnClickListener> = mutableMapOf() init { + whenever(dialog.context).thenReturn(context) saveListener(DialogInterface.BUTTON_POSITIVE) .whenever(dialog) .setPositiveButton(any(), any()) diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java index 52a8f9ed10d4..a6ed8464128a 100644 --- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java @@ -286,32 +286,33 @@ class AssociationRequestsProcessor { selfManaged, /* notifyOnDeviceNearby */ false, /* revoked */ false, timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0); - if (deviceProfile != null) { - // If the "Device Profile" is specified, make the companion application a holder of the - // corresponding role. - addRoleHolderForAssociation(mService.getContext(), association, success -> { - if (success) { - addAssociationToStore(association, deviceProfile); - - sendCallbackAndFinish(association, callback, resultReceiver); - } else { - Slog.e(TAG, "Failed to add u" + userId + "\\" + packageName - + " to the list of " + deviceProfile + " holders."); - - sendCallbackAndFinish(null, callback, resultReceiver); - } - }); - } else { - addAssociationToStore(association, null); - - sendCallbackAndFinish(association, callback, resultReceiver); - } + // Add role holder for association (if specified) and add new association to store. + maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver); // Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since // maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case // that there are other devices with the same profile, so the role holder won't be removed. } + public void maybeGrantRoleAndStoreAssociation(@NonNull AssociationInfo association, + @Nullable IAssociationRequestCallback callback, + @Nullable ResultReceiver resultReceiver) { + // If the "Device Profile" is specified, make the companion application a holder of the + // corresponding role. + // If it is null, then the operation will succeed without granting any role. + addRoleHolderForAssociation(mService.getContext(), association, success -> { + if (success) { + addAssociationToStore(association); + sendCallbackAndFinish(association, callback, resultReceiver); + } else { + Slog.e(TAG, "Failed to add u" + association.getUserId() + + "\\" + association.getPackageName() + + " to the list of " + association.getDeviceProfile() + " holders."); + sendCallbackAndFinish(null, callback, resultReceiver); + } + }); + } + public void enableSystemDataSync(int associationId, int flags) { AssociationInfo association = mAssociationStore.getAssociationById(associationId); AssociationInfo updated = (new AssociationInfo.Builder(association)) @@ -326,15 +327,14 @@ class AssociationRequestsProcessor { mAssociationStore.updateAssociation(updated); } - private void addAssociationToStore(@NonNull AssociationInfo association, - @Nullable String deviceProfile) { + private void addAssociationToStore(@NonNull AssociationInfo association) { Slog.i(TAG, "New CDM association created=" + association); mAssociationStore.addAssociation(association); mService.updateSpecialAccessPermissionForAssociatedPackage(association); - logCreateAssociation(deviceProfile); + logCreateAssociation(association.getDeviceProfile()); } private void sendCallbackAndFinish(@Nullable AssociationInfo association, diff --git a/services/companion/java/com/android/server/companion/RolesUtils.java b/services/companion/java/com/android/server/companion/RolesUtils.java index 163f614fb65d..af9d2d783100 100644 --- a/services/companion/java/com/android/server/companion/RolesUtils.java +++ b/services/companion/java/com/android/server/companion/RolesUtils.java @@ -47,6 +47,17 @@ final class RolesUtils { return roleHolders.contains(packageName); } + /** + * Attempt to add the association's companion app as the role holder for the device profile + * specified in the association. If the association does not have any device profile specified, + * then the operation will always be successful as a no-op. + * + * @param context + * @param associationInfo the association for which the role should be granted to the app + * @param roleGrantResult the result callback for adding role holder. True if successful, and + * false if failed. If the association does not have any device profile + * specified, then the operation will always be successful as a no-op. + */ static void addRoleHolderForAssociation( @NonNull Context context, @NonNull AssociationInfo associationInfo, @NonNull Consumer<Boolean> roleGrantResult) { @@ -55,7 +66,11 @@ final class RolesUtils { } final String deviceProfile = associationInfo.getDeviceProfile(); - if (deviceProfile == null) return; + if (deviceProfile == null) { + // If no device profile is specified, then no-op and resolve callback with success. + roleGrantResult.accept(true); + return; + } final RoleManager roleManager = context.getSystemService(RoleManager.class); diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING index 575db01931e6..e90910a13b3b 100644 --- a/services/core/java/com/android/server/am/TEST_MAPPING +++ b/services/core/java/com/android/server/am/TEST_MAPPING @@ -146,6 +146,15 @@ { "include-filter": "android.app.cts.ServiceTest" }, { "include-filter": "android.app.cts.ActivityManagerFgsBgStartTest" } ] + }, + { + "name": "CtsStatsdAtomHostTestCases", + "options": [ + { "include-filter": "android.cts.statsdatom.appexit.AppExitHostTest" }, + { "exclude-annotation": "androidx.test.filters.LargeTest" }, + { "exclude-annotation": "androidx.test.filters.FlakyTest" }, + { "exclude-annotation": "org.junit.Ignore" } + ] } ] } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java index 0bb6141583d5..90da74ccaa1c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java @@ -147,7 +147,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe gestureAvailabilityDispatcher, () -> mCurrentUserId, getUserSwitchCallback())); mLockoutTracker = new LockoutFrameworkImpl(getContext(), userId -> mLockoutResetDispatcher.notifyLockoutResetCallbacks( - getSensorProperties().sensorId)); + getSensorProperties().sensorId), getHandler()); } @Override diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java index 2f77275890dd..0e05a7923db4 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java @@ -19,6 +19,7 @@ package com.android.server.biometrics.sensors.fingerprint.hidl; import static android.Manifest.permission.RESET_FINGERPRINT_LOCKOUT; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; @@ -81,19 +82,30 @@ public class LockoutFrameworkImpl implements LockoutTracker { @NonNull LockoutResetCallback lockoutResetCallback) { this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId, new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)); + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE), + null /* handler */); + } + + public LockoutFrameworkImpl(@NonNull Context context, + @NonNull LockoutResetCallback lockoutResetCallback, + @NonNull Handler handler) { + this(context, lockoutResetCallback, (userId) -> PendingIntent.getBroadcast(context, userId, + new Intent(ACTION_LOCKOUT_RESET).putExtra(KEY_LOCKOUT_RESET_USER, userId), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE), + handler); } @VisibleForTesting LockoutFrameworkImpl(@NonNull Context context, @NonNull LockoutResetCallback lockoutResetCallback, - @NonNull Function<Integer, PendingIntent> lockoutResetIntent) { + @NonNull Function<Integer, PendingIntent> lockoutResetIntent, + @Nullable Handler handler) { mLockoutResetCallback = lockoutResetCallback; mTimedLockoutCleared = new SparseBooleanArray(); mFailedAttempts = new SparseIntArray(); mAlarmManager = context.getSystemService(AlarmManager.class); mLockoutReceiver = new LockoutReceiver(); - mHandler = new Handler(Looper.getMainLooper()); + mHandler = handler == null ? new Handler(Looper.getMainLooper()) : handler; mLockoutResetIntent = lockoutResetIntent; context.registerReceiver(mLockoutReceiver, new IntentFilter(ACTION_LOCKOUT_RESET), diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index 56a94ec06ad4..49f607095b90 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -1424,7 +1424,11 @@ public class ClipboardService extends SystemService { String defaultIme = Settings.Secure.getStringForUser(getContext().getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD, userId); if (!TextUtils.isEmpty(defaultIme)) { - final String imePkg = ComponentName.unflattenFromString(defaultIme).getPackageName(); + final ComponentName imeComponent = ComponentName.unflattenFromString(defaultIme); + if (imeComponent == null) { + return false; + } + final String imePkg = imeComponent.getPackageName(); return imePkg.equals(packageName); } return false; diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java index 66807aeb6629..f96bb8fb6c6f 100644 --- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java +++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java @@ -52,6 +52,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.OptionalInt; +import java.util.function.IntConsumer; // TODO(b/210039666): See if we can make this class thread-safe. final class HandwritingModeController { @@ -84,14 +85,14 @@ final class HandwritingModeController { private boolean mDelegatorFromDefaultHomePackage; private Runnable mDelegationIdleTimeoutRunnable; private Handler mDelegationIdleTimeoutHandler; - + private IntConsumer mPointerToolTypeConsumer; private HandwritingEventReceiverSurface mHandwritingSurface; private int mCurrentRequestId; @AnyThread HandwritingModeController(Context context, Looper uiThreadLooper, - Runnable inkWindowInitRunnable) { + Runnable inkWindowInitRunnable, IntConsumer toolTypeConsumer) { mContext = context; mLooper = uiThreadLooper; mCurrentDisplayId = Display.INVALID_DISPLAY; @@ -100,6 +101,7 @@ final class HandwritingModeController { mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mCurrentRequestId = 0; mInkWindowInitRunnable = inkWindowInitRunnable; + mPointerToolTypeConsumer = toolTypeConsumer; } /** @@ -355,6 +357,11 @@ final class HandwritingModeController { return false; } final MotionEvent event = (MotionEvent) ev; + if (mPointerToolTypeConsumer != null && event.getAction() == MotionEvent.ACTION_DOWN) { + int toolType = event.getToolType(event.getActionIndex()); + // notify IME of change in tool type. + mPointerToolTypeConsumer.accept(toolType); + } if (!event.isStylusPointer()) { return false; } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 4f998ee2cfa2..24bcb4ece7aa 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -124,6 +124,7 @@ import android.view.WindowManager.DisplayImePolicy; import android.view.WindowManager.LayoutParams; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputConnection; @@ -206,6 +207,7 @@ import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntConsumer; /** * This class provides a system service that manages input methods. @@ -1713,8 +1715,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor); mNonPreemptibleInputMethods = mRes.getStringArray( com.android.internal.R.array.config_nonPreemptibleInputMethods); + IntConsumer toolTypeConsumer = + Flags.useHandwritingListenerForTooltype() + ? toolType -> onUpdateEditorToolType(toolType) : null; mHwController = new HandwritingModeController(mContext, thread.getLooper(), - new InkWindowInitializer()); + new InkWindowInitializer(), toolTypeConsumer); registerDeviceListenerAndCheckStylusSupport(); } @@ -1735,6 +1740,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } + private void onUpdateEditorToolType(int toolType) { + synchronized (ImfLock.class) { + IInputMethodInvoker curMethod = getCurMethodLocked(); + if (curMethod != null) { + curMethod.updateEditorToolType(toolType); + } + } + } + @GuardedBy("ImfLock.class") private void resetDefaultImeLocked(Context context) { // Do not reset the default (current) IME when it is a 3rd-party IME @@ -3525,7 +3539,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME); mCurStatsToken = null; - if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) { + if (!Flags.useHandwritingListenerForTooltype() + && lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) { curMethod.updateEditorToolType(lastClickToolType); } mVisibilityApplier.performShowIme(windowToken, statsToken, diff --git a/services/core/java/com/android/server/pdb/TEST_MAPPING b/services/core/java/com/android/server/pdb/TEST_MAPPING index 1aa8601bdcf9..9e9802354a4d 100644 --- a/services/core/java/com/android/server/pdb/TEST_MAPPING +++ b/services/core/java/com/android/server/pdb/TEST_MAPPING @@ -1,5 +1,5 @@ { - "postsubmit": [ + "presubmit": [ { "name": "FrameworksServicesTests", "options": [ diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java index 7b5192c4bd6b..e3aba0f6bc6f 100644 --- a/services/core/java/com/android/server/utils/AnrTimer.java +++ b/services/core/java/com/android/server/utils/AnrTimer.java @@ -16,21 +16,30 @@ package com.android.server.utils; +import static android.text.TextUtils.formatSimple; + import android.annotation.NonNull; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.os.Trace; +import android.text.TextUtils; import android.text.format.TimeMigrationUtils; +import android.util.ArrayMap; import android.util.IndentingPrintWriter; import android.util.Log; +import android.util.LongSparseArray; +import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.Keep; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.RingBuffer; +import java.lang.ref.WeakReference; import java.io.PrintWriter; import java.util.Arrays; +import java.util.ArrayList; import java.util.Objects; /** @@ -60,9 +69,14 @@ import java.util.Objects; * is restarted with the extension timeout. If extensions are disabled or if the extension is zero, * the client process is notified of the expiration. * + * <p>Instances use native resources but not system resources when the feature is enabled. + * Instances should be explicitly closed unless they are being closed as part of process + * exit. (So, instances in system server generally need not be explicitly closed since they are + * created during process start and will last until process exit.) + * * @hide */ -public class AnrTimer<V> { +public class AnrTimer<V> implements AutoCloseable { /** * The log tag. @@ -87,6 +101,12 @@ public class AnrTimer<V> { private static final long TRACE_TAG = Trace.TRACE_TAG_ACTIVITY_MANAGER; /** + * Enable tracing from the time a timer expires until it is accepted or discarded. This is + * used to diagnose long latencies in the client. + */ + private static final boolean ENABLE_TRACING = false; + + /** * Return true if the feature is enabled. By default, the value is take from the Flags class * but it can be changed for local testing. */ @@ -103,6 +123,9 @@ public class AnrTimer<V> { } } + /** The default injector. */ + private static final Injector sDefaultInjector = new Injector(); + /** * An error is defined by its issue, the operation that detected the error, the tag of the * affected service, a short stack of the bad call, and the stringified arg associated with @@ -160,41 +183,46 @@ public class AnrTimer<V> { /** A lock for the AnrTimer instance. */ private final Object mLock = new Object(); - /** - * The total number of timers started. - */ + /** The map from client argument to the associated timer ID. */ + @GuardedBy("mLock") + private final ArrayMap<V, Integer> mTimerIdMap = new ArrayMap<>(); + + /** Reverse map from timer ID to client argument. */ + @GuardedBy("mLock") + private final SparseArray<V> mTimerArgMap = new SparseArray<>(); + + /** The highwater mark of started, but not closed, timers. */ + @GuardedBy("mLock") + private int mMaxStarted = 0; + + /** The total number of timers started. */ @GuardedBy("mLock") private int mTotalStarted = 0; - /** - * The total number of errors detected. - */ + /** The total number of errors detected. */ @GuardedBy("mLock") private int mTotalErrors = 0; - /** - * The handler for messages sent from this instance. - */ + /** The total number of timers that have expired. */ + @GuardedBy("mLock") + private int mTotalExpired = 0; + + /** The handler for messages sent from this instance. */ private final Handler mHandler; - /** - * The message type for messages sent from this interface. - */ + /** The message type for messages sent from this interface. */ private final int mWhat; - /** - * A label that identifies the AnrTimer associated with a Timer in log messages. - */ + /** A label that identifies the AnrTimer associated with a Timer in log messages. */ private final String mLabel; - /** - * Whether this timer instance supports extending timeouts. - */ + /** Whether this timer instance supports extending timeouts. */ private final boolean mExtend; - /** - * The top-level switch for the feature enabled or disabled. - */ + /** The injector used to create this instance. This is only used for testing. */ + private final Injector mInjector; + + /** The top-level switch for the feature enabled or disabled. */ private final FeatureSwitch mFeature; /** @@ -223,7 +251,27 @@ public class AnrTimer<V> { mWhat = what; mLabel = label; mExtend = extend; - mFeature = new FeatureDisabled(); + mInjector = injector; + boolean enabled = mInjector.anrTimerServiceEnabled() && nativeTimersSupported(); + mFeature = createFeatureSwitch(enabled); + } + + // Return the correct feature. FeatureEnabled is returned if and only if the feature is + // flag-enabled and if the native shadow was successfully created. Otherwise, FeatureDisabled + // is returned. + private FeatureSwitch createFeatureSwitch(boolean enabled) { + if (!enabled) { + return new FeatureDisabled(); + } else { + try { + return new FeatureEnabled(); + } catch (RuntimeException e) { + // Something went wrong in the native layer. Log the error and fall back on the + // feature-disabled logic. + Log.e(TAG, e.toString()); + return new FeatureDisabled(); + } + } } /** @@ -245,7 +293,7 @@ public class AnrTimer<V> { * @param extend A flag to indicate if expired timers can be granted extensions. */ public AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend) { - this(handler, what, label, extend, new Injector()); + this(handler, what, label, extend, sDefaultInjector); } /** @@ -272,19 +320,44 @@ public class AnrTimer<V> { } /** + * Start a trace on the timer. The trace is laid down in the AnrTimerTrack. + */ + private void traceBegin(int timerId, int pid, int uid, String what) { + if (ENABLE_TRACING) { + final String label = formatSimple("%s(%d,%d,%s)", what, pid, uid, mLabel); + final int cookie = timerId; + Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK, label, cookie); + } + } + + /** + * End a trace on the timer. + */ + private void traceEnd(int timerId) { + if (ENABLE_TRACING) { + final int cookie = timerId; + Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK, cookie); + } + } + + /** * The FeatureSwitch class provides a quick switch between feature-enabled behavior and * feature-disabled behavior. */ private abstract class FeatureSwitch { abstract void start(@NonNull V arg, int pid, int uid, long timeoutMs); - abstract void cancel(@NonNull V arg); + abstract boolean cancel(@NonNull V arg); - abstract void accept(@NonNull V arg); + abstract boolean accept(@NonNull V arg); - abstract void discard(@NonNull V arg); + abstract boolean discard(@NonNull V arg); abstract boolean enabled(); + + abstract void dump(PrintWriter pw, boolean verbose); + + abstract void close(); } /** @@ -301,18 +374,21 @@ public class AnrTimer<V> { /** Cancel a timer by removing the message from the client's handler. */ @Override - void cancel(@NonNull V arg) { + boolean cancel(@NonNull V arg) { mHandler.removeMessages(mWhat, arg); + return true; } /** accept() is a no-op when the feature is disabled. */ @Override - void accept(@NonNull V arg) { + boolean accept(@NonNull V arg) { + return true; } /** discard() is a no-op when the feature is disabled. */ @Override - void discard(@NonNull V arg) { + boolean discard(@NonNull V arg) { + return true; } /** The feature is not enabled. */ @@ -320,12 +396,179 @@ public class AnrTimer<V> { boolean enabled() { return false; } + + /** dump() is a no-op when the feature is disabled. */ + @Override + void dump(PrintWriter pw, boolean verbose) { + } + + /** close() is a no-op when the feature is disabled. */ + @Override + void close() { + } + } + + /** + * A static list of AnrTimer instances. The list is traversed by dumpsys. Only instances + * using native resources are included. + */ + @GuardedBy("sAnrTimerList") + private static final LongSparseArray<WeakReference<AnrTimer>> sAnrTimerList = + new LongSparseArray<>(); + + /** + * The FeatureEnabled class enables the AnrTimer logic. It is used when the AnrTimer service + * is enabled via Flags.anrTimerServiceEnabled. + */ + private class FeatureEnabled extends FeatureSwitch { + + /** + * The native timer that supports this instance. The value is set to non-zero when the + * native timer is created and it is set back to zero when the native timer is freed. + */ + private long mNative = 0; + + /** Fetch the native tag (an integer) for the given label. */ + FeatureEnabled() { + mNative = nativeAnrTimerCreate(mLabel); + if (mNative == 0) throw new IllegalArgumentException("unable to create native timer"); + synchronized (sAnrTimerList) { + sAnrTimerList.put(mNative, new WeakReference(AnrTimer.this)); + } + } + + /** + * Start a timer. + */ + @Override + void start(@NonNull V arg, int pid, int uid, long timeoutMs) { + synchronized (mLock) { + if (mTimerIdMap.containsKey(arg)) { + // There is an existing timer. Cancel it. + cancel(arg); + } + int timerId = nativeAnrTimerStart(mNative, pid, uid, timeoutMs, mExtend); + if (timerId > 0) { + mTimerIdMap.put(arg, timerId); + mTimerArgMap.put(timerId, arg); + mTotalStarted++; + mMaxStarted = Math.max(mMaxStarted, mTimerIdMap.size()); + } else { + throw new RuntimeException("unable to start timer"); + } + } + } + + /** + * Cancel a timer. No error is reported if the timer is not found because some clients + * cancel timers from common code that runs even if a timer was never started. + */ + @Override + boolean cancel(@NonNull V arg) { + synchronized (mLock) { + Integer timer = removeLocked(arg); + if (timer == null) { + return false; + } + if (!nativeAnrTimerCancel(mNative, timer)) { + // There may be an expiration message in flight. Cancel it. + mHandler.removeMessages(mWhat, arg); + return false; + } + return true; + } + } + + /** + * Accept a timer in the framework-level handler. The timeout has been accepted and the + * timeout handler is executing. + */ + @Override + boolean accept(@NonNull V arg) { + synchronized (mLock) { + Integer timer = removeLocked(arg); + if (timer == null) { + notFoundLocked("accept", arg); + return false; + } + nativeAnrTimerAccept(mNative, timer); + traceEnd(timer); + return true; + } + } + + /** + * Discard a timer in the framework-level handler. For whatever reason, the timer is no + * longer interesting. No statistics are collected. Return false if the time was not + * found. + */ + @Override + boolean discard(@NonNull V arg) { + synchronized (mLock) { + Integer timer = removeLocked(arg); + if (timer == null) { + notFoundLocked("discard", arg); + return false; + } + nativeAnrTimerDiscard(mNative, timer); + traceEnd(timer); + return true; + } + } + + /** The feature is enabled. */ + @Override + boolean enabled() { + return true; + } + + /** Dump statistics from the native layer. */ + @Override + void dump(PrintWriter pw, boolean verbose) { + synchronized (mLock) { + if (mNative != 0) { + nativeAnrTimerDump(mNative, verbose); + } else { + pw.println("closed"); + } + } + } + + /** Free native resources. */ + @Override + void close() { + // Remove self from the list of active timers. + synchronized (sAnrTimerList) { + sAnrTimerList.remove(mNative); + } + synchronized (mLock) { + if (mNative != 0) nativeAnrTimerClose(mNative); + mNative = 0; + } + } + + /** + * Delete the entries associated with arg from the maps and return the ID of the timer, if + * any. + */ + @GuardedBy("mLock") + private Integer removeLocked(V arg) { + Integer r = mTimerIdMap.remove(arg); + if (r != null) { + synchronized (mTimerArgMap) { + mTimerArgMap.remove(r); + } + } + return r; + } } /** * Start a timer associated with arg. The same object must be used to cancel, accept, or * discard a timer later. If a timer already exists with the same arg, then the existing timer - * is canceled and a new timer is created. + * is canceled and a new timer is created. The timeout is signed but negative delays are + * nonsensical. Rather than throw an exception, timeouts less than 0ms are forced to 0ms. This + * allows a client to deliver an immediate timeout via the AnrTimer. * * @param arg The key by which the timer is known. This is never examined or modified. * @param pid The Linux process ID of the target being timed. @@ -333,25 +576,39 @@ public class AnrTimer<V> { * @param timeoutMs The timer timeout, in milliseconds. */ public void start(@NonNull V arg, int pid, int uid, long timeoutMs) { + if (timeoutMs < 0) timeoutMs = 0; mFeature.start(arg, pid, uid, timeoutMs); } /** * Cancel the running timer associated with arg. The timer is forgotten. If the timer has - * expired, the call is treated as a discard. No errors are reported if the timer does not - * exist or if the timer has expired. + * expired, the call is treated as a discard. The function returns true if a running timer was + * found, and false if an expired timer was found or if no timer was found. After this call, + * the timer does not exist. + * + * Note: the return value is always true if the feature is not enabled. + * + * @param arg The key by which the timer is known. This is never examined or modified. + * @return True if a running timer was canceled. */ - public void cancel(@NonNull V arg) { - mFeature.cancel(arg); + public boolean cancel(@NonNull V arg) { + return mFeature.cancel(arg); } /** * Accept the expired timer associated with arg. This indicates that the caller considers the - * timer expiration to be a true ANR. (See {@link #discard} for an alternate response.) It is - * an error to accept a running timer, however the running timer will be canceled. + * timer expiration to be a true ANR. (See {@link #discard} for an alternate response.) The + * function returns true if an expired timer was found and false if a running timer was found or + * if no timer was found. After this call, the timer does not exist. It is an error to accept + * a running timer, however, the running timer will be canceled. + * + * Note: the return value is always true if the feature is not enabled. + * + * @param arg The key by which the timer is known. This is never examined or modified. + * @return True if an expired timer was accepted. */ - public void accept(@NonNull V arg) { - mFeature.accept(arg); + public boolean accept(@NonNull V arg) { + return mFeature.accept(arg); } /** @@ -359,11 +616,57 @@ public class AnrTimer<V> { * timer expiration to be a false ANR. ((See {@link #accept} for an alternate response.) One * reason to discard an expired timer is if the process being timed was also being debugged: * such a process could be stopped at a breakpoint and its failure to respond would not be an - * error. It is an error to discard a running timer, however the running timer will be - * canceled. + * error. After this call thie timer does not exist. It is an error to discard a running timer, + * however the running timer will be canceled. + * + * Note: the return value is always true if the feature is not enabled. + * + * @param arg The key by which the timer is known. This is never examined or modified. + * @return True if an expired timer was discarded. + */ + public boolean discard(@NonNull V arg) { + return mFeature.discard(arg); + } + + /** + * The notifier that a timer has fired. The timerId and original pid/uid are supplied. This + * method is called from native code. This method takes mLock so that a timer cannot expire + * in the middle of another operation (like start or cancel). + */ + @Keep + private boolean expire(int timerId, int pid, int uid) { + traceBegin(timerId, pid, uid, "expired"); + V arg = null; + synchronized (mLock) { + arg = mTimerArgMap.get(timerId); + if (arg == null) { + Log.e(TAG, formatSimple("failed to expire timer %s:%d : arg not found", + mLabel, timerId)); + mTotalErrors++; + return false; + } + mTotalExpired++; + } + mHandler.sendMessage(Message.obtain(mHandler, mWhat, arg)); + return true; + } + + /** + * Close the object and free any native resources. */ - public void discard(@NonNull V arg) { - mFeature.discard(arg); + public void close() { + mFeature.close(); + } + + /** + * Ensure any native resources are freed when the object is GC'ed. Best practice is to close + * the object explicitly, but overriding finalize() avoids accidental leaks. + */ + @SuppressWarnings("Finalize") + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); } /** @@ -373,8 +676,11 @@ public class AnrTimer<V> { synchronized (mLock) { pw.format("timer: %s\n", mLabel); pw.increaseIndent(); - pw.format("started=%d errors=%d\n", mTotalStarted, mTotalErrors); + pw.format("started=%d maxStarted=%d running=%d expired=%d errors=%d\n", + mTotalStarted, mMaxStarted, mTimerIdMap.size(), + mTotalExpired, mTotalErrors); pw.decreaseIndent(); + mFeature.dump(pw, false); } } @@ -386,6 +692,13 @@ public class AnrTimer<V> { } /** + * The current time in milliseconds. + */ + private static long now() { + return SystemClock.uptimeMillis(); + } + + /** * Dump all errors to the output stream. */ private static void dumpErrors(IndentingPrintWriter ipw) { @@ -422,23 +735,89 @@ public class AnrTimer<V> { mTotalErrors++; } - /** - * Log an error about a timer not found. - */ + /** Record an error about a timer not found. */ @GuardedBy("mLock") private void notFoundLocked(String operation, Object arg) { recordErrorLocked(operation, "notFound", arg); } - /** - * Dumpsys output. - */ - public static void dump(@NonNull PrintWriter pw, boolean verbose) { + /** Dumpsys output, allowing for overrides. */ + @VisibleForTesting + static void dump(@NonNull PrintWriter pw, boolean verbose, @NonNull Injector injector) { + if (!injector.anrTimerServiceEnabled()) return; + final IndentingPrintWriter ipw = new IndentingPrintWriter(pw); ipw.println("AnrTimer statistics"); ipw.increaseIndent(); + synchronized (sAnrTimerList) { + final int size = sAnrTimerList.size(); + ipw.println("reporting " + size + " timers"); + for (int i = 0; i < size; i++) { + AnrTimer a = sAnrTimerList.valueAt(i).get(); + if (a != null) a.dump(ipw); + } + } if (verbose) dumpErrors(ipw); ipw.format("AnrTimerEnd\n"); ipw.decreaseIndent(); } + + /** Dumpsys output. There is no output if the feature is not enabled. */ + public static void dump(@NonNull PrintWriter pw, boolean verbose) { + dump(pw, verbose, sDefaultInjector); + } + + /** + * Return true if the native timers are supported. Native timers are supported if the method + * nativeAnrTimerSupported() can be executed and it returns true. + */ + private static boolean nativeTimersSupported() { + try { + return nativeAnrTimerSupported(); + } catch (java.lang.UnsatisfiedLinkError e) { + return false; + } + } + + /** + * Native methods + */ + + /** Return true if the native AnrTimer code is operational. */ + private static native boolean nativeAnrTimerSupported(); + + /** + * Create a new native timer with the given key and name. The key is not used by the native + * code but it is returned to the Java layer in the expiration handler. The name is only for + * logging. Unlike the other methods, this is an instance method: the "this" parameter is + * passed into the native layer. + */ + private native long nativeAnrTimerCreate(String name); + + /** Release the native resources. No further operations are premitted. */ + private static native int nativeAnrTimerClose(long service); + + /** Start a timer and return its ID. Zero is returned on error. */ + private static native int nativeAnrTimerStart(long service, int pid, int uid, long timeoutMs, + boolean extend); + + /** + * Cancel a timer by ID. Return true if the timer was running and canceled. Return false if + * the timer was not found or if the timer had already expired. + */ + private static native boolean nativeAnrTimerCancel(long service, int timerId); + + /** Accept an expired timer by ID. Return true if the timer was found. */ + private static native boolean nativeAnrTimerAccept(long service, int timerId); + + /** Discard an expired timer by ID. Return true if the timer was found. */ + private static native boolean nativeAnrTimerDiscard(long service, int timerId); + + /** Prod the native library to log a few statistics. */ + private static native void nativeAnrTimerDump(long service, boolean verbose); + + // This is not a native method but it is a native interface, in the sense that it is called from + // the native layer to report timer expiration. The function must return true if the expiration + // message is delivered to the upper layers and false if it could not be delivered. + // private boolean expire(int timerId, int pid, int uid); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index f43c1b01e87c..3959a5e54cbf 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3691,19 +3691,13 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return false; } - // If the app is using legacy-entry (not auto-enter), then we will get a client-request - // that was actually a server-request (via pause(userLeaving=true)). This happens when - // the app is PAUSING, so detect that case here. - boolean originallyFromClient = fromClient - && (!r.isState(PAUSING) || params.isAutoEnterEnabled()); - - // If PiP2 flag is on and client-request to enter PiP came via onUserLeaveHint(), - // we request a direct transition from Shell to TRANSIT_PIP_LEGACY to get the startWct - // with the right entry bounds. - if (isPip2ExperimentEnabled() && !originallyFromClient && !params.isAutoEnterEnabled()) { + // If PiP2 flag is on and client-request to enter PiP comes in, + // we request a direct transition from Shell to TRANSIT_PIP to get the startWct + // with the right entry bounds. So PiP activity isn't moved to a pinned task until after + // Shell calls back into Core with the entry bounds passed through. + if (isPip2ExperimentEnabled()) { final Transition legacyEnterPipTransition = new Transition(TRANSIT_PIP, - 0 /* flags */, getTransitionController(), - mWindowManager.mSyncEngine); + 0 /* flags */, getTransitionController(), mWindowManager.mSyncEngine); legacyEnterPipTransition.setPipActivity(r); getTransitionController().startCollectOrQueue(legacyEnterPipTransition, (deferred) -> { getTransitionController().requestStartTransition(legacyEnterPipTransition, @@ -3712,6 +3706,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return true; } + // If the app is using legacy-entry (not auto-enter), then we will get a client-request + // that was actually a server-request (via pause(userLeaving=true)). This happens when + // the app is PAUSING, so detect that case here. + boolean originallyFromClient = fromClient + && (!r.isState(PAUSING) || params.isAutoEnterEnabled()); + // Create a transition only for this pip entry if it is coming from the app without the // system requesting that the app enter-pip. If the system requested it, that means it // should be part of that transition if possible. diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index b19f3d813985..dfa9dcecfbb5 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -79,6 +79,7 @@ cc_library_static { ":lib_cachedAppOptimizer_native", ":lib_gameManagerService_native", ":lib_oomConnection_native", + ":lib_anrTimer_native", ], include_dirs: [ @@ -246,3 +247,10 @@ filegroup { name: "lib_oomConnection_native", srcs: ["com_android_server_am_OomConnection.cpp"], } + +filegroup { + name: "lib_anrTimer_native", + srcs: [ + "com_android_server_utils_AnrTimer.cpp", + ], +} diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp new file mode 100644 index 000000000000..97b18fac91f4 --- /dev/null +++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp @@ -0,0 +1,918 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <time.h> +#include <pthread.h> +#include <sys/timerfd.h> +#include <inttypes.h> + +#include <algorithm> +#include <list> +#include <memory> +#include <set> +#include <string> +#include <vector> + +#define LOG_TAG "AnrTimerService" + +#include <jni.h> +#include <nativehelper/JNIHelp.h> +#include "android_runtime/AndroidRuntime.h" +#include "core_jni_helpers.h" + +#include <utils/Mutex.h> +#include <utils/Timers.h> + +#include <utils/Log.h> +#include <utils/Timers.h> +#include <android-base/logging.h> +#include <android-base/stringprintf.h> +#include <android-base/unique_fd.h> + +using ::android::base::StringPrintf; + + +// Native support is unavailable on WIN32 platforms. This macro preemptively disables it. +#ifdef _WIN32 +#define NATIVE_SUPPORT 0 +#else +#define NATIVE_SUPPORT 1 +#endif + +namespace android { + +// using namespace android; + +// Almost nothing in this module needs to be in the android namespace. +namespace { + +// If not on a Posix system, create stub timerfd methods. These are defined to allow +// compilation. They are not functional. Also, they do not leak outside this compilation unit. +#ifdef _WIN32 +int timer_create() { + return -1; +} +int timer_settime(int, int, void const *, void *) { + return -1; +} +#else +int timer_create() { + return timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); +} +int timer_settime(int fd, int flags, const struct itimerspec *new_value, + struct itimerspec *_Nullable old_value) { + return timerfd_settime(fd, flags, new_value, old_value); +} +#endif + +// A local debug flag that gates a set of log messages for debug only. This is normally const +// false so the debug statements are not included in the image. The flag can be set true in a +// unit test image to debug test failures. +const bool DEBUG = false; + +// Return the current time in nanoseconds. This time is relative to system boot. +nsecs_t now() { + return systemTime(SYSTEM_TIME_MONOTONIC); +} + +/** + * This class encapsulates the anr timer service. The service manages a list of individual + * timers. A timer is either Running or Expired. Once started, a timer may be canceled or + * accepted. Both actions collect statistics about the timer and then delete it. An expired + * timer may also be discarded, which deletes the timer without collecting any statistics. + * + * All public methods in this class are thread-safe. + */ +class AnrTimerService { + private: + class ProcessStats; + class Timer; + + public: + + // The class that actually runs the clock. + class Ticker; + + // A timer is identified by a timer_id_t. Timer IDs are unique in the moment. + using timer_id_t = uint32_t; + + // A manifest constant. No timer is ever created with this ID. + static const timer_id_t NOTIMER = 0; + + // A notifier is called with a timer ID, the timer's tag, and the client's cookie. The pid + // and uid that were originally assigned to the timer are passed as well. + using notifier_t = bool (*)(timer_id_t, int pid, int uid, void* cookie, jweak object); + + enum Status { + Invalid, + Running, + Expired, + Canceled + }; + + /** + * Create a timer service. The service is initialized with a name used for logging. The + * constructor is also given the notifier callback, and two cookies for the callback: the + * traditional void* and an int. + */ + AnrTimerService(char const* label, notifier_t notifier, void* cookie, jweak jtimer, Ticker*); + + // Delete the service and clean up memory. + ~AnrTimerService(); + + // Start a timer and return the associated timer ID. It does not matter if the same pid/uid + // are already in the running list. Once start() is called, one of cancel(), accept(), or + // discard() must be called to clean up the internal data structures. + timer_id_t start(int pid, int uid, nsecs_t timeout, bool extend); + + // Cancel a timer and remove it from all lists. This is called when the event being timed + // has occurred. If the timer was Running, the function returns true. The other + // possibilities are that the timer was Expired or non-existent; in both cases, the function + // returns false. + bool cancel(timer_id_t timerId); + + // Accept a timer and remove it from all lists. This is called when the upper layers accept + // that a timer has expired. If the timer was Expired, the function returns true. The + // other possibilities are tha the timer was Running or non-existing; in both cases, the + // function returns false. + bool accept(timer_id_t timerId); + + // Discard a timer without collecting any statistics. This is called when the upper layers + // recognize that a timer expired but decide the expiration is not significant. If the + // timer was Expired, the function returns true. The other possibilities are tha the timer + // was Running or non-existing; in both cases, the function returns false. + bool discard(timer_id_t timerId); + + // A timer has expired. + void expire(timer_id_t); + + // Dump a small amount of state to the log file. + void dump(bool verbose) const; + + // Return the Java object associated with this instance. + jweak jtimer() const { + return notifierObject_; + } + + private: + // The service cannot be copied. + AnrTimerService(AnrTimerService const &) = delete; + + // Insert a timer into the running list. The lock must be held by the caller. + void insert(const Timer&); + + // Remove a timer from the lists and return it. The lock must be held by the caller. + Timer remove(timer_id_t timerId); + + // Return a string representation of a status value. + static char const *statusString(Status); + + // The name of this service, for logging. + std::string const label_; + + // The callback that is invoked when a timer expires. + notifier_t const notifier_; + + // The two cookies passed to the notifier. + void* notifierCookie_; + jweak notifierObject_; + + // The global lock + mutable Mutex lock_; + + // The list of all timers that are still running. This is sorted by ID for fast lookup. + std::set<Timer> running_; + + // The maximum number of active timers. + size_t maxActive_; + + // Simple counters + struct Counters { + // The number of timers started, canceled, accepted, discarded, and expired. + size_t started; + size_t canceled; + size_t accepted; + size_t discarded; + size_t expired; + + // The number of times there were zero active timers. + size_t drained; + + // The number of times a protocol error was seen. + size_t error; + }; + + Counters counters_; + + // The clock used by this AnrTimerService. + Ticker *ticker_; +}; + +class AnrTimerService::ProcessStats { + public: + nsecs_t cpu_time; + nsecs_t cpu_delay; + + ProcessStats() : + cpu_time(0), + cpu_delay(0) { + } + + // Collect all statistics for a process. Return true if the fill succeeded and false if it + // did not. If there is any problem, the statistics are zeroed. + bool fill(int pid) { + cpu_time = 0; + cpu_delay = 0; + + char path[PATH_MAX]; + snprintf(path, sizeof(path), "/proc/%u/schedstat", pid); + ::android::base::unique_fd fd(open(path, O_RDONLY | O_CLOEXEC)); + if (!fd.ok()) { + return false; + } + char buffer[128]; + ssize_t len = read(fd, buffer, sizeof(buffer)); + if (len <= 0) { + return false; + } + if (len >= sizeof(buffer)) { + ALOGE("proc file too big: %s", path); + return false; + } + buffer[len] = 0; + unsigned long t1; + unsigned long t2; + if (sscanf(buffer, "%lu %lu", &t1, &t2) != 2) { + return false; + } + cpu_time = t1; + cpu_delay = t2; + return true; + } +}; + +class AnrTimerService::Timer { + public: + // A unique ID assigned when the Timer is created. + timer_id_t const id; + + // The creation parameters. The timeout is the original, relative timeout. + int const pid; + int const uid; + nsecs_t const timeout; + bool const extend; + + // The state of this timer. + Status status; + + // The scheduled timeout. This is an absolute time. It may be extended. + nsecs_t scheduled; + + // True if this timer has been extended. + bool extended; + + // Bookkeeping for extensions. The initial state of the process. This is collected only if + // the timer is extensible. + ProcessStats initial; + + // The default constructor is used to create timers that are Invalid, representing the "not + // found" condition when a collection is searched. + Timer() : + id(NOTIMER), + pid(0), + uid(0), + timeout(0), + extend(false), + status(Invalid), + scheduled(0), + extended(false) { + } + + // This constructor creates a timer with the specified id. This can be used as the argument + // to find(). + Timer(timer_id_t id) : + id(id), + pid(0), + uid(0), + timeout(0), + extend(false), + status(Invalid), + scheduled(0), + extended(false) { + } + + // Create a new timer. This starts the timer. + Timer(int pid, int uid, nsecs_t timeout, bool extend) : + id(nextId()), + pid(pid), + uid(uid), + timeout(timeout), + extend(extend), + status(Running), + scheduled(now() + timeout), + extended(false) { + if (extend && pid != 0) { + initial.fill(pid); + } + } + + // Cancel a timer. Return the headroom (which may be negative). This does not, as yet, + // account for extensions. + void cancel() { + ALOGW_IF(DEBUG && status != Running, "cancel %s", toString().c_str()); + status = Canceled; + } + + // Expire a timer. Return true if the timer is expired and false otherwise. The function + // returns false if the timer is eligible for extension. If the function returns false, the + // scheduled time is updated. + bool expire() { + ALOGI_IF(DEBUG, "expire %s", toString().c_str()); + nsecs_t extension = 0; + if (extend && !extended) { + // Only one extension is permitted. + extended = true; + ProcessStats current; + current.fill(pid); + extension = current.cpu_delay - initial.cpu_delay; + if (extension < 0) extension = 0; + if (extension > timeout) extension = timeout; + } + if (extension == 0) { + status = Expired; + } else { + scheduled += extension; + } + return status == Expired; + } + + // Accept a timeout. + void accept() { + } + + // Discard a timeout. + void discard() { + } + + // Timers are sorted by id, which is unique. This provides fast lookups. + bool operator<(Timer const &r) const { + return id < r.id; + } + + bool operator==(timer_id_t r) const { + return id == r; + } + + std::string toString() const { + return StringPrintf("timer id=%d pid=%d status=%s", id, pid, statusString(status)); + } + + std::string toString(nsecs_t now) const { + uint32_t ms = nanoseconds_to_milliseconds(now - scheduled); + return StringPrintf("timer id=%d pid=%d status=%s scheduled=%ums", + id, pid, statusString(status), -ms); + } + + static int maxId() { + return idGen; + } + + private: + // Get the next free ID. NOTIMER is never returned. + static timer_id_t nextId() { + timer_id_t id = idGen.fetch_add(1); + while (id == NOTIMER) { + id = idGen.fetch_add(1); + } + return id; + } + + // IDs start at 1. A zero ID is invalid. + static std::atomic<timer_id_t> idGen; +}; + +// IDs start at 1. +std::atomic<AnrTimerService::timer_id_t> AnrTimerService::Timer::idGen(1); + +/** + * Manage a set of timers and notify clients when there is a timeout. + */ +class AnrTimerService::Ticker { + private: + struct Entry { + const nsecs_t scheduled; + const timer_id_t id; + AnrTimerService* const service; + + Entry(nsecs_t scheduled, timer_id_t id, AnrTimerService* service) : + scheduled(scheduled), id(id), service(service) {}; + + bool operator<(const Entry &r) const { + return scheduled == r.scheduled ? id < r.id : scheduled < r.scheduled; + } + }; + + public: + + // Construct the ticker. This creates the timerfd file descriptor and starts the monitor + // thread. The monitor thread is given a unique name. + Ticker() { + timerFd_ = timer_create(); + if (timerFd_ < 0) { + ALOGE("failed to create timerFd: %s", strerror(errno)); + return; + } + + if (pthread_create(&watcher_, 0, run, this) != 0) { + ALOGE("failed to start thread: %s", strerror(errno)); + watcher_ = 0; + ::close(timerFd_); + return; + } + + // 16 is a magic number from the kernel. Thread names may not be longer than this many + // bytes, including the terminating null. The snprintf() method will truncate properly. + char name[16]; + snprintf(name, sizeof(name), "AnrTimerService"); + pthread_setname_np(watcher_, name); + + ready_ = true; + } + + ~Ticker() { + // Closing the file descriptor will close the monitor process, if any. + if (timerFd_ >= 0) ::close(timerFd_); + timerFd_ = -1; + watcher_ = 0; + } + + // Insert a timer. Unless canceled, the timer will expire at the scheduled time. If it + // expires, the service will be notified with the id. + void insert(nsecs_t scheduled, timer_id_t id, AnrTimerService *service) { + Entry e(scheduled, id, service); + AutoMutex _l(lock_); + timer_id_t front = headTimerId(); + running_.insert(e); + if (front != headTimerId()) restartLocked(); + maxRunning_ = std::max(maxRunning_, running_.size()); + } + + // Remove a timer. The timer is identified by its scheduled timeout and id. Technically, + // the id is sufficient (because timer IDs are unique) but using the timeout is more + // efficient. + void remove(nsecs_t scheduled, timer_id_t id) { + Entry key(scheduled, id, 0); + AutoMutex _l(lock_); + timer_id_t front = headTimerId(); + auto found = running_.find(key); + if (found != running_.end()) running_.erase(found); + if (front != headTimerId()) restartLocked(); + } + + // Remove every timer associated with the service. + void remove(AnrTimerService const* service) { + AutoMutex _l(lock_); + timer_id_t front = headTimerId(); + for (auto i = running_.begin(); i != running_.end(); i++) { + if (i->service == service) { + running_.erase(i); + } + } + if (front != headTimerId()) restartLocked(); + } + + // Return the number of timers still running. + size_t running() const { + AutoMutex _l(lock_); + return running_.size(); + } + + // Return the high-water mark of timers running. + size_t maxRunning() const { + AutoMutex _l(lock_); + return maxRunning_; + } + + private: + + // Return the head of the running list. The lock must be held by the caller. + timer_id_t headTimerId() { + return running_.empty() ? NOTIMER : running_.cbegin()->id; + } + + // A simple wrapper that meets the requirements of pthread_create. + static void* run(void* arg) { + reinterpret_cast<Ticker*>(arg)->monitor(); + ALOGI("monitor exited"); + return 0; + } + + // Loop (almost) forever. Whenever the timerfd expires, expire as many entries as + // possible. The loop terminates when the read fails; this generally indicates that the + // file descriptor has been closed and the thread can exit. + void monitor() { + uint64_t token = 0; + while (read(timerFd_, &token, sizeof(token)) == sizeof(token)) { + // Move expired timers into the local ready list. This is done inside + // the lock. Then, outside the lock, expire them. + nsecs_t current = now(); + std::vector<Entry> ready; + { + AutoMutex _l(lock_); + while (!running_.empty()) { + Entry timer = *(running_.begin()); + if (timer.scheduled <= current) { + ready.push_back(timer); + running_.erase(running_.cbegin()); + } else { + break; + } + } + restartLocked(); + } + // Call the notifiers outside the lock. Calling the notifiers with the lock held + // can lead to deadlock, if the Java-side handler also takes a lock. Note that the + // timerfd is already running. + for (auto i = ready.begin(); i != ready.end(); i++) { + Entry e = *i; + e.service->expire(e.id); + } + } + } + + // Restart the ticker. The caller must be holding the lock. This method updates the + // timerFd_ to expire at the time of the first Entry in the running list. This method does + // not check to see if the currently programmed expiration time is different from the + // scheduled expiration time of the first entry. + void restartLocked() { + if (!running_.empty()) { + Entry const x = *(running_.cbegin()); + nsecs_t delay = x.scheduled - now(); + // Force a minimum timeout of 10ns. + if (delay < 10) delay = 10; + time_t sec = nanoseconds_to_seconds(delay); + time_t ns = delay - seconds_to_nanoseconds(sec); + struct itimerspec setting = { + .it_interval = { 0, 0 }, + .it_value = { sec, ns }, + }; + timer_settime(timerFd_, 0, &setting, nullptr); + restarted_++; + ALOGI_IF(DEBUG, "restarted timerfd for %ld.%09ld", sec, ns); + } else { + const struct itimerspec setting = { + .it_interval = { 0, 0 }, + .it_value = { 0, 0 }, + }; + timer_settime(timerFd_, 0, &setting, nullptr); + drained_++; + ALOGI_IF(DEBUG, "drained timer list"); + } + } + + // The usual lock. + mutable Mutex lock_; + + // True if the object was initialized properly. Android does not support throwing C++ + // exceptions, so clients should check this flag after constructing the object. This is + // effectively const after the instance has been created. + bool ready_ = false; + + // The file descriptor of the timer. + int timerFd_ = -1; + + // The thread that monitors the timer. + pthread_t watcher_ = 0; + + // The number of times the timer was restarted. + size_t restarted_ = 0; + + // The number of times the timer list was exhausted. + size_t drained_ = 0; + + // The highwater mark of timers that are running. + size_t maxRunning_ = 0; + + // The list of timers that are scheduled. This set is sorted by timeout and then by timer + // ID. A set is sufficient (as opposed to a multiset) because timer IDs are unique. + std::set<Entry> running_; +}; + + +AnrTimerService::AnrTimerService(char const* label, + notifier_t notifier, void* cookie, jweak jtimer, Ticker* ticker) : + label_(label), + notifier_(notifier), + notifierCookie_(cookie), + notifierObject_(jtimer), + ticker_(ticker) { + + // Zero the statistics + maxActive_ = 0; + memset(&counters_, 0, sizeof(counters_)); + + ALOGI_IF(DEBUG, "initialized %s", label); +} + +AnrTimerService::~AnrTimerService() { + AutoMutex _l(lock_); + ticker_->remove(this); +} + +char const *AnrTimerService::statusString(Status s) { + switch (s) { + case Invalid: return "invalid"; + case Running: return "running"; + case Expired: return "expired"; + case Canceled: return "canceled"; + } + return "unknown"; +} + +AnrTimerService::timer_id_t AnrTimerService::start(int pid, int uid, + nsecs_t timeout, bool extend) { + ALOGI_IF(DEBUG, "starting"); + AutoMutex _l(lock_); + Timer t(pid, uid, timeout, extend); + insert(t); + counters_.started++; + + ALOGI_IF(DEBUG, "started timer %u timeout=%zu", t.id, static_cast<size_t>(timeout)); + return t.id; +} + +bool AnrTimerService::cancel(timer_id_t timerId) { + ALOGI_IF(DEBUG, "canceling %u", timerId); + if (timerId == NOTIMER) return false; + AutoMutex _l(lock_); + Timer timer = remove(timerId); + + bool result = timer.status == Running; + if (timer.status != Invalid) { + timer.cancel(); + } else { + counters_.error++; + } + counters_.canceled++; + ALOGI_IF(DEBUG, "canceled timer %u", timerId); + return result; +} + +bool AnrTimerService::accept(timer_id_t timerId) { + ALOGI_IF(DEBUG, "accepting %u", timerId); + if (timerId == NOTIMER) return false; + AutoMutex _l(lock_); + Timer timer = remove(timerId); + + bool result = timer.status == Expired; + if (timer.status == Expired) { + timer.accept(); + } else { + counters_.error++; + } + counters_.accepted++; + ALOGI_IF(DEBUG, "accepted timer %u", timerId); + return result; +} + +bool AnrTimerService::discard(timer_id_t timerId) { + ALOGI_IF(DEBUG, "discarding %u", timerId); + if (timerId == NOTIMER) return false; + AutoMutex _l(lock_); + Timer timer = remove(timerId); + + bool result = timer.status == Expired; + if (timer.status == Expired) { + timer.discard(); + } else { + counters_.error++; + } + counters_.discarded++; + ALOGI_IF(DEBUG, "discarded timer %u", timerId); + return result; +} + +// Hold the lock in order to manage the running list. +// the listener. +void AnrTimerService::expire(timer_id_t timerId) { + ALOGI_IF(DEBUG, "expiring %u", timerId); + // Save the timer attributes for the notification + int pid = 0; + int uid = 0; + bool expired = false; + { + AutoMutex _l(lock_); + Timer t = remove(timerId); + expired = t.expire(); + if (t.status == Invalid) { + ALOGW_IF(DEBUG, "error: expired invalid timer %u", timerId); + return; + } else { + // The timer is either Running (because it was extended) or expired (and is awaiting an + // accept or discard). + insert(t); + } + } + + // Deliver the notification outside of the lock. + if (expired) { + if (!notifier_(timerId, pid, uid, notifierCookie_, notifierObject_)) { + AutoMutex _l(lock_); + // Notification failed, which means the listener will never call accept() or + // discard(). Do not reinsert the timer. + remove(timerId); + } + } + ALOGI_IF(DEBUG, "expired timer %u", timerId); +} + +void AnrTimerService::insert(const Timer& t) { + running_.insert(t); + if (t.status == Running) { + // Only forward running timers to the ticker. Expired timers are handled separately. + ticker_->insert(t.scheduled, t.id, this); + maxActive_ = std::max(maxActive_, running_.size()); + } +} + +AnrTimerService::Timer AnrTimerService::remove(timer_id_t timerId) { + Timer key(timerId); + auto found = running_.find(key); + if (found != running_.end()) { + Timer result = *found; + running_.erase(found); + ticker_->remove(result.scheduled, result.id); + return result; + } + return Timer(); +} + +void AnrTimerService::dump(bool verbose) const { + AutoMutex _l(lock_); + ALOGI("timer %s ops started=%zu canceled=%zu accepted=%zu discarded=%zu expired=%zu", + label_.c_str(), + counters_.started, counters_.canceled, counters_.accepted, + counters_.discarded, counters_.expired); + ALOGI("timer %s stats max-active=%zu/%zu running=%zu/%zu errors=%zu", + label_.c_str(), + maxActive_, ticker_->maxRunning(), running_.size(), ticker_->running(), + counters_.error); + + if (verbose) { + nsecs_t time = now(); + for (auto i = running_.begin(); i != running_.end(); i++) { + Timer t = *i; + ALOGI(" running %s", t.toString(time).c_str()); + } + } +} + +/** + * True if the native methods are supported in this process. Native methods are supported only + * if the initialization succeeds. + */ +bool nativeSupportEnabled = false; + +/** + * Singleton/globals for the anr timer. Among other things, this includes a Ticker* and a use + * count. The JNI layer creates a single Ticker for all operational AnrTimers. The Ticker is + * created when the first AnrTimer is created, and is deleted when the last AnrTimer is closed. + */ +static Mutex gAnrLock; +struct AnrArgs { + jclass clazz = NULL; + jmethodID func = NULL; + JavaVM* vm = NULL; + AnrTimerService::Ticker* ticker = nullptr; + int tickerUseCount = 0;; +}; +static AnrArgs gAnrArgs; + +// The cookie is the address of the AnrArgs object to which the notification should be sent. +static bool anrNotify(AnrTimerService::timer_id_t timerId, int pid, int uid, + void* cookie, jweak jtimer) { + AutoMutex _l(gAnrLock); + AnrArgs* target = reinterpret_cast<AnrArgs* >(cookie); + JNIEnv *env; + if (target->vm->AttachCurrentThread(&env, 0) != JNI_OK) { + ALOGE("failed to attach thread to JavaVM"); + return false; + } + jboolean r = false; + jobject timer = env->NewGlobalRef(jtimer); + if (timer != nullptr) { + r = env->CallBooleanMethod(timer, target->func, timerId, pid, uid); + env->DeleteGlobalRef(timer); + } + target->vm->DetachCurrentThread(); + return r; +} + +jboolean anrTimerSupported(JNIEnv* env, jclass) { + return nativeSupportEnabled; +} + +jlong anrTimerCreate(JNIEnv* env, jobject jtimer, jstring jname) { + if (!nativeSupportEnabled) return 0; + AutoMutex _l(gAnrLock); + if (!gAnrArgs.ticker) { + gAnrArgs.ticker = new AnrTimerService::Ticker(); + } + gAnrArgs.tickerUseCount++; + + ScopedUtfChars name(env, jname); + jobject timer = env->NewWeakGlobalRef(jtimer); + AnrTimerService* service = + new AnrTimerService(name.c_str(), anrNotify, &gAnrArgs, timer, gAnrArgs.ticker); + return reinterpret_cast<jlong>(service); +} + +AnrTimerService *toService(jlong pointer) { + return reinterpret_cast<AnrTimerService*>(pointer); +} + +jint anrTimerClose(JNIEnv* env, jclass, jlong ptr) { + if (!nativeSupportEnabled) return -1; + if (ptr == 0) return -1; + AutoMutex _l(gAnrLock); + AnrTimerService *s = toService(ptr); + env->DeleteWeakGlobalRef(s->jtimer()); + delete s; + if (--gAnrArgs.tickerUseCount <= 0) { + delete gAnrArgs.ticker; + gAnrArgs.ticker = nullptr; + } + return 0; +} + +jint anrTimerStart(JNIEnv* env, jclass, jlong ptr, + jint pid, jint uid, jlong timeout, jboolean extend) { + if (!nativeSupportEnabled) return 0; + // On the Java side, timeouts are expressed in milliseconds and must be converted to + // nanoseconds before being passed to the library code. + return toService(ptr)->start(pid, uid, milliseconds_to_nanoseconds(timeout), extend); +} + +jboolean anrTimerCancel(JNIEnv* env, jclass, jlong ptr, jint timerId) { + if (!nativeSupportEnabled) return false; + return toService(ptr)->cancel(timerId); +} + +jboolean anrTimerAccept(JNIEnv* env, jclass, jlong ptr, jint timerId) { + if (!nativeSupportEnabled) return false; + return toService(ptr)->accept(timerId); +} + +jboolean anrTimerDiscard(JNIEnv* env, jclass, jlong ptr, jint timerId) { + if (!nativeSupportEnabled) return false; + return toService(ptr)->discard(timerId); +} + +jint anrTimerDump(JNIEnv *env, jclass, jlong ptr, jboolean verbose) { + if (!nativeSupportEnabled) return -1; + toService(ptr)->dump(verbose); + return 0; +} + +static const JNINativeMethod methods[] = { + {"nativeAnrTimerSupported", "()Z", (void*) anrTimerSupported}, + {"nativeAnrTimerCreate", "(Ljava/lang/String;)J", (void*) anrTimerCreate}, + {"nativeAnrTimerClose", "(J)I", (void*) anrTimerClose}, + {"nativeAnrTimerStart", "(JIIJZ)I", (void*) anrTimerStart}, + {"nativeAnrTimerCancel", "(JI)Z", (void*) anrTimerCancel}, + {"nativeAnrTimerAccept", "(JI)Z", (void*) anrTimerAccept}, + {"nativeAnrTimerDiscard", "(JI)Z", (void*) anrTimerDiscard}, + {"nativeAnrTimerDump", "(JZ)V", (void*) anrTimerDump}, +}; + +} // anonymous namespace + +int register_android_server_utils_AnrTimer(JNIEnv* env) +{ + static const char *className = "com/android/server/utils/AnrTimer"; + jniRegisterNativeMethods(env, className, methods, NELEM(methods)); + + jclass service = FindClassOrDie(env, className); + gAnrArgs.clazz = MakeGlobalRefOrDie(env, service); + gAnrArgs.func = env->GetMethodID(gAnrArgs.clazz, "expire", "(III)Z"); + env->GetJavaVM(&gAnrArgs.vm); + + nativeSupportEnabled = NATIVE_SUPPORT; + + return 0; +} + +} // namespace android diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 11734da5b1ac..f3158d11b9a4 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -52,6 +52,7 @@ int register_android_server_Watchdog(JNIEnv* env); int register_android_server_HardwarePropertiesManagerService(JNIEnv* env); int register_android_server_SyntheticPasswordManager(JNIEnv* env); int register_android_hardware_display_DisplayViewport(JNIEnv* env); +int register_android_server_utils_AnrTimer(JNIEnv *env); int register_android_server_am_OomConnection(JNIEnv* env); int register_android_server_am_CachedAppOptimizer(JNIEnv* env); int register_android_server_am_LowMemDetector(JNIEnv* env); @@ -113,6 +114,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_storage_AppFuse(env); register_android_server_SyntheticPasswordManager(env); register_android_hardware_display_DisplayViewport(env); + register_android_server_utils_AnrTimer(env); register_android_server_am_OomConnection(env); register_android_server_am_CachedAppOptimizer(env); register_android_server_am_LowMemDetector(env); diff --git a/services/tests/servicestests/jni/Android.bp b/services/tests/servicestests/jni/Android.bp index 174beb81d3eb..c30e4eb666b4 100644 --- a/services/tests/servicestests/jni/Android.bp +++ b/services/tests/servicestests/jni/Android.bp @@ -23,6 +23,7 @@ cc_library_shared { ":lib_cachedAppOptimizer_native", ":lib_gameManagerService_native", ":lib_oomConnection_native", + ":lib_anrTimer_native", "onload.cpp", ], @@ -55,4 +56,4 @@ cc_library_shared { "android.hardware.graphics.mapper@4.0", "android.hidl.token@1.0-utils", ], -}
\ No newline at end of file +} diff --git a/services/tests/servicestests/jni/onload.cpp b/services/tests/servicestests/jni/onload.cpp index f160b3d97367..25487c5aabbe 100644 --- a/services/tests/servicestests/jni/onload.cpp +++ b/services/tests/servicestests/jni/onload.cpp @@ -27,6 +27,7 @@ namespace android { int register_android_server_am_CachedAppOptimizer(JNIEnv* env); int register_android_server_app_GameManagerService(JNIEnv* env); int register_android_server_am_OomConnection(JNIEnv* env); +int register_android_server_utils_AnrTimer(JNIEnv *env); }; using namespace android; @@ -44,5 +45,6 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_am_CachedAppOptimizer(env); register_android_server_app_GameManagerService(env); register_android_server_am_OomConnection(env); + register_android_server_utils_AnrTimer(env); return JNI_VERSION_1_4; } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java index 59c94dc1f250..89a49615dbe1 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapterTest.java @@ -65,7 +65,6 @@ import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvide import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintResetLockoutClient; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; @@ -248,7 +247,6 @@ public class HidlToAidlSensorAdapterTest { } @Test - @Ignore("b/317403648") public void lockoutPermanentResetViaClient() { setLockoutPermanent(); diff --git a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java index 861d14a2cf66..6c085e085f4e 100644 --- a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java +++ b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java @@ -23,17 +23,21 @@ import static org.junit.Assert.assertTrue; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.util.Log; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; import com.android.internal.annotations.GuardedBy; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -45,6 +49,9 @@ import java.util.concurrent.TimeUnit; @RunWith(Parameterized.class) public class AnrTimerTest { + // A log tag. + private static final String TAG = "AnrTimerTest"; + // The commonly used message timeout key. private static final int MSG_TIMEOUT = 1; @@ -63,9 +70,7 @@ public class AnrTimerTest { } } - /** - * The test handler is a self-contained object for a single test. - */ + /** The test helper is a self-contained object for a single test. */ private static class Helper { final Object mLock = new Object(); @@ -114,7 +119,7 @@ public class AnrTimerTest { /** * Force AnrTimer to use the test parameter for the feature flag. */ - class TestInjector extends AnrTimer.Injector { + private class TestInjector extends AnrTimer.Injector { @Override boolean anrTimerServiceEnabled() { return mEnabled; @@ -124,9 +129,9 @@ public class AnrTimerTest { /** * An instrumented AnrTimer. */ - private static class TestAnrTimer extends AnrTimer<TestArg> { + private class TestAnrTimer extends AnrTimer<TestArg> { private TestAnrTimer(Handler h, int key, String tag) { - super(h, key, tag); + super(h, key, tag, false, new TestInjector()); } TestAnrTimer(Helper helper) { @@ -173,35 +178,103 @@ public class AnrTimerTest { @Test public void testSimpleTimeout() throws Exception { Helper helper = new Helper(1); - TestAnrTimer timer = new TestAnrTimer(helper); - TestArg t = new TestArg(1, 1); - timer.start(t, 10); - // Delivery is immediate but occurs on a different thread. - assertTrue(helper.await(5000)); - TestArg[] result = helper.messages(1); - validate(t, result[0]); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + // One-time check that the injector is working as expected. + assertEquals(mEnabled, timer.serviceEnabled()); + TestArg t = new TestArg(1, 1); + timer.start(t, 10); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(1); + validate(t, result[0]); + } } /** - * Verify that if three timers are scheduled, they are delivered in time order. + * Verify that a restarted timer is delivered exactly once. The initial timer value is very + * large, to ensure it does not expire before the timer can be restarted. + */ + @Test + public void testTimerRestart() throws Exception { + Helper helper = new Helper(1); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + TestArg t = new TestArg(1, 1); + timer.start(t, 10000); + // Briefly pause. + assertFalse(helper.await(10)); + timer.start(t, 10); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(1); + validate(t, result[0]); + } + } + + /** + * Verify that a restarted timer is delivered exactly once. The initial timer value is very + * large, to ensure it does not expire before the timer can be restarted. + */ + @Test + public void testTimerZero() throws Exception { + Helper helper = new Helper(1); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + TestArg t = new TestArg(1, 1); + timer.start(t, 0); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(1); + validate(t, result[0]); + } + } + + /** + * Verify that if three timers are scheduled on a single AnrTimer, they are delivered in time + * order. */ @Test public void testMultipleTimers() throws Exception { // Expect three messages. Helper helper = new Helper(3); - TestAnrTimer timer = new TestAnrTimer(helper); TestArg t1 = new TestArg(1, 1); TestArg t2 = new TestArg(1, 2); TestArg t3 = new TestArg(1, 3); - timer.start(t1, 50); - timer.start(t2, 60); - timer.start(t3, 40); - // Delivery is immediate but occurs on a different thread. - assertTrue(helper.await(5000)); - TestArg[] result = helper.messages(3); - validate(t3, result[0]); - validate(t1, result[1]); - validate(t2, result[2]); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + timer.start(t1, 50); + timer.start(t2, 60); + timer.start(t3, 40); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(3); + validate(t3, result[0]); + validate(t1, result[1]); + validate(t2, result[2]); + } + } + + /** + * Verify that if three timers are scheduled on three separate AnrTimers, they are delivered + * in time order. + */ + @Test + public void testMultipleServices() throws Exception { + // Expect three messages. + Helper helper = new Helper(3); + TestArg t1 = new TestArg(1, 1); + TestArg t2 = new TestArg(1, 2); + TestArg t3 = new TestArg(1, 3); + try (TestAnrTimer x1 = new TestAnrTimer(helper); + TestAnrTimer x2 = new TestAnrTimer(helper); + TestAnrTimer x3 = new TestAnrTimer(helper)) { + x1.start(t1, 50); + x2.start(t2, 60); + x3.start(t3, 40); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(3); + validate(t3, result[0]); + validate(t1, result[1]); + validate(t2, result[2]); + } } /** @@ -211,20 +284,109 @@ public class AnrTimerTest { public void testCancelTimer() throws Exception { // Expect two messages. Helper helper = new Helper(2); - TestAnrTimer timer = new TestAnrTimer(helper); TestArg t1 = new TestArg(1, 1); TestArg t2 = new TestArg(1, 2); TestArg t3 = new TestArg(1, 3); - timer.start(t1, 50); - timer.start(t2, 60); - timer.start(t3, 40); - // Briefly pause. - assertFalse(helper.await(10)); - timer.cancel(t1); - // Delivery is immediate but occurs on a different thread. - assertTrue(helper.await(5000)); - TestArg[] result = helper.messages(2); - validate(t3, result[0]); - validate(t2, result[1]); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + timer.start(t1, 50); + timer.start(t2, 60); + timer.start(t3, 40); + // Briefly pause. + assertFalse(helper.await(10)); + timer.cancel(t1); + // Delivery is immediate but occurs on a different thread. + assertTrue(helper.await(5000)); + TestArg[] result = helper.messages(2); + validate(t3, result[0]); + validate(t2, result[1]); + } + } + + /** + * Return the dump string. + */ + private String getDumpOutput() { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + AnrTimer.dump(pw, true, new TestInjector()); + pw.close(); + return sw.getBuffer().toString(); + } + + /** + * Verify the dump output. + */ + @Test + public void testDumpOutput() throws Exception { + String r1 = getDumpOutput(); + assertEquals(false, r1.contains("timer:")); + + Helper helper = new Helper(2); + TestArg t1 = new TestArg(1, 1); + TestArg t2 = new TestArg(1, 2); + TestArg t3 = new TestArg(1, 3); + try (TestAnrTimer timer = new TestAnrTimer(helper)) { + timer.start(t1, 5000); + timer.start(t2, 5000); + timer.start(t3, 5000); + + String r2 = getDumpOutput(); + // There are timers in the list if and only if the feature is enabled. + final boolean expected = mEnabled; + assertEquals(expected, r2.contains("timer:")); + } + + String r3 = getDumpOutput(); + assertEquals(false, r3.contains("timer:")); + } + + /** + * Verify that GC works as expected. This test will almost certainly be flaky, since it + * relies on the finalizers running, which is a best-effort on the part of the JVM. + * Therefore, the test is marked @Ignore. Remove that annotation to run the test locally. + */ + @Ignore + @Test + public void testGarbageCollection() throws Exception { + if (!mEnabled) return; + + String r1 = getDumpOutput(); + assertEquals(false, r1.contains("timer:")); + + Helper helper = new Helper(2); + TestArg t1 = new TestArg(1, 1); + TestArg t2 = new TestArg(1, 2); + TestArg t3 = new TestArg(1, 3); + // The timer is explicitly not closed. It is, however, scoped to the next block. + { + TestAnrTimer timer = new TestAnrTimer(helper); + timer.start(t1, 5000); + timer.start(t2, 5000); + timer.start(t3, 5000); + + String r2 = getDumpOutput(); + // There are timers in the list if and only if the feature is enabled. + final boolean expected = mEnabled; + assertEquals(expected, r2.contains("timer:")); + } + + // Try to make finalizers run. The timer object above should be a candidate. Finalizers + // are run on their own thread, so pause this thread to give that thread some time. + String r3 = getDumpOutput(); + for (int i = 0; i < 10 && r3.contains("timer:"); i++) { + Log.i(TAG, "requesting finalization " + i); + System.gc(); + System.runFinalization(); + Thread.sleep(4 * 1000); + r3 = getDumpOutput(); + } + + // The timer was not explicitly closed but it should have been implicitly closed by GC. + assertEquals(false, r3.contains("timer:")); + } + + // TODO: [b/302724778] Remove manual JNI load + static { + System.loadLibrary("servicestestjni"); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 39779b00f62f..f1edd9a59b99 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -303,7 +303,6 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -14061,7 +14060,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne() throws RemoteException { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags @@ -14073,7 +14071,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification(nr1); // Create old notification. - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel specific notifications via listener. @@ -14091,16 +14090,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_old_cancelOne() throws RemoteException { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED); // Create old notifications. - final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr1); - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel specific notifications via listener. @@ -14119,7 +14119,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne_flagDisabled() throws RemoteException { mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags @@ -14131,7 +14130,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification(nr1); // Create old notification. - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel specific notifications via listener. @@ -14150,7 +14150,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll() throws RemoteException { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags @@ -14162,7 +14161,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification(nr1); // Create old notification. - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel all notifications via listener. @@ -14179,16 +14179,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_old_cancelAll() throws RemoteException { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED); // Create old notifications. - final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr1); - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel all notifications via listener. @@ -14206,7 +14207,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @Ignore("b/316989461") public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll_flagDisabled() throws RemoteException { mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags @@ -14218,7 +14218,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification(nr1); // Create old notification. - final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0); + final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, + System.currentTimeMillis() - 60000); mService.addNotification(nr2); // Cancel all notifications via listener. diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 6497ee9cb1f2..782d89cdcd29 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -115,6 +115,9 @@ import android.os.Binder; import android.os.RemoteException; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.ArraySet; import android.util.DisplayMetrics; import android.view.Display; @@ -146,6 +149,7 @@ import com.android.server.LocalServices; import com.android.server.policy.WindowManagerPolicy; import com.android.server.wm.utils.WmDisplayCutout; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -172,6 +176,10 @@ import java.util.concurrent.TimeoutException; @RunWith(WindowTestRunner.class) public class DisplayContentTests extends WindowTestsBase { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @SetupWindows(addAllCommonWindows = true) @Test public void testForAllWindows() { @@ -508,6 +516,7 @@ public class DisplayContentTests extends WindowTestsBase { * Tests tapping on a root task in different display results in window gaining focus. */ @Test + @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_REMOVE_POINTER_EVENT_TRACKING_IN_WM) public void testInputEventBringsCorrectDisplayInFocus() { DisplayContent dc0 = mWm.getDefaultDisplayContentLocked(); // Create a second display diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 6c8663a8eb14..ff7b3921cdcb 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -3785,9 +3785,9 @@ public class SubscriptionManager { Map<ParcelUuid, SubscriptionInfo> groupMap = new HashMap<>(); for (SubscriptionInfo info : availableList) { - // Opportunistic subscriptions are considered invisible + // Grouped opportunistic subscriptions are considered invisible // to users so they should never be returned. - if (!isSubscriptionVisible(info)) continue; + if (info.getGroupUuid() != null && info.isOpportunistic()) continue; ParcelUuid groupUuid = info.getGroupUuid(); if (groupUuid == null) { diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java index 60b5ce75e2f7..80c1e5be3a32 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java @@ -810,6 +810,7 @@ public class GraphicsActivity extends Activity { private FpsRange convertCategory(int category) { switch (category) { + case Surface.FRAME_RATE_CATEGORY_HIGH_HINT: case Surface.FRAME_RATE_CATEGORY_HIGH: return FRAME_RATE_CATEGORY_HIGH; case Surface.FRAME_RATE_CATEGORY_NORMAL: diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java index 4b56c107cf22..caaee634c57a 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java @@ -93,6 +93,12 @@ public class SurfaceControlTest { } @Test + public void testSurfaceControlFrameRateCategoryHighHint() throws InterruptedException { + GraphicsActivity activity = mActivityRule.getActivity(); + activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_HIGH_HINT); + } + + @Test public void testSurfaceControlFrameRateCategoryNormal() throws InterruptedException { GraphicsActivity activity = mActivityRule.getActivity(); activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_NORMAL); |