diff options
56 files changed, 3092 insertions, 944 deletions
diff --git a/core/java/android/app/IUidObserver.aidl b/core/java/android/app/IUidObserver.aidl index 0c920f1359f3..60c2eeddac63 100644 --- a/core/java/android/app/IUidObserver.aidl +++ b/core/java/android/app/IUidObserver.aidl @@ -58,8 +58,9 @@ oneway interface IUidObserver { * Report a proc oom adj change associated with a uid. * * @param uid The uid for which the state change is being reported. + * @param adj The minimum OOM adj among all processes with this uid. */ - void onUidProcAdjChanged(int uid); + void onUidProcAdjChanged(int uid, int adj); // =============== End of transactions used on native side as well ============================ diff --git a/core/java/android/app/UidObserver.java b/core/java/android/app/UidObserver.java index 9e928073ac5c..519662448e91 100644 --- a/core/java/android/app/UidObserver.java +++ b/core/java/android/app/UidObserver.java @@ -41,7 +41,7 @@ public class UidObserver extends IUidObserver.Stub { } @Override - public void onUidProcAdjChanged(int uid) { + public void onUidProcAdjChanged(int uid, int adj) { } @Override diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 24e28e95cd98..5bcbaa10e95b 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -65,7 +65,6 @@ import android.util.Log; import android.view.WindowManager.LayoutParams; import com.android.internal.R; -import com.android.internal.util.FrameworkStatsLog; import java.io.IOException; import java.lang.annotation.Retention; @@ -2533,38 +2532,6 @@ public class UserManager { } /** - * Returns the enum defined in the statsd UserLifecycleJourneyReported atom corresponding to the - * user type. - * @hide - */ - public static int getUserTypeForStatsd(@NonNull String userType) { - switch (userType) { - case USER_TYPE_FULL_SYSTEM: - return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SYSTEM; - case USER_TYPE_FULL_SECONDARY: - return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY; - case USER_TYPE_FULL_GUEST: - return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_GUEST; - case USER_TYPE_FULL_DEMO: - return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_DEMO; - case USER_TYPE_FULL_RESTRICTED: - return FrameworkStatsLog - .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_RESTRICTED; - case USER_TYPE_PROFILE_MANAGED: - return FrameworkStatsLog - .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__PROFILE_MANAGED; - case USER_TYPE_SYSTEM_HEADLESS: - return FrameworkStatsLog - .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__SYSTEM_HEADLESS; - case USER_TYPE_PROFILE_CLONE: - return FrameworkStatsLog - .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__PROFILE_CLONE; - default: - return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN; - } - } - - /** * @hide * @deprecated Use {@link #isRestrictedProfile()} */ diff --git a/core/tests/coretests/jni/NativeWorkSourceParcelTest.cpp b/core/tests/coretests/jni/NativeWorkSourceParcelTest.cpp index db1f7bde4b4f..187e2c137f85 100644 --- a/core/tests/coretests/jni/NativeWorkSourceParcelTest.cpp +++ b/core/tests/coretests/jni/NativeWorkSourceParcelTest.cpp @@ -77,15 +77,18 @@ static void nativeUnparcelAndVerifyWorkSource(JNIEnv* env, jobject /* obj */, jo Parcel* parcel = nativeGetParcelData(env, wsParcel); int32_t endMarker; - // read WorkSource and if no error read end marker - status_t err = ws.readFromParcel(parcel) ?: parcel->readInt32(&endMarker); - int32_t dataAvailable = parcel->dataAvail(); - + status_t err = ws.readFromParcel(parcel); if (err != OK) { - ALOGE("WorkSource readFromParcel failed %d", err); + jniThrowException(env, "java/lang/IllegalArgumentException", + StringPrintf("WorkSource readFromParcel failed: %d", err).c_str()); + } + err = parcel->readInt32(&endMarker); + if (err != OK) { + jniThrowException(env, "java/lang/IllegalArgumentException", + StringPrintf("Failed to read endMarker: %d", err).c_str()); } - // Now we have a native WorkSource object, verify it. + int32_t dataAvailable = parcel->dataAvail(); if (dataAvailable > 0) { // not all data read from the parcel jniThrowException(env, "java/lang/IllegalArgumentException", StringPrintf("WorkSource contains more data than native read (%d)", diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 40cb7f2194e9..20a43fc1beb0 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -520,6 +520,8 @@ applications that come with the platform <permission name="android.permission.SOUND_TRIGGER_RUN_IN_BATTERY_SAVER"/> <!-- Permission required for CTS test - SatelliteManagerTest --> <permission name="android.permission.SATELLITE_COMMUNICATION"/> + <!-- Permission required for GTS test - GtsAttestationVerificationDeviceSideTestCases --> + <permission name="android.permission.USE_ATTESTATION_VERIFICATION_SERVICE" /> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 80e920fefe5f..28368ef37061 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -189,12 +189,13 @@ public abstract class WMShellBaseModule { static Optional<DragAndDropController> provideDragAndDropController(Context context, ShellInit shellInit, ShellController shellController, + ShellCommandHandler shellCommandHandler, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, @ShellMainThread ShellExecutor mainExecutor) { return Optional.ofNullable(DragAndDropController.create(context, shellInit, shellController, - displayController, uiEventLogger, iconProvider, mainExecutor)); + shellCommandHandler, displayController, uiEventLogger, iconProvider, mainExecutor)); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index 091de3a86461..be2489da3628 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -35,10 +35,14 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP; + import android.content.ClipDescription; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.res.Configuration; +import android.graphics.HardwareRenderer; import android.graphics.PixelFormat; import android.util.Slog; import android.util.SparseArray; @@ -50,6 +54,8 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; +import androidx.annotation.BinderThread; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.logging.InstanceId; @@ -58,25 +64,31 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ExternalInterfaceBinder; +import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ExternalMainThread; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import java.io.PrintWriter; import java.util.ArrayList; /** * Handles the global drag and drop handling for the Shell. */ -public class DragAndDropController implements DisplayController.OnDisplaysChangedListener, +public class DragAndDropController implements RemoteCallable<DragAndDropController>, + DisplayController.OnDisplaysChangedListener, View.OnDragListener, ComponentCallbacks2 { private static final String TAG = DragAndDropController.class.getSimpleName(); private final Context mContext; private final ShellController mShellController; + private final ShellCommandHandler mShellCommandHandler; private final DisplayController mDisplayController; private final DragAndDropEventLogger mLogger; private final IconProvider mIconProvider; @@ -100,6 +112,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange public static DragAndDropController create(Context context, ShellInit shellInit, ShellController shellController, + ShellCommandHandler shellCommandHandler, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, @@ -107,19 +120,21 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange if (!context.getResources().getBoolean(R.bool.config_enableShellDragDrop)) { return null; } - return new DragAndDropController(context, shellInit, shellController, displayController, - uiEventLogger, iconProvider, mainExecutor); + return new DragAndDropController(context, shellInit, shellController, shellCommandHandler, + displayController, uiEventLogger, iconProvider, mainExecutor); } DragAndDropController(Context context, ShellInit shellInit, ShellController shellController, + ShellCommandHandler shellCommandHandler, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, ShellExecutor mainExecutor) { mContext = context; mShellController = shellController; + mShellCommandHandler = shellCommandHandler; mDisplayController = displayController; mLogger = new DragAndDropEventLogger(uiEventLogger); mIconProvider = iconProvider; @@ -137,6 +152,23 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange mMainExecutor.executeDelayed(() -> { mDisplayController.addDisplayWindowListener(this); }, 0); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_DRAG_AND_DROP, + this::createExternalInterface, this); + mShellCommandHandler.addDumpCallback(this::dump, this); + } + + private ExternalInterfaceBinder createExternalInterface() { + return new IDragAndDropImpl(this); + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; } /** @@ -156,7 +188,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange mListeners.remove(listener); } - private void notifyListeners() { + private void notifyDragStarted() { for (int i = 0; i < mListeners.size(); i++) { mListeners.get(i).onDragStarted(); } @@ -273,7 +305,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange pd.dragLayout.prepare(mDisplayController.getDisplayLayout(displayId), event.getClipData(), loggerSessionId); setDropTargetWindowVisibility(pd, View.VISIBLE); - notifyListeners(); + notifyDragStarted(); break; case ACTION_DRAG_ENTERED: pd.dragLayout.show(); @@ -327,13 +359,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange } private void setDropTargetWindowVisibility(PerDisplay pd, int visibility) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, - "Set drop target window visibility: displayId=%d visibility=%d", - pd.displayId, visibility); - pd.rootView.setVisibility(visibility); - if (visibility == View.VISIBLE) { - pd.rootView.requestApplyInsets(); - } + pd.setWindowVisibility(visibility); } private String getMimeTypes(ClipDescription description) { @@ -347,6 +373,18 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange return mimeTypes; } + /** + * Returns if any displays are currently ready to handle a drag/drop. + */ + private boolean isReadyToHandleDrag() { + for (int i = 0; i < mDisplayDropTargets.size(); i++) { + if (mDisplayDropTargets.valueAt(i).mHasDrawn) { + return true; + } + } + return false; + } + // Note: Component callbacks are always called on the main thread of the process @ExternalMainThread @Override @@ -372,12 +410,53 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange // Do nothing } - private static class PerDisplay { + /** + * Dumps information about this controller. + */ + public void dump(@NonNull PrintWriter pw, String prefix) { + pw.println(prefix + TAG); + pw.println(prefix + " listeners=" + mListeners.size()); + } + + /** + * The interface for calls from outside the host process. + */ + @BinderThread + private static class IDragAndDropImpl extends IDragAndDrop.Stub + implements ExternalInterfaceBinder { + private DragAndDropController mController; + + public IDragAndDropImpl(DragAndDropController controller) { + mController = controller; + } + + /** + * Invalidates this instance, preventing future calls from updating the controller. + */ + @Override + public void invalidate() { + mController = null; + } + + @Override + public boolean isReadyToHandleDrag() { + boolean[] result = new boolean[1]; + executeRemoteCallWithTaskPermission(mController, "isReadyToHandleDrag", + controller -> result[0] = controller.isReadyToHandleDrag(), + true /* blocking */ + ); + return result[0]; + } + } + + private static class PerDisplay implements HardwareRenderer.FrameDrawingCallback { final int displayId; final Context context; final WindowManager wm; final FrameLayout rootView; final DragLayout dragLayout; + // Tracks whether the window has fully drawn since it was last made visible + boolean mHasDrawn; boolean isHandlingDrag; // A count of the number of active drags in progress to ensure that we only hide the window @@ -391,5 +470,25 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange rootView = rv; dragLayout = dl; } + + private void setWindowVisibility(int visibility) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Set drop target window visibility: displayId=%d visibility=%d", + displayId, visibility); + rootView.setVisibility(visibility); + if (visibility == View.VISIBLE) { + rootView.requestApplyInsets(); + if (!mHasDrawn && rootView.getViewRootImpl() != null) { + rootView.getViewRootImpl().registerRtFrameCallback(this); + } + } else { + mHasDrawn = false; + } + } + + @Override + public void onFrameDraw(long frame) { + mHasDrawn = true; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/IDragAndDrop.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/IDragAndDrop.aidl new file mode 100644 index 000000000000..aeb0c63fa4c5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/IDragAndDrop.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.draganddrop; + +/** + * Interface that is exposed to remote callers to manipulate drag and drop. + */ +interface IDragAndDrop { + /** + * Returns whether the shell drop target is showing and will handle a drag/drop. + */ + boolean isReadyToHandleDrag() = 1; +} +// Last id = 1
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java index bfa63909cd47..5f54f58557d1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java @@ -42,4 +42,6 @@ public class ShellSharedConstants { public static final String KEY_EXTRA_SHELL_FLOATING_TASKS = "extra_shell_floating_tasks"; // See IDesktopMode.aidl public static final String KEY_EXTRA_SHELL_DESKTOP_MODE = "extra_shell_desktop_mode"; + // See IDragAndDrop.aidl + public static final String KEY_EXTRA_SHELL_DRAG_AND_DROP = "extra_shell_drag_and_drop"; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index f79f1f7a27b1..a73ab776486b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -533,8 +533,12 @@ public class Transitions implements RemoteCallable<Transitions>, final int layer; // Put all the OPEN/SHOW on top if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { - // Wallpaper is always at the bottom. - layer = -zSplitLine; + // Wallpaper is always at the bottom, opening wallpaper on top of closing one. + if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { + layer = -zSplitLine + numChanges - i; + } else { + layer = -zSplitLine - i; + } } else if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { if (isOpening) { // put on top diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java index 523cb6629d9a..54f36f61859d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -48,6 +48,7 @@ import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -71,6 +72,8 @@ public class DragAndDropControllerTest extends ShellTestCase { @Mock private ShellController mShellController; @Mock + private ShellCommandHandler mShellCommandHandler; + @Mock private DisplayController mDisplayController; @Mock private UiEventLogger mUiEventLogger; @@ -89,7 +92,8 @@ public class DragAndDropControllerTest extends ShellTestCase { public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); mController = new DragAndDropController(mContext, mShellInit, mShellController, - mDisplayController, mUiEventLogger, mIconProvider, mMainExecutor); + mShellCommandHandler, mDisplayController, mUiEventLogger, mIconProvider, + mMainExecutor); mController.onInit(); } diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp index 7e9d44fbdbd1..c00a2707e0a2 100644 --- a/libs/hwui/renderthread/CacheManager.cpp +++ b/libs/hwui/renderthread/CacheManager.cpp @@ -29,6 +29,7 @@ #include "Layer.h" #include "Properties.h" #include "RenderThread.h" +#include "VulkanManager.h" #include "pipeline/skia/ATraceMemoryDump.h" #include "pipeline/skia/ShaderCache.h" #include "pipeline/skia/SkiaMemoryTracer.h" @@ -182,8 +183,14 @@ void CacheManager::dumpMemoryUsage(String8& log, const RenderState* renderState) } log.appendFormat("Contexts: %zu (stopped = %zu)\n", mCanvasContexts.size(), stoppedContexts); + auto vkInstance = VulkanManager::peekInstance(); if (!mGrContext) { - log.appendFormat("No GPU context.\n"); + if (!vkInstance) { + log.appendFormat("No GPU context.\n"); + } else { + log.appendFormat("No GrContext; however %d remaining Vulkan refs", + vkInstance->getStrongCount() - 1); + } return; } std::vector<skiapipeline::ResourcePair> cpuResourceMap = { diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index f198bca9060c..4cffc6c2efe3 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -107,11 +107,11 @@ GrVkGetProc VulkanManager::sSkiaGetProp = [](const char* proc_name, VkInstance i #define GET_INST_PROC(F) m##F = (PFN_vk##F)vkGetInstanceProcAddr(mInstance, "vk" #F) #define GET_DEV_PROC(F) m##F = (PFN_vk##F)vkGetDeviceProcAddr(mDevice, "vk" #F) -sp<VulkanManager> VulkanManager::getInstance() { - // cache a weakptr to the context to enable a second thread to share the same vulkan state - static wp<VulkanManager> sWeakInstance = nullptr; - static std::mutex sLock; +// cache a weakptr to the context to enable a second thread to share the same vulkan state +static wp<VulkanManager> sWeakInstance = nullptr; +static std::mutex sLock; +sp<VulkanManager> VulkanManager::getInstance() { std::lock_guard _lock{sLock}; sp<VulkanManager> vulkanManager = sWeakInstance.promote(); if (!vulkanManager.get()) { @@ -122,6 +122,11 @@ sp<VulkanManager> VulkanManager::getInstance() { return vulkanManager; } +sp<VulkanManager> VulkanManager::peekInstance() { + std::lock_guard _lock{sLock}; + return sWeakInstance.promote(); +} + VulkanManager::~VulkanManager() { if (mDevice != VK_NULL_HANDLE) { mDeviceWaitIdle(mDevice); @@ -404,9 +409,13 @@ void VulkanManager::initialize() { } } -sk_sp<GrDirectContext> VulkanManager::createContext(const GrContextOptions& options, - ContextType contextType) { +static void onGrContextReleased(void* context) { + VulkanManager* manager = (VulkanManager*)context; + manager->decStrong((void*)onGrContextReleased); +} +sk_sp<GrDirectContext> VulkanManager::createContext(GrContextOptions& options, + ContextType contextType) { GrVkBackendContext backendContext; backendContext.fInstance = mInstance; backendContext.fPhysicalDevice = mPhysicalDevice; @@ -418,6 +427,11 @@ sk_sp<GrDirectContext> VulkanManager::createContext(const GrContextOptions& opti backendContext.fDeviceFeatures2 = &mPhysicalDeviceFeatures2; backendContext.fGetProc = sSkiaGetProp; + LOG_ALWAYS_FATAL_IF(options.fContextDeleteProc != nullptr, "Conflicting fContextDeleteProcs!"); + this->incStrong((void*)onGrContextReleased); + options.fContextDeleteContext = this; + options.fContextDeleteProc = onGrContextReleased; + return GrDirectContext::MakeVulkan(backendContext, options); } diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h index c5196eeccea3..00a40c0c85c3 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -66,6 +66,7 @@ class RenderThread; class VulkanManager final : public RefBase { public: static sp<VulkanManager> getInstance(); + static sp<VulkanManager> peekInstance(); // Sets up the vulkan context that is shared amonst all clients of the VulkanManager. This must // be call once before use of the VulkanManager. Multiple calls after the first will simiply @@ -109,7 +110,7 @@ public: }; // returns a Skia graphic context used to draw content on the specified thread - sk_sp<GrDirectContext> createContext(const GrContextOptions& options, + sk_sp<GrDirectContext> createContext(GrContextOptions& options, ContextType contextType = ContextType::kRenderThread); uint32_t getDriverVersion() const { return mDriverVersion; } diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index e73cf87ba9f3..3123ee6dd4d7 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -1237,6 +1237,9 @@ public class AudioSystem public static final Set<Integer> DEVICE_IN_ALL_SCO_SET; /** @hide */ public static final Set<Integer> DEVICE_IN_ALL_USB_SET; + /** @hide */ + public static final Set<Integer> DEVICE_IN_ALL_BLE_SET; + static { DEVICE_IN_ALL_SET = new HashSet<>(); DEVICE_IN_ALL_SET.add(DEVICE_IN_COMMUNICATION); @@ -1276,6 +1279,66 @@ public class AudioSystem DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_ACCESSORY); DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_DEVICE); DEVICE_IN_ALL_USB_SET.add(DEVICE_IN_USB_HEADSET); + + DEVICE_IN_ALL_BLE_SET = new HashSet<>(); + DEVICE_IN_ALL_BLE_SET.add(DEVICE_IN_BLE_HEADSET); + } + + /** @hide */ + public static boolean isBluetoothDevice(int deviceType) { + return isBluetoothA2dpOutDevice(deviceType) + || isBluetoothScoDevice(deviceType) + || isBluetoothLeDevice(deviceType); + } + + /** @hide */ + public static boolean isBluetoothOutDevice(int deviceType) { + return isBluetoothA2dpOutDevice(deviceType) + || isBluetoothScoOutDevice(deviceType) + || isBluetoothLeOutDevice(deviceType); + } + + /** @hide */ + public static boolean isBluetoothInDevice(int deviceType) { + return isBluetoothScoInDevice(deviceType) + || isBluetoothLeInDevice(deviceType); + } + + /** @hide */ + public static boolean isBluetoothA2dpOutDevice(int deviceType) { + return DEVICE_OUT_ALL_A2DP_SET.contains(deviceType); + } + + /** @hide */ + public static boolean isBluetoothScoOutDevice(int deviceType) { + return DEVICE_OUT_ALL_SCO_SET.contains(deviceType); + } + + /** @hide */ + public static boolean isBluetoothScoInDevice(int deviceType) { + return DEVICE_IN_ALL_SCO_SET.contains(deviceType); + } + + /** @hide */ + public static boolean isBluetoothScoDevice(int deviceType) { + return isBluetoothScoOutDevice(deviceType) + || isBluetoothScoInDevice(deviceType); + } + + /** @hide */ + public static boolean isBluetoothLeOutDevice(int deviceType) { + return DEVICE_OUT_ALL_BLE_SET.contains(deviceType); + } + + /** @hide */ + public static boolean isBluetoothLeInDevice(int deviceType) { + return DEVICE_IN_ALL_BLE_SET.contains(deviceType); + } + + /** @hide */ + public static boolean isBluetoothLeDevice(int deviceType) { + return isBluetoothLeOutDevice(deviceType) + || isBluetoothLeInDevice(deviceType); } /** @hide */ diff --git a/native/android/activity_manager.cpp b/native/android/activity_manager.cpp index 155a355241c8..bc6a84f01517 100644 --- a/native/android/activity_manager.cpp +++ b/native/android/activity_manager.cpp @@ -45,7 +45,7 @@ struct UidObserver : public BnUidObserver, public virtual IBinder::DeathRecipien void onUidIdle(uid_t uid, bool disabled) override; void onUidStateChanged(uid_t uid, int32_t procState, int64_t procStateSeq, int32_t capability) override; - void onUidProcAdjChanged(uid_t uid) override; + void onUidProcAdjChanged(uid_t uid, int32_t adj) override; // IBinder::DeathRecipient implementation void binderDied(const wp<IBinder>& who) override; @@ -121,7 +121,7 @@ void UidObserver::onUidActive(uid_t uid __unused) {} void UidObserver::onUidIdle(uid_t uid __unused, bool disabled __unused) {} -void UidObserver::onUidProcAdjChanged(uid_t uid __unused) {} +void UidObserver::onUidProcAdjChanged(uid_t uid __unused, int32_t adj __unused) {} void UidObserver::onUidStateChanged(uid_t uid, int32_t procState, int64_t procStateSeq __unused, diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index a110f56d09bd..34a2ce34738d 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -834,6 +834,9 @@ <uses-permission android:name="android.permission.LOG_FOREGROUND_RESOURCE_USE"/> + <!-- Permission required for GTS test - GtsAttestationVerificationDeviceSideTestCases --> + <uses-permission android:name="android.permission.USE_ATTESTATION_VERIFICATION_SERVICE" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" android:defaultToDeviceProtectedStorage="true" diff --git a/packages/SystemUI/res/layout/smart_action_button.xml b/packages/SystemUI/res/layout/smart_action_button.xml index 488be3a4479e..4e5785d284ee 100644 --- a/packages/SystemUI/res/layout/smart_action_button.xml +++ b/packages/SystemUI/res/layout/smart_action_button.xml @@ -29,8 +29,8 @@ android:textSize="@dimen/smart_reply_button_font_size" android:lineSpacingExtra="@dimen/smart_reply_button_line_spacing_extra" android:textColor="@color/smart_reply_button_text" - android:paddingLeft="@dimen/smart_reply_button_action_padding_left" - android:paddingRight="@dimen/smart_reply_button_padding_horizontal" + android:paddingStart="@dimen/smart_reply_button_action_padding_left" + android:paddingEnd="@dimen/smart_reply_button_padding_horizontal" android:drawablePadding="@dimen/smart_action_button_icon_padding" android:textStyle="normal" android:ellipsize="none"/> diff --git a/packages/SystemUI/res/layout/smart_reply_button.xml b/packages/SystemUI/res/layout/smart_reply_button.xml index ddf16e0afed7..b24362febbdd 100644 --- a/packages/SystemUI/res/layout/smart_reply_button.xml +++ b/packages/SystemUI/res/layout/smart_reply_button.xml @@ -31,7 +31,7 @@ android:textSize="@dimen/smart_reply_button_font_size" android:lineSpacingExtra="@dimen/smart_reply_button_line_spacing_extra" android:textColor="@color/smart_reply_button_text" - android:paddingLeft="@dimen/smart_reply_button_padding_horizontal" - android:paddingRight="@dimen/smart_reply_button_padding_horizontal" + android:paddingStart="@dimen/smart_reply_button_padding_horizontal" + android:paddingEnd="@dimen/smart_reply_button_padding_horizontal" android:textStyle="normal" android:ellipsize="none"/> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 499dfa4b3c9f..eaeaabe8779a 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -198,6 +198,9 @@ <item type="id" name="pm_lite"/> <item type="id" name="settings_button_container"/> + <!--Keyboard Backlight Dialog --> + <item type="id" name="keyboard_backlight_dialog_container"/> + <item type="id" name="log_access_dialog_allow_button" /> <item type="id" name="log_access_dialog_deny_button" /> </resources> diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 6ca409f07f6f..1fafa3d97c48 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -62,7 +62,8 @@ object Flags { val INSTANT_VOICE_REPLY = unreleasedFlag(111, "instant_voice_reply") // TODO(b/279735475): Tracking Bug - @JvmField val NEW_LIGHT_BAR_LOGIC = unreleasedFlag(279735475, "new_light_bar_logic") + @JvmField + val NEW_LIGHT_BAR_LOGIC = unreleasedFlag(279735475, "new_light_bar_logic", teamfood = true) /** * This flag is server-controlled and should stay as [unreleasedFlag] since we never want to diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt index 2ef5e19bf382..328beed3ae4b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt @@ -159,6 +159,7 @@ class KeyboardBacklightDialog( private fun buildRootView(): LinearLayout { val linearLayout = LinearLayout(context).apply { + id = R.id.keyboard_backlight_dialog_container orientation = LinearLayout.HORIZONTAL layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) setPadding( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 4522e41daf91..b4bfded58e4b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -966,7 +966,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder @Override public ApplicationInfo getApplicationInfo() { - ApplicationInfo applicationInfo = super.getApplicationInfo(); + ApplicationInfo applicationInfo = new ApplicationInfo(super.getApplicationInfo()); applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_RTL; return applicationInfo; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt index b563d86f65b1..21d03386b9e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt @@ -311,7 +311,7 @@ interface SmartActionInflater { setBounds(0, 0, newIconSize, newIconSize) } // Add the action icon to the Smart Action button. - setCompoundDrawables(iconDrawable, null, null, null) + setCompoundDrawablesRelative(iconDrawable, null, null, null) val onClickListener = View.OnClickListener { onSmartActionClick(entry, smartActions, actionIndex, action) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java index 9e88ceb3a0d1..fb6ba8542a3f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java @@ -588,15 +588,15 @@ public class SmartReplyView extends ViewGroup { } /** - * Returns the combined width of the left drawable (the action icon) and the padding between the - * drawable and the button text. + * Returns the combined width of the start drawable (the action icon) and the padding between + * the drawable and the button text. */ - private int getLeftCompoundDrawableWidthWithPadding(Button button) { - Drawable[] drawables = button.getCompoundDrawables(); - Drawable leftDrawable = drawables[0]; - if (leftDrawable == null) return 0; + private int getStartCompoundDrawableWidthWithPadding(Button button) { + Drawable[] drawables = button.getCompoundDrawablesRelative(); + Drawable startDrawable = drawables[0]; + if (startDrawable == null) return 0; - return leftDrawable.getBounds().width() + button.getCompoundDrawablePadding(); + return startDrawable.getBounds().width() + button.getCompoundDrawablePadding(); } private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) { @@ -605,8 +605,8 @@ public class SmartReplyView extends ViewGroup { // Re-measure the squeezed smart reply button. clearLayoutLineCount(button); final int widthMeasureSpec = MeasureSpec.makeMeasureSpec( - button.getPaddingLeft() + button.getPaddingRight() + textWidth - + getLeftCompoundDrawableWidthWithPadding(button), MeasureSpec.AT_MOST); + button.getPaddingStart() + button.getPaddingEnd() + textWidth + + getStartCompoundDrawableWidthWithPadding(button), MeasureSpec.AT_MOST); button.measure(widthMeasureSpec, heightMeasureSpec); if (button.getLayout() == null) { Log.wtf(TAG, "Button layout is null after measure."); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java index d9d8b6345fcb..3b0d5120cca3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java @@ -585,8 +585,6 @@ public class SmartReplyViewTest extends SysuiTestCase { // devices. layout.setBaselineAligned(false); - final boolean isRtl = mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - // Add smart replies Button previous = null; SmartReplyView.SmartReplies smartReplies = @@ -606,11 +604,7 @@ public class SmartReplyViewTest extends SysuiTestCase { if (previous != null) { ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) previous.getLayoutParams(); - if (isRtl) { - lp.leftMargin = mSpacing; - } else { - lp.rightMargin = mSpacing; - } + lp.setMarginEnd(mSpacing); } layout.addView(current); previous = current; @@ -634,11 +628,7 @@ public class SmartReplyViewTest extends SysuiTestCase { if (previous != null) { ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) previous.getLayoutParams(); - if (isRtl) { - lp.leftMargin = mSpacing; - } else { - lp.rightMargin = mSpacing; - } + lp.setMarginEnd(mSpacing); } layout.addView(current); previous = current; @@ -937,8 +927,8 @@ public class SmartReplyViewTest extends SysuiTestCase { .collect(Collectors.toList()); Button singleLineButton = buttons.get(0); Button doubleLineButton = buttons.get(1); - Drawable singleLineDrawable = singleLineButton.getCompoundDrawables()[0]; // left drawable - Drawable doubleLineDrawable = doubleLineButton.getCompoundDrawables()[0]; // left drawable + Drawable singleLineDrawable = singleLineButton.getCompoundDrawablesRelative()[0]; // start + Drawable doubleLineDrawable = doubleLineButton.getCompoundDrawablesRelative()[0]; // start assertEquals(singleLineDrawable.getBounds().width(), doubleLineDrawable.getBounds().width()); assertEquals(singleLineDrawable.getBounds().height(), diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 1a5d4253b5ba..a992765c0411 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -16060,6 +16060,8 @@ public class ActivityManagerService extends IActivityManager.Stub final int procState = uidRec != null ? uidRec.getSetProcState() : PROCESS_STATE_NONEXISTENT; + final int procAdj = uidRec != null + ? uidRec.getMinProcAdj() : ProcessList.INVALID_ADJ; final long procStateSeq = uidRec != null ? uidRec.curProcStateSeq : 0; final int capability = uidRec != null ? uidRec.getSetCapability() : 0; final boolean ephemeral = uidRec != null ? uidRec.isEphemeral() : isEphemeralLocked(uid); @@ -16075,7 +16077,7 @@ public class ActivityManagerService extends IActivityManager.Stub } final int enqueuedChange = mUidObserverController.enqueueUidChange( uidRec == null ? null : uidRec.pendingChange, - uid, change, procState, procStateSeq, capability, ephemeral); + uid, change, procState, procAdj, procStateSeq, capability, ephemeral); if (uidRec != null) { uidRec.setLastReportedChange(enqueuedChange); } diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java index 5a4d315767ca..4a69f90d9fc0 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java @@ -67,7 +67,6 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; -import android.os.UserManager; import android.text.TextUtils; import android.util.EventLog; import android.util.IndentingPrintWriter; @@ -78,6 +77,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.os.TimeoutRecord; import com.android.internal.util.FrameworkStatsLog; import com.android.server.LocalServices; +import com.android.server.pm.UserJourneyLogger; import com.android.server.pm.UserManagerInternal; import dalvik.annotation.optimization.NeverCompile; @@ -1518,7 +1518,7 @@ public class BroadcastQueueImpl extends BroadcastQueue { final UserInfo userInfo = (umInternal != null) ? umInternal.getUserInfo(r.userId) : null; if (userInfo != null) { - userType = UserManager.getUserTypeForStatsd(userInfo.userType); + userType = UserJourneyLogger.getUserTypeForStatsd(userInfo.userType); } Slog.i(TAG_BROADCAST, "BOOT_COMPLETED_BROADCAST_COMPLETION_LATENCY_REPORTED action:" diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 438a08c44ef4..0417b8cfa2e2 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -644,6 +644,11 @@ class ProcessRecord implements WindowProcessListener { } } + @GuardedBy({"mService", "mProcLock"}) + int getSetAdj() { + return mState.getSetAdj(); + } + @GuardedBy(anyOf = {"mService", "mProcLock"}) IApplicationThread getThread() { return mThread; diff --git a/services/core/java/com/android/server/am/UidObserverController.java b/services/core/java/com/android/server/am/UidObserverController.java index 51cb9878c0b3..790cc7b87f80 100644 --- a/services/core/java/com/android/server/am/UidObserverController.java +++ b/services/core/java/com/android/server/am/UidObserverController.java @@ -96,7 +96,7 @@ public class UidObserverController { } int enqueueUidChange(@Nullable ChangeRecord currentRecord, int uid, int change, int procState, - long procStateSeq, int capability, boolean ephemeral) { + int procAdj, long procStateSeq, int capability, boolean ephemeral) { synchronized (mLock) { if (mPendingUidChanges.size() == 0) { if (DEBUG_UID_OBSERVERS) { @@ -117,6 +117,7 @@ public class UidObserverController { changeRecord.uid = uid; changeRecord.change = change; changeRecord.procState = procState; + changeRecord.procAdj = procAdj; changeRecord.procStateSeq = procStateSeq; changeRecord.capability = capability; changeRecord.ephemeral = ephemeral; @@ -344,7 +345,7 @@ public class UidObserverController { } if ((reg.mWhich & ActivityManager.UID_OBSERVER_PROC_OOM_ADJ) != 0 && (change & UidRecord.CHANGE_PROCADJ) != 0) { - observer.onUidProcAdjChanged(item.uid); + observer.onUidProcAdjChanged(item.uid, item.procAdj); } } final int duration = (int) (SystemClock.uptimeMillis() - start); @@ -426,6 +427,7 @@ public class UidObserverController { public int uid; public int change; public int procState; + public int procAdj; public int capability; public boolean ephemeral; public long procStateSeq; @@ -435,6 +437,7 @@ public class UidObserverController { changeRecord.uid = uid; changeRecord.change = change; changeRecord.procState = procState; + changeRecord.procAdj = procAdj; changeRecord.capability = capability; changeRecord.ephemeral = ephemeral; changeRecord.procStateSeq = procStateSeq; diff --git a/services/core/java/com/android/server/am/UidRecord.java b/services/core/java/com/android/server/am/UidRecord.java index e39ac2b08479..993088ef106e 100644 --- a/services/core/java/com/android/server/am/UidRecord.java +++ b/services/core/java/com/android/server/am/UidRecord.java @@ -51,6 +51,12 @@ public final class UidRecord { private boolean mProcAdjChanged; @CompositeRWLock({"mService", "mProcLock"}) + private int mCurAdj; + + @CompositeRWLock({"mService", "mProcLock"}) + private int mSetAdj; + + @CompositeRWLock({"mService", "mProcLock"}) private int mCurCapability; @CompositeRWLock({"mService", "mProcLock"}) @@ -201,12 +207,24 @@ public final class UidRecord { mProcAdjChanged = false; } - @GuardedBy({"mService", "mProcLock"}) + @GuardedBy(anyOf = {"mService", "mProcLock"}) boolean getProcAdjChanged() { return mProcAdjChanged; } @GuardedBy(anyOf = {"mService", "mProcLock"}) + int getMinProcAdj() { + int minAdj = ProcessList.UNKNOWN_ADJ; + for (int i = mProcRecords.size() - 1; i >= 0; i--) { + int adj = mProcRecords.valueAt(i).getSetAdj(); + if (adj < minAdj) { + minAdj = adj; + } + } + return minAdj; + } + + @GuardedBy(anyOf = {"mService", "mProcLock"}) int getCurCapability() { return mCurCapability; } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index b2fdee7a6f89..06af2ce9d655 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -46,13 +46,24 @@ import static com.android.server.am.UserState.STATE_BOOTING; import static com.android.server.am.UserState.STATE_RUNNING_LOCKED; import static com.android.server.am.UserState.STATE_RUNNING_UNLOCKED; import static com.android.server.am.UserState.STATE_RUNNING_UNLOCKING; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_ABORTED; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_INVALID_SESSION_ID; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_BEGIN; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_FINISH; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_NONE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_START; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_STOP; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_SWITCH_FG; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_SWITCH_UI; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_UNLOCKED_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_UNLOCKING_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE; import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND_VISIBLE; import static com.android.server.pm.UserManagerInternal.USER_START_MODE_FOREGROUND; import static com.android.server.pm.UserManagerInternal.userAssignmentResultToString; import static com.android.server.pm.UserManagerInternal.userStartModeToString; -import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -123,6 +134,8 @@ import com.android.server.LocalServices; import com.android.server.SystemService.UserCompletedEventType; import com.android.server.SystemServiceManager; import com.android.server.am.UserState.KeyEvictedCallback; +import com.android.server.pm.UserJourneyLogger; +import com.android.server.pm.UserJourneyLogger.UserJourneySession; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerInternal.UserLifecycleListener; import com.android.server.pm.UserManagerInternal.UserStartMode; @@ -138,7 +151,6 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -221,75 +233,6 @@ class UserController implements Handler.Callback { // TODO(b/197344658): Increase to 10s or 15s once we have a switch-UX-is-done invocation too. private static final int USER_COMPLETED_EVENT_DELAY_MS = 5 * 1000; - // Used for statsd logging with UserLifecycleJourneyReported + UserLifecycleEventOccurred atoms - private static final long INVALID_SESSION_ID = 0; - - // The various user journeys, defined in the UserLifecycleJourneyReported atom for statsd - private static final int USER_JOURNEY_UNKNOWN = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__UNKNOWN; - private static final int USER_JOURNEY_USER_SWITCH_FG = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_SWITCH_FG; - private static final int USER_JOURNEY_USER_SWITCH_UI = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_SWITCH_UI; - private static final int USER_JOURNEY_USER_START = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_START; - private static final int USER_JOURNEY_USER_CREATE = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE; - private static final int USER_JOURNEY_USER_STOP = - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_STOP; - @IntDef(prefix = { "USER_JOURNEY" }, value = { - USER_JOURNEY_UNKNOWN, - USER_JOURNEY_USER_SWITCH_FG, - USER_JOURNEY_USER_SWITCH_UI, - USER_JOURNEY_USER_START, - USER_JOURNEY_USER_CREATE, - USER_JOURNEY_USER_STOP - }) - @interface UserJourney {} - - // The various user lifecycle events, defined in the UserLifecycleEventOccurred atom for statsd - private static final int USER_LIFECYCLE_EVENT_UNKNOWN = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNKNOWN; - private static final int USER_LIFECYCLE_EVENT_SWITCH_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__SWITCH_USER; - private static final int USER_LIFECYCLE_EVENT_START_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__START_USER; - private static final int USER_LIFECYCLE_EVENT_CREATE_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__CREATE_USER; - private static final int USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__USER_RUNNING_LOCKED; - private static final int USER_LIFECYCLE_EVENT_UNLOCKING_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNLOCKING_USER; - private static final int USER_LIFECYCLE_EVENT_UNLOCKED_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNLOCKED_USER; - private static final int USER_LIFECYCLE_EVENT_STOP_USER = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__STOP_USER; - @IntDef(prefix = { "USER_LIFECYCLE_EVENT" }, value = { - USER_LIFECYCLE_EVENT_UNKNOWN, - USER_LIFECYCLE_EVENT_SWITCH_USER, - USER_LIFECYCLE_EVENT_START_USER, - USER_LIFECYCLE_EVENT_CREATE_USER, - USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, - USER_LIFECYCLE_EVENT_UNLOCKING_USER, - USER_LIFECYCLE_EVENT_UNLOCKED_USER, - USER_LIFECYCLE_EVENT_STOP_USER - }) - @interface UserLifecycleEvent {} - - // User lifecyle event state, defined in the UserLifecycleEventOccurred atom for statsd - private static final int USER_LIFECYCLE_EVENT_STATE_BEGIN = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__BEGIN; - private static final int USER_LIFECYCLE_EVENT_STATE_FINISH = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__FINISH; - private static final int USER_LIFECYCLE_EVENT_STATE_NONE = - FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__NONE; - @IntDef(prefix = { "USER_LIFECYCLE_EVENT_STATE" }, value = { - USER_LIFECYCLE_EVENT_STATE_BEGIN, - USER_LIFECYCLE_EVENT_STATE_FINISH, - USER_LIFECYCLE_EVENT_STATE_NONE, - }) - @interface UserLifecycleEventState {} - /** * Maximum number of users we allow to be running at a time, including system user. * @@ -420,13 +363,6 @@ class UserController implements Handler.Callback { private final ArrayList<Integer> mLastActiveUsers = new ArrayList<>(); /** - * {@link UserIdInt} to {@link UserJourneySession} mapping used for statsd logging for the - * UserLifecycleJourneyReported and UserLifecycleEventOccurred atoms. - */ - @GuardedBy("mUserIdToUserJourneyMap") - private final SparseArray<UserJourneySession> mUserIdToUserJourneyMap = new SparseArray<>(); - - /** * Map of userId to {@link UserCompletedEventType} event flags, indicating which as-yet- * unreported user-starting events have transpired for the given user. */ @@ -621,8 +557,9 @@ class UserController implements Handler.Callback { // but we might immediately step into RUNNING below if the user // storage is already unlocked. if (uss.setState(STATE_BOOTING, STATE_RUNNING_LOCKED)) { - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, - USER_LIFECYCLE_EVENT_STATE_NONE); + mInjector.getUserJourneyLogger() + .logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, + EVENT_STATE_NONE); mInjector.getUserManagerInternal().setUserState(userId, uss.state); // Do not report secondary users, runtime restarts or first boot/upgrade if (userId == UserHandle.USER_SYSTEM @@ -694,8 +631,9 @@ class UserController implements Handler.Callback { private boolean finishUserUnlocking(final UserState uss) { final int userId = uss.mHandle.getIdentifier(); EventLog.writeEvent(EventLogTags.UC_FINISH_USER_UNLOCKING, userId); - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_UNLOCKING_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + mInjector.getUserJourneyLogger() + .logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_UNLOCKING_USER, + EVENT_STATE_BEGIN); // If the user key hasn't been unlocked yet, we cannot proceed. if (!StorageManager.isUserKeyUnlocked(userId)) return false; synchronized (mLock) { @@ -1073,9 +1011,7 @@ class UserController implements Handler.Callback { return; } - logUserJourneyInfo(null, getUserInfo(userId), USER_JOURNEY_USER_STOP); - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_STOP_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + logUserJourneyBegin(userId, USER_JOURNEY_USER_STOP); if (stopUserCallback != null) { uss.mStopCallbacks.add(stopUserCallback); @@ -1138,9 +1074,16 @@ class UserController implements Handler.Callback { synchronized (mLock) { if (uss.state != UserState.STATE_STOPPING) { // Whoops, we are being started back up. Abort, abort! - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_STOP_USER, - USER_LIFECYCLE_EVENT_STATE_NONE); - clearSessionId(userId); + UserJourneySession session = mInjector.getUserJourneyLogger() + .logUserJourneyFinishWithError(-1, getUserInfo(userId), + USER_JOURNEY_USER_STOP, ERROR_CODE_ABORTED); + if (session != null) { + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, session); + } else { + mInjector.getUserJourneyLogger() + .logUserJourneyFinishWithError(-1, getUserInfo(userId), + USER_JOURNEY_USER_STOP, ERROR_CODE_INVALID_SESSION_ID); + } return; } uss.setState(UserState.STATE_SHUTDOWN); @@ -1247,9 +1190,11 @@ class UserController implements Handler.Callback { mInjector.getUserManager().removeUserEvenWhenDisallowed(userId); } - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_STOP_USER, - USER_LIFECYCLE_EVENT_STATE_FINISH); - clearSessionId(userId); + UserJourneySession session = mInjector.getUserJourneyLogger() + .logUserJourneyFinish(-1, userInfo, USER_JOURNEY_USER_STOP); + if (session != null) { + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, session); + } if (lockUser) { dispatchUserLocking(userIdToLock, keyEvictedCallbacks); @@ -1259,9 +1204,11 @@ class UserController implements Handler.Callback { // which was paused while the SHUTDOWN flow of the user was in progress. resumePendingUserStarts(userId); } else { - logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_STOP_USER, - USER_LIFECYCLE_EVENT_STATE_NONE); - clearSessionId(userId); + UserJourneySession session = mInjector.getUserJourneyLogger() + .finishAndClearIncompleteUserJourney(userId, USER_JOURNEY_USER_STOP); + if (session != null) { + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, session); + } } } @@ -3135,10 +3082,7 @@ class UserController implements Handler.Callback { public boolean handleMessage(Message msg) { switch (msg.what) { case START_USER_SWITCH_FG_MSG: - logUserJourneyInfo(getUserInfo(getCurrentUserId()), getUserInfo(msg.arg1), - USER_JOURNEY_USER_SWITCH_FG); - logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_SWITCH_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + logUserJourneyBegin(msg.arg1, USER_JOURNEY_USER_SWITCH_FG); startUserInForeground(msg.arg1); break; case REPORT_USER_SWITCH_MSG: @@ -3160,18 +3104,15 @@ class UserController implements Handler.Callback { mInjector.batteryStatsServiceNoteEvent( BatteryStats.HistoryItem.EVENT_USER_RUNNING_START, Integer.toString(msg.arg1), msg.arg1); - logUserJourneyInfo(null, getUserInfo(msg.arg1), USER_JOURNEY_USER_START); - logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_START_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + logUserJourneyBegin(msg.arg1, USER_JOURNEY_USER_START); mInjector.onUserStarting(/* userId= */ msg.arg1); scheduleOnUserCompletedEvent(msg.arg1, UserCompletedEventType.EVENT_TYPE_USER_STARTING, USER_COMPLETED_EVENT_DELAY_MS); - logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_START_USER, - USER_LIFECYCLE_EVENT_STATE_FINISH); - clearSessionId(msg.arg1, USER_JOURNEY_USER_START); + mInjector.getUserJourneyLogger().logUserJourneyFinish(-1 , getUserInfo(msg.arg1), + USER_JOURNEY_USER_START); break; case USER_UNLOCK_MSG: final int userId = msg.arg1; @@ -3180,10 +3121,11 @@ class UserController implements Handler.Callback { FgThread.getHandler().post(() -> { mInjector.loadUserRecents(userId); }); - logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_UNLOCKING_USER, - USER_LIFECYCLE_EVENT_STATE_FINISH); - logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_UNLOCKED_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + + mInjector.getUserJourneyLogger().logUserLifecycleEvent(msg.arg1, + USER_LIFECYCLE_EVENT_UNLOCKING_USER, EVENT_STATE_FINISH); + mInjector.getUserJourneyLogger().logUserLifecycleEvent(msg.arg1, + USER_LIFECYCLE_EVENT_UNLOCKED_USER, EVENT_STATE_BEGIN); final TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("finishUserUnlocked-" + userId); @@ -3199,9 +3141,9 @@ class UserController implements Handler.Callback { // (No need to acquire lock to read mCurrentUserId since it is volatile.) // TODO: Find something to wait for in the case of a profile. mCurrentUserId == msg.arg1 ? USER_COMPLETED_EVENT_DELAY_MS : 1000); - logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_UNLOCKED_USER, - USER_LIFECYCLE_EVENT_STATE_FINISH); - clearSessionId(msg.arg1); + mInjector.getUserJourneyLogger().logUserLifecycleEvent(msg.arg1, + USER_LIFECYCLE_EVENT_UNLOCKED_USER, EVENT_STATE_FINISH); + // Unlocking user is not a journey no need to clear sessionId break; case USER_CURRENT_MSG: mInjector.batteryStatsServiceNoteEvent( @@ -3224,22 +3166,24 @@ class UserController implements Handler.Callback { break; case REPORT_USER_SWITCH_COMPLETE_MSG: dispatchUserSwitchComplete(msg.arg1, msg.arg2); - logUserLifecycleEvent(msg.arg2, USER_LIFECYCLE_EVENT_SWITCH_USER, - USER_LIFECYCLE_EVENT_STATE_FINISH); + UserJourneySession session = mInjector.getUserJourneyLogger() + .logUserSwitchJourneyFinish(msg.arg1, getUserInfo(msg.arg2)); + if (session != null) { + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, session); + } break; case REPORT_LOCKED_BOOT_COMPLETE_MSG: dispatchLockedBootComplete(msg.arg1); break; case START_USER_SWITCH_UI_MSG: final Pair<UserInfo, UserInfo> fromToUserPair = (Pair<UserInfo, UserInfo>) msg.obj; - logUserJourneyInfo(fromToUserPair.first, fromToUserPair.second, - USER_JOURNEY_USER_SWITCH_UI); - logUserLifecycleEvent(fromToUserPair.second.id, USER_LIFECYCLE_EVENT_SWITCH_USER, - USER_LIFECYCLE_EVENT_STATE_BEGIN); + logUserJourneyBegin(fromToUserPair.second.id, USER_JOURNEY_USER_SWITCH_UI); showUserSwitchDialog(fromToUserPair); break; case CLEAR_USER_JOURNEY_SESSION_MSG: - logAndClearSessionId(msg.arg1); + mInjector.getUserJourneyLogger() + .finishAndClearIncompleteUserJourney(msg.arg1, msg.arg2); + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, msg.obj); break; case COMPLETE_USER_SWITCH_MSG: completeUserSwitch(msg.arg1, msg.arg2); @@ -3317,123 +3261,29 @@ class UserController implements Handler.Callback { * statsd helper method for logging the start of a user journey via a UserLifecycleEventOccurred * atom given the originating and targeting users for the journey. */ - private void logUserJourneyInfo(UserInfo origin, UserInfo target, @UserJourney int journey) { - final long newSessionId = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE); - synchronized (mUserIdToUserJourneyMap) { - UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(target.id); - if (userJourneySession != null) { - // TODO(b/157007231): Move this logic to a separate class/file. - if ((userJourneySession.mJourney == USER_JOURNEY_USER_SWITCH_UI - || userJourneySession.mJourney == USER_JOURNEY_USER_SWITCH_FG) - && (journey == USER_JOURNEY_USER_START - || journey == USER_JOURNEY_USER_STOP)) { - /* - * There is already a user switch journey, and a user start or stop journey for - * the same target user received. New journey is most likely a part of user - * switch journey so no need to create a new journey. - */ - if (DEBUG_MU) { - Slogf.d(TAG, journey + " not logged as it is expected to be part of " - + userJourneySession.mJourney); - } - return; - } - /* - * Possible reasons for this condition to be true: - * - A user switch journey is received while another user switch journey is in - * process for the same user. - * - A user switch journey is received while user start journey is in process for - * the same user. - * - A user start journey is received while another user start journey is in process - * for the same user. - * In all cases potentially an incomplete, timed-out session or multiple - * simultaneous requests. It is not possible to keep track of multiple sessions for - * the same user, so previous session is abandoned. - */ - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, - userJourneySession.mSessionId, target.id, USER_LIFECYCLE_EVENT_UNKNOWN, - USER_LIFECYCLE_EVENT_STATE_NONE); - } - + private void logUserJourneyBegin(int targetId, + @UserJourneyLogger.UserJourney int journey) { + UserJourneySession oldSession = mInjector.getUserJourneyLogger() + .finishAndClearIncompleteUserJourney(targetId, journey); + if (oldSession != null) { if (DEBUG_MU) { Slogf.d(TAG, - "Starting a new journey: " + journey + " with session id: " + newSessionId); + "Starting a new journey: " + journey + " with session id: " + + oldSession); } - - userJourneySession = new UserJourneySession(newSessionId, journey); - mUserIdToUserJourneyMap.put(target.id, userJourneySession); /* - * User lifecyle journey would be complete when {@code #clearSessionId} is called after - * the last expected lifecycle event for the journey. It may be possible that the last - * event is not called, e.g., user not unlocked after user switching. In such cases user - * journey is cleared after {@link USER_JOURNEY_TIMEOUT}. + * User lifecycle journey would be complete when {@code #clearSessionId} is called + * after the last expected lifecycle event for the journey. It may be possible that + * the last event is not called, e.g., user not unlocked after user switching. In such + * cases user journey is cleared after {@link USER_JOURNEY_TIMEOUT}. */ - mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG); - mHandler.sendMessageDelayed(mHandler.obtainMessage(CLEAR_USER_JOURNEY_SESSION_MSG, - target.id, /* arg2= */ 0), USER_JOURNEY_TIMEOUT_MS); - } - - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED, newSessionId, - journey, origin != null ? origin.id : -1, - target.id, UserManager.getUserTypeForStatsd(target.userType), target.flags); - } - - /** - * statsd helper method for logging the given event for the UserLifecycleEventOccurred statsd - * atom. - */ - private void logUserLifecycleEvent(@UserIdInt int userId, @UserLifecycleEvent int event, - @UserLifecycleEventState int eventState) { - final long sessionId; - synchronized (mUserIdToUserJourneyMap) { - final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(userId); - if (userJourneySession == null || userJourneySession.mSessionId == INVALID_SESSION_ID) { - Slogf.w(TAG, "UserLifecycleEvent " + event - + " received without an active userJourneySession."); - return; - } - sessionId = userJourneySession.mSessionId; - } - - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, sessionId, userId, - event, eventState); - } - - /** - * Clears the {@link UserJourneySession} for a given {@link UserIdInt} and {@link UserJourney}. - */ - private void clearSessionId(@UserIdInt int userId, @UserJourney int journey) { - synchronized (mUserIdToUserJourneyMap) { - final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(userId); - if (userJourneySession != null && userJourneySession.mJourney == journey) { - clearSessionId(userId); - } + mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG, oldSession); } - } + UserJourneySession newSession = mInjector.getUserJourneyLogger() + .logUserJourneyBegin(targetId, journey); + mHandler.sendMessageDelayed(mHandler.obtainMessage(CLEAR_USER_JOURNEY_SESSION_MSG, + targetId, /* arg2= */ journey, newSession), USER_JOURNEY_TIMEOUT_MS); - /** - * Clears the {@link UserJourneySession} for a given {@link UserIdInt}. - */ - private void clearSessionId(@UserIdInt int userId) { - synchronized (mUserIdToUserJourneyMap) { - mHandler.removeMessages(CLEAR_USER_JOURNEY_SESSION_MSG); - mUserIdToUserJourneyMap.delete(userId); - } - } - - /** - * Log a final event of the {@link UserJourneySession} and clear it. - */ - private void logAndClearSessionId(@UserIdInt int userId) { - synchronized (mUserIdToUserJourneyMap) { - final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(userId); - if (userJourneySession != null) { - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, - userJourneySession.mSessionId, userId, USER_LIFECYCLE_EVENT_UNKNOWN, - USER_LIFECYCLE_EVENT_STATE_NONE); - } - clearSessionId(userId); - } } private BroadcastOptions getTemporaryAppAllowlistBroadcastOptions( @@ -3471,23 +3321,6 @@ class UserController implements Handler.Callback { return mLastUserUnlockingUptime; } - /** - * Helper class to store user journey and session id. - * - * <p> User journey tracks a chain of user lifecycle events occurring during different user - * activities such as user start, user switch, and user creation. - */ - // TODO(b/157007231): Move this class and user journey tracking logic to a separate file. - private static class UserJourneySession { - final long mSessionId; - @UserJourney final int mJourney; - - UserJourneySession(long sessionId, @UserJourney int journey) { - mJourney = journey; - mSessionId = sessionId; - } - } - private static class UserProgressListener extends IProgressListener.Stub { private volatile long mUnlockStarted; @Override @@ -3562,6 +3395,10 @@ class UserController implements Handler.Callback { return new Handler(mService.mUiHandler.getLooper(), callback); } + protected UserJourneyLogger getUserJourneyLogger() { + return getUserManager().getUserJourneyLogger(); + } + protected Context getContext() { return mService.mContext; } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index c879fa64ff02..bc4e8df2a4ad 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -88,14 +88,14 @@ import java.util.concurrent.atomic.AtomicBoolean; private final @NonNull AudioSystemAdapter mAudioSystem; /** ID for Communication strategy retrieved form audio policy manager */ - private int mCommunicationStrategyId = -1; + /*package*/ int mCommunicationStrategyId = -1; /** ID for Accessibility strategy retrieved form audio policy manager */ private int mAccessibilityStrategyId = -1; /** Active communication device reported by audio policy manager */ - private AudioDeviceInfo mActiveCommunicationDevice; + /*package*/ AudioDeviceInfo mActiveCommunicationDevice; /** Last preferred device set for communication strategy */ private AudioDeviceAttributes mPreferredCommunicationDevice; @@ -755,6 +755,19 @@ import java.util.concurrent.atomic.AtomicBoolean; mIsLeOutput = false; } + BtDeviceInfo(@NonNull BtDeviceInfo src, int state) { + mDevice = src.mDevice; + mState = state; + mProfile = src.mProfile; + mSupprNoisy = src.mSupprNoisy; + mVolume = src.mVolume; + mIsLeOutput = src.mIsLeOutput; + mEventSource = src.mEventSource; + mAudioSystemDevice = src.mAudioSystemDevice; + mMusicDevice = src.mMusicDevice; + mCodec = src.mCodec; + } + // redefine equality op so we can match messages intended for this device @Override public boolean equals(Object o) { @@ -821,7 +834,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * @param info struct with the (dis)connection information */ /*package*/ void queueOnBluetoothActiveDeviceChanged(@NonNull BtDeviceChangedData data) { - if (data.mInfo.getProfile() == BluetoothProfile.A2DP && data.mPreviousDevice != null + if (data.mPreviousDevice != null && data.mPreviousDevice.equals(data.mNewDevice)) { final String name = TextUtils.emptyIfNull(data.mNewDevice.getName()); new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + MediaMetrics.SEPARATOR @@ -830,7 +843,8 @@ import java.util.concurrent.atomic.AtomicBoolean; .set(MediaMetrics.Property.STATUS, data.mInfo.getProfile()) .record(); synchronized (mDeviceStateLock) { - postBluetoothA2dpDeviceConfigChange(data.mNewDevice); + postBluetoothDeviceConfigChange(createBtDeviceInfo(data, data.mNewDevice, + BluetoothProfile.STATE_CONNECTED)); } } else { synchronized (mDeviceStateLock) { @@ -1064,8 +1078,8 @@ import java.util.concurrent.atomic.AtomicBoolean; new AudioModeInfo(mode, pid, uid)); } - /*package*/ void postBluetoothA2dpDeviceConfigChange(@NonNull BluetoothDevice device) { - sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONFIG_CHANGE, SENDMSG_QUEUE, device); + /*package*/ void postBluetoothDeviceConfigChange(@NonNull BtDeviceInfo info) { + sendLMsgNoDelay(MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE, SENDMSG_QUEUE, info); } /*package*/ void startBluetoothScoForClient(IBinder cb, int pid, int scoAudioMode, @@ -1322,6 +1336,10 @@ import java.util.concurrent.atomic.AtomicBoolean; sendIMsgNoDelay(MSG_I_SCO_AUDIO_STATE_CHANGED, SENDMSG_QUEUE, state); } + /*package*/ void postNotifyPreferredAudioProfileApplied(BluetoothDevice btDevice) { + sendLMsgNoDelay(MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED, SENDMSG_QUEUE, btDevice); + } + /*package*/ static final class CommunicationDeviceInfo { final @NonNull IBinder mCb; // Identifies the requesting client for death handler final int mPid; // Requester process ID @@ -1397,9 +1415,11 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - /*package*/ boolean handleDeviceConnection(AudioDeviceAttributes attributes, boolean connect) { + /*package*/ boolean handleDeviceConnection(AudioDeviceAttributes attributes, + boolean connect, @Nullable BluetoothDevice btDevice) { synchronized (mDeviceStateLock) { - return mDeviceInventory.handleDeviceConnection(attributes, connect, false /*for test*/); + return mDeviceInventory.handleDeviceConnection( + attributes, connect, false /*for test*/, btDevice); } } @@ -1640,13 +1660,10 @@ import java.util.concurrent.atomic.AtomicBoolean; (String) msg.obj, msg.arg1); } break; - case MSG_L_A2DP_DEVICE_CONFIG_CHANGE: - final BluetoothDevice btDevice = (BluetoothDevice) msg.obj; + case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: synchronized (mDeviceStateLock) { - final int a2dpCodec = mBtHelper.getA2dpCodec(btDevice); - mDeviceInventory.onBluetoothA2dpDeviceConfigChange( - new BtHelper.BluetoothA2dpDeviceInfo(btDevice, -1, a2dpCodec), - BtHelper.EVENT_DEVICE_CONFIG_CHANGE); + mDeviceInventory.onBluetoothDeviceConfigChange( + (BtDeviceInfo) msg.obj, BtHelper.EVENT_DEVICE_CONFIG_CHANGE); } break; case MSG_BROADCAST_AUDIO_BECOMING_NOISY: @@ -1810,6 +1827,10 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_IL_SET_LEAUDIO_SUSPENDED: { setLeAudioSuspended((msg.arg1 == 1), false /*internal*/, (String) msg.obj); } break; + case MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED: { + final BluetoothDevice btDevice = (BluetoothDevice) msg.obj; + BtHelper.onNotifyPreferredAudioProfileApplied(btDevice); + } break; default: Log.wtf(TAG, "Invalid message " + msg.what); } @@ -1845,7 +1866,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_IL_BTA2DP_TIMEOUT = 10; // process change of A2DP device configuration, obj is BluetoothDevice - private static final int MSG_L_A2DP_DEVICE_CONFIG_CHANGE = 11; + private static final int MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE = 11; private static final int MSG_BROADCAST_AUDIO_BECOMING_NOISY = 12; private static final int MSG_REPORT_NEW_ROUTES = 13; @@ -1887,13 +1908,15 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_IL_SET_A2DP_SUSPENDED = 50; private static final int MSG_IL_SET_LEAUDIO_SUSPENDED = 51; + private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52; + private static boolean isMessageHandledUnderWakelock(int msgId) { switch(msgId) { case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE: case MSG_L_SET_BT_ACTIVE_DEVICE: case MSG_IL_BTA2DP_TIMEOUT: case MSG_IL_BTLEAUDIO_TIMEOUT: - case MSG_L_A2DP_DEVICE_CONFIG_CHANGE: + case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: case MSG_TOGGLE_HDMI: case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT: case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT: @@ -1985,7 +2008,7 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE: case MSG_IL_BTA2DP_TIMEOUT: case MSG_IL_BTLEAUDIO_TIMEOUT: - case MSG_L_A2DP_DEVICE_CONFIG_CHANGE: + case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: if (sLastDeviceConnectMsgTime >= time) { // add a little delay to make sure messages are ordered as expected time = sLastDeviceConnectMsgTime + 30; @@ -2005,7 +2028,7 @@ import java.util.concurrent.atomic.AtomicBoolean; static { MESSAGES_MUTE_MUSIC = new HashSet<>(); MESSAGES_MUTE_MUSIC.add(MSG_L_SET_BT_ACTIVE_DEVICE); - MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONFIG_CHANGE); + MESSAGES_MUTE_MUSIC.add(MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE); MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT); MESSAGES_MUTE_MUSIC.add(MSG_IIL_SET_FORCE_BT_A2DP_USE); } @@ -2026,7 +2049,7 @@ import java.util.concurrent.atomic.AtomicBoolean; // Do not mute on bluetooth event if music is playing on a wired headset. if ((message == MSG_L_SET_BT_ACTIVE_DEVICE || message == MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT - || message == MSG_L_A2DP_DEVICE_CONFIG_CHANGE) + || message == MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE) && AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0) && hasIntersection(mDeviceInventory.DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET, mAudioService.getDeviceSetForStream(AudioSystem.STREAM_MUSIC))) { @@ -2173,6 +2196,7 @@ import java.util.concurrent.atomic.AtomicBoolean; mDeviceInventory.removePreferredDevicesForStrategy(mCommunicationStrategyId); mDeviceInventory.removePreferredDevicesForStrategy(mAccessibilityStrategyId); } + mDeviceInventory.applyConnectedDevicesRoles(); } else { mDeviceInventory.setPreferredDevicesForStrategy( mCommunicationStrategyId, Arrays.asList(preferredCommunicationDevice)); diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 94d796bff609..773df3720ed3 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -34,11 +34,15 @@ import android.media.ICapturePresetDevicesRoleDispatcher; import android.media.IStrategyNonDefaultDevicesDispatcher; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.MediaMetrics; +import android.media.MediaRecorder.AudioSource; +import android.media.audiopolicy.AudioProductStrategy; import android.media.permission.ClearCallingIdentityContext; import android.media.permission.SafeCloseable; import android.os.Binder; +import android.os.Bundle; import android.os.RemoteCallbackList; import android.os.RemoteException; +import android.os.SystemProperties; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -50,6 +54,8 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.utils.EventLogger; +import com.google.android.collect.Sets; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; @@ -179,6 +185,8 @@ public class AudioDeviceInventory { final RemoteCallbackList<ICapturePresetDevicesRoleDispatcher> mDevRoleCapturePresetDispatchers = new RemoteCallbackList<ICapturePresetDevicesRoleDispatcher>(); + final List<AudioProductStrategy> mStrategies; + /*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) { this(broker, AudioSystemAdapter.getDefaultAdapter()); } @@ -193,8 +201,10 @@ public class AudioDeviceInventory { @Nullable AudioSystemAdapter audioSystem) { mDeviceBroker = broker; mAudioSystem = audioSystem; + mStrategies = AudioProductStrategy.getAudioProductStrategies(); + mBluetoothDualModeEnabled = SystemProperties.getBoolean( + "persist.bluetooth.enable_dual_mode_audio", false); } - /*package*/ void setDeviceBroker(@NonNull AudioDeviceBroker broker) { mDeviceBroker = broker; } @@ -211,8 +221,13 @@ public class AudioDeviceInventory { int mDeviceCodecFormat; final UUID mSensorUuid; + /** Disabled operating modes for this device. Use a negative logic so that by default + * an empty list means all modes are allowed. + * See BluetoothAdapter.AUDIO_MODE_DUPLEX and BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY */ + @NonNull ArraySet<String> mDisabledModes = new ArraySet(0); + DeviceInfo(int deviceType, String deviceName, String deviceAddress, - int deviceCodecFormat, UUID sensorUuid) { + int deviceCodecFormat, @Nullable UUID sensorUuid) { mDeviceType = deviceType; mDeviceName = deviceName == null ? "" : deviceName; mDeviceAddress = deviceAddress == null ? "" : deviceAddress; @@ -220,11 +235,31 @@ public class AudioDeviceInventory { mSensorUuid = sensorUuid; } + void setModeDisabled(String mode) { + mDisabledModes.add(mode); + } + void setModeEnabled(String mode) { + mDisabledModes.remove(mode); + } + boolean isModeEnabled(String mode) { + return !mDisabledModes.contains(mode); + } + boolean isOutputOnlyModeEnabled() { + return isModeEnabled(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY); + } + boolean isDuplexModeEnabled() { + return isModeEnabled(BluetoothAdapter.AUDIO_MODE_DUPLEX); + } + DeviceInfo(int deviceType, String deviceName, String deviceAddress, int deviceCodecFormat) { this(deviceType, deviceName, deviceAddress, deviceCodecFormat, null); } + DeviceInfo(int deviceType, String deviceName, String deviceAddress) { + this(deviceType, deviceName, deviceAddress, AudioSystem.AUDIO_FORMAT_DEFAULT); + } + @Override public String toString() { return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType) @@ -232,7 +267,8 @@ public class AudioDeviceInventory { + ") name:" + mDeviceName + " addr:" + mDeviceAddress + " codec: " + Integer.toHexString(mDeviceCodecFormat) - + " sensorUuid: " + Objects.toString(mSensorUuid) + "]"; + + " sensorUuid: " + Objects.toString(mSensorUuid) + + " disabled modes: " + mDisabledModes + "]"; } @NonNull String getKey() { @@ -317,6 +353,7 @@ public class AudioDeviceInventory { di.mDeviceCodecFormat); } mAppliedStrategyRoles.clear(); + applyConnectedDevicesRoles_l(); } synchronized (mPreferredDevices) { mPreferredDevices.forEach((strategy, devices) -> { @@ -397,8 +434,7 @@ public class AudioDeviceInventory { btInfo.mVolume * 10, btInfo.mAudioSystemDevice, "onSetBtActiveDevice"); } - makeA2dpDeviceAvailable(address, BtHelper.getName(btInfo.mDevice), - "onSetBtActiveDevice", btInfo.mCodec); + makeA2dpDeviceAvailable(btInfo, "onSetBtActiveDevice"); } break; case BluetoothProfile.HEARING_AID: @@ -414,10 +450,7 @@ public class AudioDeviceInventory { if (switchToUnavailable) { makeLeAudioDeviceUnavailableNow(address, btInfo.mAudioSystemDevice); } else if (switchToAvailable) { - makeLeAudioDeviceAvailable(address, BtHelper.getName(btInfo.mDevice), - streamType, btInfo.mVolume == -1 ? -1 : btInfo.mVolume * 10, - btInfo.mAudioSystemDevice, - "onSetBtActiveDevice"); + makeLeAudioDeviceAvailable(btInfo, streamType, "onSetBtActiveDevice"); } break; default: throw new IllegalArgumentException("Invalid profile " @@ -428,30 +461,30 @@ public class AudioDeviceInventory { @GuardedBy("AudioDeviceBroker.mDeviceStateLock") - /*package*/ void onBluetoothA2dpDeviceConfigChange( - @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int event) { + /*package*/ void onBluetoothDeviceConfigChange( + @NonNull AudioDeviceBroker.BtDeviceInfo btInfo, int event) { MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId - + "onBluetoothA2dpDeviceConfigChange") - .set(MediaMetrics.Property.EVENT, BtHelper.a2dpDeviceEventToString(event)); + + "onBluetoothDeviceConfigChange") + .set(MediaMetrics.Property.EVENT, BtHelper.deviceEventToString(event)); - final BluetoothDevice btDevice = btInfo.getBtDevice(); + final BluetoothDevice btDevice = btInfo.mDevice; if (btDevice == null) { mmi.set(MediaMetrics.Property.EARLY_RETURN, "btDevice null").record(); return; } if (AudioService.DEBUG_DEVICES) { - Log.d(TAG, "onBluetoothA2dpDeviceConfigChange btDevice=" + btDevice); + Log.d(TAG, "onBluetoothDeviceConfigChange btDevice=" + btDevice); } - int a2dpVolume = btInfo.getVolume(); - @AudioSystem.AudioFormatNativeEnumForBtCodec final int a2dpCodec = btInfo.getCodec(); + int volume = btInfo.mVolume; + @AudioSystem.AudioFormatNativeEnumForBtCodec final int audioCodec = btInfo.mCodec; String address = btDevice.getAddress(); if (!BluetoothAdapter.checkBluetoothAddress(address)) { address = ""; } AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "onBluetoothA2dpDeviceConfigChange addr=" + address - + " event=" + BtHelper.a2dpDeviceEventToString(event))); + "onBluetoothDeviceConfigChange addr=" + address + + " event=" + BtHelper.deviceEventToString(event))); synchronized (mDevicesLock) { if (mDeviceBroker.hasScheduledA2dpConnection(btDevice)) { @@ -466,53 +499,53 @@ public class AudioDeviceInventory { AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); final DeviceInfo di = mConnectedDevices.get(key); if (di == null) { - Log.e(TAG, "invalid null DeviceInfo in onBluetoothA2dpDeviceConfigChange"); + Log.e(TAG, "invalid null DeviceInfo in onBluetoothDeviceConfigChange"); mmi.set(MediaMetrics.Property.EARLY_RETURN, "null DeviceInfo").record(); return; } mmi.set(MediaMetrics.Property.ADDRESS, address) .set(MediaMetrics.Property.ENCODING, - AudioSystem.audioFormatToString(a2dpCodec)) - .set(MediaMetrics.Property.INDEX, a2dpVolume) + AudioSystem.audioFormatToString(audioCodec)) + .set(MediaMetrics.Property.INDEX, volume) .set(MediaMetrics.Property.NAME, di.mDeviceName); - if (event == BtHelper.EVENT_ACTIVE_DEVICE_CHANGE) { - // Device is connected - if (a2dpVolume != -1) { - mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC, - // convert index to internal representation in VolumeStreamState - a2dpVolume * 10, - AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, - "onBluetoothA2dpDeviceConfigChange"); - } - } else if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) { - if (di.mDeviceCodecFormat != a2dpCodec) { - di.mDeviceCodecFormat = a2dpCodec; - mConnectedDevices.replace(key, di); - } - } - final int res = mAudioSystem.handleDeviceConfigChange( - AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, - BtHelper.getName(btDevice), a2dpCodec); - if (res != AudioSystem.AUDIO_STATUS_OK) { - AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "APM handleDeviceConfigChange failed for A2DP device addr=" + address - + " codec=" + AudioSystem.audioFormatToString(a2dpCodec)) - .printLog(TAG)); + if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) { + boolean a2dpCodecChange = false; + if (btInfo.mProfile == BluetoothProfile.A2DP) { + if (di.mDeviceCodecFormat != audioCodec) { + di.mDeviceCodecFormat = audioCodec; + mConnectedDevices.replace(key, di); + a2dpCodecChange = true; + } + final int res = mAudioSystem.handleDeviceConfigChange( + btInfo.mAudioSystemDevice, address, + BtHelper.getName(btDevice), audioCodec); + + if (res != AudioSystem.AUDIO_STATUS_OK) { + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( + "APM handleDeviceConfigChange failed for A2DP device addr=" + + address + " codec=" + + AudioSystem.audioFormatToString(audioCodec)) + .printLog(TAG)); + + // force A2DP device disconnection in case of error so that AudioService + // state is consistent with audio policy manager state + setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btInfo, + BluetoothProfile.STATE_DISCONNECTED)); + } else { + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( + "APM handleDeviceConfigChange success for A2DP device addr=" + + address + + " codec=" + AudioSystem.audioFormatToString(audioCodec)) + .printLog(TAG)); - int musicDevice = mDeviceBroker.getDeviceForStream(AudioSystem.STREAM_MUSIC); - // force A2DP device disconnection in case of error so that AudioService state is - // consistent with audio policy manager state - setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btDevice, - BluetoothProfile.A2DP, BluetoothProfile.STATE_DISCONNECTED, - musicDevice, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP)); - } else { - AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "APM handleDeviceConfigChange success for A2DP device addr=" + address - + " codec=" + AudioSystem.audioFormatToString(a2dpCodec)) - .printLog(TAG)); + } + } + if (!a2dpCodecChange) { + updateBluetoothPreferredModes_l(btDevice /*connectedDevice*/); + } } } mmi.record(); @@ -595,7 +628,7 @@ public class AudioDeviceInventory { } if (!handleDeviceConnection(wdcs.mAttributes, - wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest)) { + wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest, null)) { // change of connection state failed, bailout mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed") .record(); @@ -1134,10 +1167,11 @@ public class AudioDeviceInventory { * @param connect true if connection * @param isForTesting if true, not calling AudioSystem for the connection as this is * just for testing + * @param btDevice the corresponding Bluetooth device when relevant. * @return false if an error was reported by AudioSystem */ /*package*/ boolean handleDeviceConnection(AudioDeviceAttributes attributes, boolean connect, - boolean isForTesting) { + boolean isForTesting, @Nullable BluetoothDevice btDevice) { int device = attributes.getInternalType(); String address = attributes.getAddress(); String deviceName = attributes.getName(); @@ -1152,6 +1186,7 @@ public class AudioDeviceInventory { .set(MediaMetrics.Property.MODE, connect ? MediaMetrics.Value.CONNECT : MediaMetrics.Value.DISCONNECT) .set(MediaMetrics.Property.NAME, deviceName); + boolean status = false; synchronized (mDevicesLock) { final String deviceKey = DeviceInfo.makeDeviceListKey(device, address); if (AudioService.DEBUG_DEVICES) { @@ -1179,25 +1214,31 @@ public class AudioDeviceInventory { .record(); return false; } - mConnectedDevices.put(deviceKey, new DeviceInfo( - device, deviceName, address, AudioSystem.AUDIO_FORMAT_DEFAULT)); + mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, address)); mDeviceBroker.postAccessoryPlugMediaUnmute(device); - mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record(); - return true; + status = true; } else if (!connect && isConnected) { mAudioSystem.setDeviceConnectionState(attributes, AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); // always remove even if disconnection failed mConnectedDevices.remove(deviceKey); - purgeDevicesRoles_l(); + status = true; + } + if (status) { + if (AudioSystem.isBluetoothScoDevice(device)) { + updateBluetoothPreferredModes_l(connect ? btDevice : null /*connectedDevice*/); + if (!connect) { + purgeDevicesRoles_l(); + } + } mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record(); - return true; + } else { + Log.w(TAG, "handleDeviceConnection() failed, deviceKey=" + deviceKey + + ", deviceSpec=" + di + ", connect=" + connect); + mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.DISCONNECTED).record(); } - Log.w(TAG, "handleDeviceConnection() failed, deviceKey=" + deviceKey - + ", deviceSpec=" + di + ", connect=" + connect); } - mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.DISCONNECTED).record(); - return false; + return status; } @@ -1406,15 +1447,20 @@ public class AudioDeviceInventory { // Internal utilities @GuardedBy("mDevicesLock") - private void makeA2dpDeviceAvailable(String address, String name, String eventSource, - int a2dpCodec) { + private void makeA2dpDeviceAvailable(AudioDeviceBroker.BtDeviceInfo btInfo, + String eventSource) { + final String address = btInfo.mDevice.getAddress(); + final String name = BtHelper.getName(btInfo.mDevice); + final int a2dpCodec = btInfo.mCodec; + // enable A2DP before notifying A2DP connection to avoid unnecessary processing in // audio policy manager mDeviceBroker.setBluetoothA2dpOnInt(true, true /*fromA2dp*/, eventSource); // at this point there could be another A2DP device already connected in APM, but it // doesn't matter as this new one will overwrite the previous one - final int res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( - AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name), + AudioDeviceAttributes ada = new AudioDeviceAttributes( + AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name); + final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, a2dpCodec); // TODO: log in MediaMetrics once distinction between connection failure and @@ -1436,8 +1482,7 @@ public class AudioDeviceInventory { // The convention for head tracking sensors associated with A2DP devices is to // use a UUID derived from the MAC address as follows: // time_low = 0, time_mid = 0, time_hi = 0, clock_seq = 0, node = MAC Address - UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes( - new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address)); + UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada); final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name, address, a2dpCodec, sensorUuid); final String diKey = di.getKey(); @@ -1448,6 +1493,208 @@ public class AudioDeviceInventory { mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP); setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/); + + updateBluetoothPreferredModes_l(btInfo.mDevice /*connectedDevice*/); + } + + static final int[] CAPTURE_PRESETS = new int[] {AudioSource.MIC, AudioSource.CAMCORDER, + AudioSource.VOICE_RECOGNITION, AudioSource.VOICE_COMMUNICATION, + AudioSource.UNPROCESSED, AudioSource.VOICE_PERFORMANCE, AudioSource.HOTWORD}; + + // reflects system property persist.bluetooth.enable_dual_mode_audio + final boolean mBluetoothDualModeEnabled; + /** + * Goes over all connected Bluetooth devices and set the audio policy device role to DISABLED + * or not according to their own and other devices modes. + * The top priority is given to LE devices, then SCO ,then A2DP. + */ + @GuardedBy("mDevicesLock") + private void applyConnectedDevicesRoles_l() { + if (!mBluetoothDualModeEnabled) { + return; + } + DeviceInfo leOutDevice = + getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_BLE_SET); + DeviceInfo leInDevice = + getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_BLE_SET); + DeviceInfo a2dpDevice = + getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_A2DP_SET); + DeviceInfo scoOutDevice = + getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_SCO_SET); + DeviceInfo scoInDevice = + getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_SCO_SET); + boolean disableA2dp = (leOutDevice != null && leOutDevice.isOutputOnlyModeEnabled()); + boolean disableSco = (leOutDevice != null && leOutDevice.isDuplexModeEnabled()) + || (leInDevice != null && leInDevice.isDuplexModeEnabled()); + AudioDeviceAttributes communicationDevice = + mDeviceBroker.mActiveCommunicationDevice == null + ? null : ((mDeviceBroker.isInCommunication() + && mDeviceBroker.mActiveCommunicationDevice != null) + ? new AudioDeviceAttributes(mDeviceBroker.mActiveCommunicationDevice) + : null); + + if (AudioService.DEBUG_DEVICES) { + Log.i(TAG, "applyConnectedDevicesRoles_l\n - leOutDevice: " + leOutDevice + + "\n - leInDevice: " + leInDevice + + "\n - a2dpDevice: " + a2dpDevice + + "\n - scoOutDevice: " + scoOutDevice + + "\n - scoInDevice: " + scoInDevice + + "\n - disableA2dp: " + disableA2dp + + ", disableSco: " + disableSco); + } + + for (DeviceInfo di : mConnectedDevices.values()) { + if (!AudioSystem.isBluetoothDevice(di.mDeviceType)) { + continue; + } + AudioDeviceAttributes ada = + new AudioDeviceAttributes(di.mDeviceType, di.mDeviceAddress, di.mDeviceName); + if (AudioService.DEBUG_DEVICES) { + Log.i(TAG, " + checking Device: " + ada); + } + if (ada.equalTypeAddress(communicationDevice)) { + continue; + } + + if (AudioSystem.isBluetoothOutDevice(di.mDeviceType)) { + for (AudioProductStrategy strategy : mStrategies) { + boolean disable = false; + if (strategy.getId() == mDeviceBroker.mCommunicationStrategyId) { + if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) { + disable = disableSco || !di.isDuplexModeEnabled(); + } else if (AudioSystem.isBluetoothLeDevice(di.mDeviceType)) { + disable = !di.isDuplexModeEnabled(); + } + } else { + if (AudioSystem.isBluetoothA2dpOutDevice(di.mDeviceType)) { + disable = disableA2dp || !di.isOutputOnlyModeEnabled(); + } else if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) { + disable = disableSco || !di.isOutputOnlyModeEnabled(); + } else if (AudioSystem.isBluetoothLeDevice(di.mDeviceType)) { + disable = !di.isOutputOnlyModeEnabled(); + } + } + if (AudioService.DEBUG_DEVICES) { + Log.i(TAG, " - strategy: " + strategy.getId() + + ", disable: " + disable); + } + if (disable) { + addDevicesRoleForStrategy(strategy.getId(), + AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada)); + } else { + removeDevicesRoleForStrategy(strategy.getId(), + AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada)); + } + } + } + if (AudioSystem.isBluetoothInDevice(di.mDeviceType)) { + for (int capturePreset : CAPTURE_PRESETS) { + boolean disable = false; + if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) { + disable = disableSco || !di.isDuplexModeEnabled(); + } else if (AudioSystem.isBluetoothLeDevice(di.mDeviceType)) { + disable = !di.isDuplexModeEnabled(); + } + if (AudioService.DEBUG_DEVICES) { + Log.i(TAG, " - capturePreset: " + capturePreset + + ", disable: " + disable); + } + if (disable) { + addDevicesRoleForCapturePreset(capturePreset, + AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada)); + } else { + removeDevicesRoleForCapturePreset(capturePreset, + AudioSystem.DEVICE_ROLE_DISABLED, Arrays.asList(ada)); + } + } + } + } + } + + /* package */ void applyConnectedDevicesRoles() { + synchronized (mDevicesLock) { + applyConnectedDevicesRoles_l(); + } + } + + @GuardedBy("mDevicesLock") + int checkProfileIsConnected(int profile) { + switch (profile) { + case BluetoothProfile.HEADSET: + if (getFirstConnectedDeviceOfTypes( + AudioSystem.DEVICE_OUT_ALL_SCO_SET) != null + || getFirstConnectedDeviceOfTypes( + AudioSystem.DEVICE_IN_ALL_SCO_SET) != null) { + return profile; + } + break; + case BluetoothProfile.A2DP: + if (getFirstConnectedDeviceOfTypes( + AudioSystem.DEVICE_OUT_ALL_A2DP_SET) != null) { + return profile; + } + break; + case BluetoothProfile.LE_AUDIO: + case BluetoothProfile.LE_AUDIO_BROADCAST: + if (getFirstConnectedDeviceOfTypes( + AudioSystem.DEVICE_OUT_ALL_BLE_SET) != null + || getFirstConnectedDeviceOfTypes( + AudioSystem.DEVICE_IN_ALL_BLE_SET) != null) { + return profile; + } + break; + default: + break; + } + return 0; + } + + @GuardedBy("mDevicesLock") + private void updateBluetoothPreferredModes_l(BluetoothDevice connectedDevice) { + if (!mBluetoothDualModeEnabled) { + return; + } + HashSet<String> processedAddresses = new HashSet<>(0); + for (DeviceInfo di : mConnectedDevices.values()) { + if (!AudioSystem.isBluetoothDevice(di.mDeviceType) + || processedAddresses.contains(di.mDeviceAddress)) { + continue; + } + Bundle preferredProfiles = BtHelper.getPreferredAudioProfiles(di.mDeviceAddress); + if (AudioService.DEBUG_DEVICES) { + Log.i(TAG, "updateBluetoothPreferredModes_l processing device address: " + + di.mDeviceAddress + ", preferredProfiles: " + preferredProfiles); + } + for (DeviceInfo di2 : mConnectedDevices.values()) { + if (!AudioSystem.isBluetoothDevice(di2.mDeviceType) + || !di.mDeviceAddress.equals(di2.mDeviceAddress)) { + continue; + } + int profile = BtHelper.getProfileFromType(di2.mDeviceType); + if (profile == 0) { + continue; + } + int preferredProfile = checkProfileIsConnected( + preferredProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)); + if (preferredProfile == profile || preferredProfile == 0) { + di2.setModeEnabled(BluetoothAdapter.AUDIO_MODE_DUPLEX); + } else { + di2.setModeDisabled(BluetoothAdapter.AUDIO_MODE_DUPLEX); + } + preferredProfile = checkProfileIsConnected( + preferredProfiles.getInt(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY)); + if (preferredProfile == profile || preferredProfile == 0) { + di2.setModeEnabled(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY); + } else { + di2.setModeDisabled(BluetoothAdapter.AUDIO_MODE_OUTPUT_ONLY); + } + } + processedAddresses.add(di.mDeviceAddress); + } + applyConnectedDevicesRoles_l(); + if (connectedDevice != null) { + mDeviceBroker.postNotifyPreferredAudioProfileApplied(connectedDevice); + } } @GuardedBy("mDevicesLock") @@ -1495,6 +1742,7 @@ public class AudioDeviceInventory { // Remove A2DP routes as well setCurrentAudioRouteNameIfPossible(null, true /*fromA2dp*/); mmi.record(); + updateBluetoothPreferredModes_l(null /*connectedDevice*/); purgeDevicesRoles_l(); } @@ -1525,8 +1773,7 @@ public class AudioDeviceInventory { AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.put( DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address), - new DeviceInfo(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, "", - address, AudioSystem.AUDIO_FORMAT_DEFAULT)); + new DeviceInfo(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, "", address)); } @GuardedBy("mDevicesLock") @@ -1552,8 +1799,7 @@ public class AudioDeviceInventory { AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.put( DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address), - new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name, - address, AudioSystem.AUDIO_FORMAT_DEFAULT)); + new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name, address)); mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_HEARING_AID); mDeviceBroker.postApplyVolumeOnDevice(streamType, AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable"); @@ -1591,29 +1837,56 @@ public class AudioDeviceInventory { * @return true if a DEVICE_OUT_HEARING_AID is connected, false otherwise. */ boolean isHearingAidConnected() { + return getFirstConnectedDeviceOfTypes( + Sets.newHashSet(AudioSystem.DEVICE_OUT_HEARING_AID)) != null; + } + + /** + * Returns a DeviceInfo for the first connected device matching one of the supplied types + */ + private DeviceInfo getFirstConnectedDeviceOfTypes(Set<Integer> internalTypes) { + List<DeviceInfo> devices = getConnectedDevicesOfTypes(internalTypes); + return devices.isEmpty() ? null : devices.get(0); + } + + /** + * Returns a list of connected devices matching one of the supplied types + */ + private List<DeviceInfo> getConnectedDevicesOfTypes(Set<Integer> internalTypes) { + ArrayList<DeviceInfo> devices = new ArrayList<>(); synchronized (mDevicesLock) { for (DeviceInfo di : mConnectedDevices.values()) { - if (di.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) { - return true; + if (internalTypes.contains(di.mDeviceType)) { + devices.add(di); } } - return false; } + return devices; + } + + /* package */ AudioDeviceAttributes getDeviceOfType(int type) { + DeviceInfo di = getFirstConnectedDeviceOfTypes(Sets.newHashSet(type)); + return di == null ? null : new AudioDeviceAttributes( + di.mDeviceType, di.mDeviceAddress, di.mDeviceName); } @GuardedBy("mDevicesLock") - private void makeLeAudioDeviceAvailable(String address, String name, int streamType, - int volumeIndex, int device, String eventSource) { + private void makeLeAudioDeviceAvailable( + AudioDeviceBroker.BtDeviceInfo btInfo, int streamType, String eventSource) { + final String address = btInfo.mDevice.getAddress(); + final String name = BtHelper.getName(btInfo.mDevice); + final int volumeIndex = btInfo.mVolume == -1 ? -1 : btInfo.mVolume * 10; + final int device = btInfo.mAudioSystemDevice; + if (device != AudioSystem.DEVICE_NONE) { /* Audio Policy sees Le Audio similar to A2DP. Let's make sure * AUDIO_POLICY_FORCE_NO_BT_A2DP is not set */ mDeviceBroker.setBluetoothA2dpOnInt(true, false /*fromA2dp*/, eventSource); - final int res = AudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( - device, address, name), - AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioDeviceAttributes ada = new AudioDeviceAttributes(device, address, name); + final int res = AudioSystem.setDeviceConnectionState(ada, + AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM failed to make available LE Audio device addr=" + address @@ -1624,12 +1897,13 @@ public class AudioDeviceInventory { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "LE Audio device addr=" + address + " now available").printLog(TAG)); } - // Reset LEA suspend state each time a new sink is connected mDeviceBroker.clearLeAudioSuspended(); + UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada); mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address), - new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT)); + new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT, + sensorUuid)); mDeviceBroker.postAccessoryPlugMediaUnmute(device); setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false); } @@ -1645,6 +1919,8 @@ public class AudioDeviceInventory { final int maxIndex = mDeviceBroker.getMaxVssVolumeForStream(streamType); mDeviceBroker.postSetLeAudioVolumeIndex(leAudioVolIndex, maxIndex, streamType); mDeviceBroker.postApplyVolumeOnDevice(streamType, device, "makeLeAudioDeviceAvailable"); + + updateBluetoothPreferredModes_l(btInfo.mDevice /*connectedDevice*/); } @GuardedBy("mDevicesLock") @@ -1669,6 +1945,7 @@ public class AudioDeviceInventory { } setCurrentAudioRouteNameIfPossible(null, false /*fromA2dp*/); + updateBluetoothPreferredModes_l(null /*connectedDevice*/); purgeDevicesRoles_l(); } @@ -2005,18 +2282,6 @@ public class AudioDeviceInventory { } } - /* package */ AudioDeviceAttributes getDeviceOfType(int type) { - synchronized (mDevicesLock) { - for (DeviceInfo di : mConnectedDevices.values()) { - if (di.mDeviceType == type) { - return new AudioDeviceAttributes( - di.mDeviceType, di.mDeviceAddress, di.mDeviceName); - } - } - } - return null; - } - //---------------------------------------------------------- // For tests only @@ -2027,10 +2292,12 @@ public class AudioDeviceInventory { */ @VisibleForTesting public boolean isA2dpDeviceConnected(@NonNull BluetoothDevice device) { - final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, - device.getAddress()); - synchronized (mDevicesLock) { - return (mConnectedDevices.get(key) != null); + for (DeviceInfo di : getConnectedDevicesOfTypes( + Sets.newHashSet(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP))) { + if (di.mDeviceAddress.equals(device.getAddress())) { + return true; + } } + return false; } } diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 8c27c3ecfd87..e46c3cc4a285 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -33,6 +33,7 @@ import android.media.AudioManager; import android.media.AudioSystem; import android.media.BluetoothProfileConnectionInfo; import android.os.Binder; +import android.os.Bundle; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; @@ -150,60 +151,12 @@ public class BtHelper { } } - //---------------------------------------------------------------------- - /*package*/ static class BluetoothA2dpDeviceInfo { - private final @NonNull BluetoothDevice mBtDevice; - private final int mVolume; - private final @AudioSystem.AudioFormatNativeEnumForBtCodec int mCodec; - - BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice) { - this(btDevice, -1, AudioSystem.AUDIO_FORMAT_DEFAULT); - } - - BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice, int volume, int codec) { - mBtDevice = btDevice; - mVolume = volume; - mCodec = codec; - } - - public @NonNull BluetoothDevice getBtDevice() { - return mBtDevice; - } - - public int getVolume() { - return mVolume; - } - - public @AudioSystem.AudioFormatNativeEnumForBtCodec int getCodec() { - return mCodec; - } - - // redefine equality op so we can match messages intended for this device - @Override - public boolean equals(Object o) { - if (o == null) { - return false; - } - if (this == o) { - return true; - } - if (o instanceof BluetoothA2dpDeviceInfo) { - return mBtDevice.equals(((BluetoothA2dpDeviceInfo) o).getBtDevice()); - } - return false; - } - - - } - // A2DP device events /*package*/ static final int EVENT_DEVICE_CONFIG_CHANGE = 0; - /*package*/ static final int EVENT_ACTIVE_DEVICE_CHANGE = 1; - /*package*/ static String a2dpDeviceEventToString(int event) { + /*package*/ static String deviceEventToString(int event) { switch (event) { case EVENT_DEVICE_CONFIG_CHANGE: return "DEVICE_CONFIG_CHANGE"; - case EVENT_ACTIVE_DEVICE_CHANGE: return "ACTIVE_DEVICE_CHANGE"; default: return new String("invalid event:" + event); } @@ -620,11 +573,12 @@ public class BtHelper { return btHeadsetDeviceToAudioDevice(mBluetoothHeadsetDevice); } - private AudioDeviceAttributes btHeadsetDeviceToAudioDevice(BluetoothDevice btDevice) { + private static AudioDeviceAttributes btHeadsetDeviceToAudioDevice(BluetoothDevice btDevice) { if (btDevice == null) { return new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, ""); } String address = btDevice.getAddress(); + String name = getName(btDevice); if (!BluetoothAdapter.checkBluetoothAddress(address)) { address = ""; } @@ -646,7 +600,7 @@ public class BtHelper { + " btClass: " + (btClass == null ? "Unknown" : btClass) + " nativeType: " + nativeType + " address: " + address); } - return new AudioDeviceAttributes(nativeType, address); + return new AudioDeviceAttributes(nativeType, address, name); } private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) { @@ -655,12 +609,9 @@ public class BtHelper { } int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET; AudioDeviceAttributes audioDevice = btHeadsetDeviceToAudioDevice(btDevice); - String btDeviceName = getName(btDevice); boolean result = false; if (isActive) { - result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( - audioDevice.getInternalType(), audioDevice.getAddress(), btDeviceName), - isActive); + result |= mDeviceBroker.handleDeviceConnection(audioDevice, isActive, btDevice); } else { int[] outDeviceTypes = { AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, @@ -669,14 +620,14 @@ public class BtHelper { }; for (int outDeviceType : outDeviceTypes) { result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( - outDeviceType, audioDevice.getAddress(), btDeviceName), - isActive); + outDeviceType, audioDevice.getAddress(), audioDevice.getName()), + isActive, btDevice); } } // handleDeviceConnection() && result to make sure the method get executed result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( - inDevice, audioDevice.getAddress(), btDeviceName), - isActive) && result; + inDevice, audioDevice.getAddress(), audioDevice.getName()), + isActive, btDevice) && result; return result; } @@ -973,6 +924,30 @@ public class BtHelper { } } + /*package */ static int getProfileFromType(int deviceType) { + if (AudioSystem.isBluetoothA2dpOutDevice(deviceType)) { + return BluetoothProfile.A2DP; + } else if (AudioSystem.isBluetoothScoDevice(deviceType)) { + return BluetoothProfile.HEADSET; + } else if (AudioSystem.isBluetoothLeDevice(deviceType)) { + return BluetoothProfile.LE_AUDIO; + } + return 0; // 0 is not a valid profile + } + + /*package */ static Bundle getPreferredAudioProfiles(String address) { + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + return adapter.getPreferredAudioProfiles(adapter.getRemoteDevice(address)); + } + + /** + * Notifies Bluetooth framework that new preferred audio profiles for Bluetooth devices + * have been applied. + */ + public static void onNotifyPreferredAudioProfileApplied(BluetoothDevice btDevice) { + BluetoothAdapter.getDefaultAdapter().notifyActiveDeviceChangeApplied(btDevice); + } + /** * Returns the string equivalent for the btDeviceClass class. */ diff --git a/services/core/java/com/android/server/pm/UserJourneyLogger.java b/services/core/java/com/android/server/pm/UserJourneyLogger.java new file mode 100644 index 000000000000..f48a1669c259 --- /dev/null +++ b/services/core/java/com/android/server/pm/UserJourneyLogger.java @@ -0,0 +1,552 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static android.os.UserManager.USER_TYPE_FULL_DEMO; +import static android.os.UserManager.USER_TYPE_FULL_GUEST; +import static android.os.UserManager.USER_TYPE_FULL_RESTRICTED; +import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; +import static android.os.UserManager.USER_TYPE_FULL_SYSTEM; +import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; +import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; +import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS; + +import static com.android.internal.util.FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNKNOWN; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.pm.UserInfo; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * This class is logging User Lifecycle statsd events and synchronise User Lifecycle Journeys + * by making sure all events are called in correct order and errors are reported in case of + * unexpected journeys. This class also makes sure that all user sub-journeys are logged so + * for example User Switch also log User Start Journey. + */ +public class UserJourneyLogger { + + public static final int ERROR_CODE_INVALID_SESSION_ID = 0; + public static final int ERROR_CODE_UNSPECIFIED = -1; + /* + * Possible reasons for ERROR_CODE_INCOMPLETE_OR_TIMEOUT to occur: + * - A user switch journey is received while another user switch journey is in + * process for the same user. + * - A user switch journey is received while user start journey is in process for + * the same user. + * - A user start journey is received while another user start journey is in process + * for the same user. + * In all cases potentially an incomplete, timed-out session or multiple + * simultaneous requests. It is not possible to keep track of multiple sessions for + * the same user, so previous session is abandoned. + */ + public static final int ERROR_CODE_INCOMPLETE_OR_TIMEOUT = 2; + public static final int ERROR_CODE_ABORTED = 3; + public static final int ERROR_CODE_NULL_USER_INFO = 4; + public static final int ERROR_CODE_USER_ALREADY_AN_ADMIN = 5; + public static final int ERROR_CODE_USER_IS_NOT_AN_ADMIN = 6; + + @IntDef(prefix = {"ERROR_CODE"}, value = { + ERROR_CODE_UNSPECIFIED, + ERROR_CODE_INCOMPLETE_OR_TIMEOUT, + ERROR_CODE_ABORTED, + ERROR_CODE_NULL_USER_INFO, + ERROR_CODE_USER_ALREADY_AN_ADMIN, + ERROR_CODE_USER_IS_NOT_AN_ADMIN, + ERROR_CODE_INVALID_SESSION_ID + }) + public @interface UserJourneyErrorCode { + } + + // The various user journeys, defined in the UserLifecycleJourneyReported atom for statsd + public static final int USER_JOURNEY_UNKNOWN = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__UNKNOWN; + public static final int USER_JOURNEY_USER_SWITCH_FG = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_SWITCH_FG; + public static final int USER_JOURNEY_USER_SWITCH_UI = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_SWITCH_UI; + public static final int USER_JOURNEY_USER_START = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_START; + public static final int USER_JOURNEY_USER_CREATE = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE; + public static final int USER_JOURNEY_USER_STOP = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_STOP; + public static final int USER_JOURNEY_USER_REMOVE = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_REMOVE; + public static final int USER_JOURNEY_GRANT_ADMIN = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN; + public static final int USER_JOURNEY_REVOKE_ADMIN = + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN; + + @IntDef(prefix = {"USER_JOURNEY"}, value = { + USER_JOURNEY_UNKNOWN, + USER_JOURNEY_USER_SWITCH_FG, + USER_JOURNEY_USER_SWITCH_UI, + USER_JOURNEY_USER_START, + USER_JOURNEY_USER_STOP, + USER_JOURNEY_USER_CREATE, + USER_JOURNEY_USER_REMOVE, + USER_JOURNEY_GRANT_ADMIN, + USER_JOURNEY_REVOKE_ADMIN + }) + public @interface UserJourney { + } + + + // The various user lifecycle events, defined in the UserLifecycleEventOccurred atom for statsd + public static final int USER_LIFECYCLE_EVENT_UNKNOWN = + USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNKNOWN; + public static final int USER_LIFECYCLE_EVENT_SWITCH_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__SWITCH_USER; + public static final int USER_LIFECYCLE_EVENT_START_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__START_USER; + public static final int USER_LIFECYCLE_EVENT_CREATE_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__CREATE_USER; + public static final int USER_LIFECYCLE_EVENT_REMOVE_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REMOVE_USER; + public static final int USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__USER_RUNNING_LOCKED; + public static final int USER_LIFECYCLE_EVENT_UNLOCKING_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNLOCKING_USER; + public static final int USER_LIFECYCLE_EVENT_UNLOCKED_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNLOCKED_USER; + public static final int USER_LIFECYCLE_EVENT_STOP_USER = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__STOP_USER; + public static final int USER_LIFECYCLE_EVENT_GRANT_ADMIN = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__GRANT_ADMIN; + public static final int USER_LIFECYCLE_EVENT_REVOKE_ADMIN = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REVOKE_ADMIN; + + @IntDef(prefix = {"USER_LIFECYCLE_EVENT"}, value = { + USER_LIFECYCLE_EVENT_UNKNOWN, + USER_LIFECYCLE_EVENT_SWITCH_USER, + USER_LIFECYCLE_EVENT_START_USER, + USER_LIFECYCLE_EVENT_CREATE_USER, + USER_LIFECYCLE_EVENT_REMOVE_USER, + USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, + USER_LIFECYCLE_EVENT_UNLOCKING_USER, + USER_LIFECYCLE_EVENT_UNLOCKED_USER, + USER_LIFECYCLE_EVENT_STOP_USER, + USER_LIFECYCLE_EVENT_GRANT_ADMIN, + USER_LIFECYCLE_EVENT_REVOKE_ADMIN + }) + public @interface UserLifecycleEvent { + } + + // User lifecycle event state, defined in the UserLifecycleEventOccurred atom for statsd + public static final int EVENT_STATE_BEGIN = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__BEGIN; + public static final int EVENT_STATE_FINISH = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__FINISH; + public static final int EVENT_STATE_NONE = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__NONE; + public static final int EVENT_STATE_CANCEL = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__CANCEL; + public static final int EVENT_STATE_ERROR = + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__ERROR; + + @IntDef(prefix = {"EVENT_STATE"}, value = { + EVENT_STATE_BEGIN, + EVENT_STATE_FINISH, + EVENT_STATE_NONE, + EVENT_STATE_CANCEL, + EVENT_STATE_ERROR, + }) + public @interface UserLifecycleEventState { + } + + private static final int USER_ID_KEY_MULTIPLICATION = 100; + + private final Object mLock = new Object(); + + /** + * {@link UserIdInt} and {@link UserJourney} to {@link UserJourneySession} mapping used for + * statsd logging for the UserLifecycleJourneyReported and UserLifecycleEventOccurred atoms. + */ + @GuardedBy("mLock") + private final SparseArray<UserJourneySession> mUserIdToUserJourneyMap = new SparseArray<>(); + + /** + * Returns event equivalent of given journey + */ + @UserLifecycleEvent + private static int journeyToEvent(@UserJourney int journey) { + switch (journey) { + case USER_JOURNEY_USER_SWITCH_UI: + case USER_JOURNEY_USER_SWITCH_FG: + return USER_LIFECYCLE_EVENT_SWITCH_USER; + case USER_JOURNEY_USER_START: + return USER_LIFECYCLE_EVENT_START_USER; + case USER_JOURNEY_USER_CREATE: + return USER_LIFECYCLE_EVENT_CREATE_USER; + case USER_JOURNEY_USER_STOP: + return USER_LIFECYCLE_EVENT_STOP_USER; + case USER_JOURNEY_USER_REMOVE: + return USER_LIFECYCLE_EVENT_REMOVE_USER; + case USER_JOURNEY_GRANT_ADMIN: + return USER_LIFECYCLE_EVENT_GRANT_ADMIN; + case USER_JOURNEY_REVOKE_ADMIN: + return USER_LIFECYCLE_EVENT_REVOKE_ADMIN; + default: + return USER_LIFECYCLE_EVENT_UNKNOWN; + } + } + + /** + * Returns the enum defined in the statsd UserLifecycleJourneyReported atom corresponding to + * the user type. + * Changes to this method require changes in CTS file + * com.android.cts.packagemanager.stats.device.UserInfoUtil + * which is duplicate for CTS tests purposes. + */ + public static int getUserTypeForStatsd(@NonNull String userType) { + switch (userType) { + case USER_TYPE_FULL_SYSTEM: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SYSTEM; + case USER_TYPE_FULL_SECONDARY: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY; + case USER_TYPE_FULL_GUEST: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_GUEST; + case USER_TYPE_FULL_DEMO: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_DEMO; + case USER_TYPE_FULL_RESTRICTED: + return FrameworkStatsLog + .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_RESTRICTED; + case USER_TYPE_PROFILE_MANAGED: + return FrameworkStatsLog + .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__PROFILE_MANAGED; + case USER_TYPE_SYSTEM_HEADLESS: + return FrameworkStatsLog + .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__SYSTEM_HEADLESS; + case USER_TYPE_PROFILE_CLONE: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__PROFILE_CLONE; + default: + return FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN; + } + } + + /** + * Map error code to the event finish state. + */ + @UserLifecycleEventState + private static int errorToFinishState(@UserJourneyErrorCode int errorCode) { + switch (errorCode) { + case ERROR_CODE_ABORTED: + return EVENT_STATE_CANCEL; + case ERROR_CODE_UNSPECIFIED: + return EVENT_STATE_FINISH; + default: + return EVENT_STATE_ERROR; + } + } + + /** + * Simply logging USER_LIFECYCLE_JOURNEY_REPORTED if session exists. + * If session does not exist then it logs ERROR_CODE_INVALID_SESSION_ID + */ + @VisibleForTesting + public void logUserLifecycleJourneyReported(@Nullable UserJourneySession session, + @UserJourney int journey, @UserIdInt int originalUserId, @UserIdInt int targetUserId, + int userType, int userFlags, @UserJourneyErrorCode int errorCode) { + if (session == null) { + writeUserLifecycleJourneyReported(-1, journey, originalUserId, targetUserId, + userType, userFlags, ERROR_CODE_INVALID_SESSION_ID); + } else { + writeUserLifecycleJourneyReported( + session.mSessionId, journey, originalUserId, targetUserId, userType, userFlags, + errorCode); + } + } + + /** + * Helper method for spy testing + */ + @VisibleForTesting + public void writeUserLifecycleJourneyReported(long sessionId, int journey, int originalUserId, + int targetUserId, int userType, int userFlags, int errorCode) { + FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED, + sessionId, journey, originalUserId, targetUserId, userType, userFlags, + errorCode); + } + + /** + * Simply logging USER_LIFECYCLE_EVENT_OCCURRED if session exists. + * If session does not exist then it logs ERROR_CODE_INVALID_SESSION_ID + * and EVENT_STATE_ERROR + */ + @VisibleForTesting + public void logUserLifecycleEventOccurred(UserJourneySession session, + @UserIdInt int targetUserId, @UserLifecycleEvent int event, + @UserLifecycleEventState int state, @UserJourneyErrorCode int errorCode) { + if (session == null) { + writeUserLifecycleEventOccurred(-1, targetUserId, event, + EVENT_STATE_ERROR, ERROR_CODE_INVALID_SESSION_ID); + } else { + writeUserLifecycleEventOccurred(session.mSessionId, targetUserId, event, state, + errorCode); + } + } + + /** + * Helper method for spy testing + */ + @VisibleForTesting + public void writeUserLifecycleEventOccurred(long sessionId, int userId, int event, int state, + int errorCode) { + FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, + sessionId, userId, event, state, errorCode); + } + + /** + * statsd helper method for logging the given event for the UserLifecycleEventOccurred statsd + * atom. It finds the user journey session for target user id and logs it as that journey. + */ + public void logUserLifecycleEvent(@UserIdInt int userId, @UserLifecycleEvent int event, + @UserLifecycleEventState int eventState) { + final UserJourneySession userJourneySession = findUserJourneySession(userId); + logUserLifecycleEventOccurred(userJourneySession, userId, + event, eventState, UserJourneyLogger.ERROR_CODE_UNSPECIFIED); + } + + /** + * Returns first user session from mUserIdToUserJourneyMap for given user id, + * or null if user id was not found in mUserIdToUserJourneyMap. + */ + private @Nullable UserJourneySession findUserJourneySession(@UserIdInt int userId) { + synchronized (mLock) { + final int keyMapSize = mUserIdToUserJourneyMap.size(); + for (int i = 0; i < keyMapSize; i++) { + int key = mUserIdToUserJourneyMap.keyAt(i); + if (key / USER_ID_KEY_MULTIPLICATION == userId) { + return mUserIdToUserJourneyMap.get(key); + } + } + } + return null; + } + + /** + * Returns unique id for user and journey. For example if user id = 11 and journey = 7 + * then unique key = 11 * 100 + 7 = 1107 + */ + private int getUserJourneyKey(@UserIdInt int targetUserId, @UserJourney int journey) { + // We leave 99 for user journeys ids. + return (targetUserId * USER_ID_KEY_MULTIPLICATION) + journey; + } + + /** + * Special use case when user journey incomplete or timeout and current user is unclear + */ + @VisibleForTesting + public UserJourneySession finishAndClearIncompleteUserJourney(@UserIdInt int targetUserId, + @UserJourney int journey) { + synchronized (mLock) { + final int key = getUserJourneyKey(targetUserId, journey); + final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(key); + if (userJourneySession != null) { + logUserLifecycleEventOccurred( + userJourneySession, + targetUserId, + journeyToEvent(userJourneySession.mJourney), + EVENT_STATE_ERROR, + UserJourneyLogger.ERROR_CODE_INCOMPLETE_OR_TIMEOUT); + + logUserLifecycleJourneyReported( + userJourneySession, + journey, + /* originalUserId= */ -1, + targetUserId, + getUserTypeForStatsd(""), -1, + ERROR_CODE_INCOMPLETE_OR_TIMEOUT); + mUserIdToUserJourneyMap.remove(key); + + return userJourneySession; + } + } + return null; + } + + /** + * Log user journey event and report finishing without error + */ + public UserJourneySession logUserJourneyFinish(@UserIdInt int originalUserId, + UserInfo targetUser, @UserJourney int journey) { + return logUserJourneyFinishWithError(originalUserId, targetUser, journey, + ERROR_CODE_UNSPECIFIED); + } + + /** + * Special case when it is unknown which user switch journey was used and checking both + */ + @VisibleForTesting + public UserJourneySession logUserSwitchJourneyFinish(@UserIdInt int originalUserId, + UserInfo targetUser) { + synchronized (mLock) { + final int key_fg = getUserJourneyKey(targetUser.id, USER_JOURNEY_USER_SWITCH_FG); + final int key_ui = getUserJourneyKey(targetUser.id, USER_JOURNEY_USER_SWITCH_UI); + + if (mUserIdToUserJourneyMap.contains(key_fg)) { + return logUserJourneyFinish(originalUserId, targetUser, + USER_JOURNEY_USER_SWITCH_FG); + } + + if (mUserIdToUserJourneyMap.contains(key_ui)) { + return logUserJourneyFinish(originalUserId, targetUser, + USER_JOURNEY_USER_SWITCH_UI); + } + + return null; + } + } + + /** + * Log user journey event and report finishing with error + */ + public UserJourneySession logUserJourneyFinishWithError(@UserIdInt int originalUserId, + UserInfo targetUser, @UserJourney int journey, @UserJourneyErrorCode int errorCode) { + synchronized (mLock) { + final int state = errorToFinishState(errorCode); + final int key = getUserJourneyKey(targetUser.id, journey); + final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(key); + if (userJourneySession != null) { + logUserLifecycleEventOccurred( + userJourneySession, targetUser.id, + journeyToEvent(userJourneySession.mJourney), + state, + errorCode); + + logUserLifecycleJourneyReported( + userJourneySession, + journey, originalUserId, targetUser.id, + getUserTypeForStatsd(targetUser.userType), + targetUser.flags, + errorCode); + mUserIdToUserJourneyMap.remove(key); + + return userJourneySession; + } + } + return null; + } + + /** + * Log event and report finish when user is null. This is edge case when UserInfo + * can not be passed because it is null, therefore all information are passed as arguments. + */ + public UserJourneySession logNullUserJourneyError(@UserJourney int journey, + @UserIdInt int currentUserId, @UserIdInt int targetUserId, String targetUserType, + int targetUserFlags) { + synchronized (mLock) { + final int key = getUserJourneyKey(targetUserId, journey); + final UserJourneySession session = mUserIdToUserJourneyMap.get(key); + + logUserLifecycleEventOccurred( + session, targetUserId, journeyToEvent(journey), + EVENT_STATE_ERROR, + ERROR_CODE_NULL_USER_INFO); + + logUserLifecycleJourneyReported( + session, journey, currentUserId, targetUserId, + getUserTypeForStatsd(targetUserType), targetUserFlags, + ERROR_CODE_NULL_USER_INFO); + + mUserIdToUserJourneyMap.remove(key); + return session; + } + } + + /** + * Log for user creation finish event and report. This is edge case when target user id is + * different in begin event and finish event as it is unknown what is user id + * until it has been created. + */ + public UserJourneySession logUserCreateJourneyFinish(@UserIdInt int originalUserId, + UserInfo targetUser) { + synchronized (mLock) { + // we do not know user id until we create new user which is why we use -1 + // as user id to create and find session, but we log correct id. + final int key = getUserJourneyKey(-1, USER_JOURNEY_USER_CREATE); + final UserJourneySession userJourneySession = mUserIdToUserJourneyMap.get(key); + if (userJourneySession != null) { + logUserLifecycleEventOccurred( + userJourneySession, targetUser.id, + USER_LIFECYCLE_EVENT_CREATE_USER, + EVENT_STATE_FINISH, + ERROR_CODE_UNSPECIFIED); + + logUserLifecycleJourneyReported( + userJourneySession, + USER_JOURNEY_USER_CREATE, originalUserId, targetUser.id, + getUserTypeForStatsd(targetUser.userType), + targetUser.flags, + ERROR_CODE_UNSPECIFIED); + mUserIdToUserJourneyMap.remove(key); + + return userJourneySession; + } + } + return null; + } + + /** + * Adds new UserJourneySession to mUserIdToUserJourneyMap and log UserJourneyEvent Begin state + */ + public UserJourneySession logUserJourneyBegin(@UserIdInt int targetId, + @UserJourney int journey) { + final long newSessionId = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE); + synchronized (mLock) { + final int key = getUserJourneyKey(targetId, journey); + final UserJourneySession userJourneySession = + new UserJourneySession(newSessionId, journey); + mUserIdToUserJourneyMap.append(key, userJourneySession); + + logUserLifecycleEventOccurred( + userJourneySession, targetId, + journeyToEvent(userJourneySession.mJourney), + EVENT_STATE_BEGIN, + ERROR_CODE_UNSPECIFIED); + + return userJourneySession; + } + } + + /** + * Helper class to store user journey and session id. + * + * <p> User journey tracks a chain of user lifecycle events occurring during different user + * activities such as user start, user switch, and user creation. + */ + public static class UserJourneySession { + public final long mSessionId; + @UserJourney + public final int mJourney; + + @VisibleForTesting + public UserJourneySession(long sessionId, @UserJourney int journey) { + mJourney = journey; + mSessionId = sessionId; + } + } +} diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 5f8efe29459d..b92cdde5910f 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -23,6 +23,15 @@ import static android.os.UserManager.DISALLOW_USER_SWITCH; import static android.os.UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY; import static android.os.UserManager.USER_OPERATION_ERROR_UNKNOWN; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_ABORTED; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_UNSPECIFIED; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_USER_ALREADY_AN_ADMIN; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_USER_IS_NOT_AN_ADMIN; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_GRANT_ADMIN; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_REVOKE_ADMIN; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_CREATE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_REMOVE; + import android.Manifest; import android.accounts.Account; import android.accounts.AccountManager; @@ -162,7 +171,6 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -519,6 +527,8 @@ public class UserManagerService extends IUserManager.Stub { @GuardedBy("mUserLifecycleListeners") private final ArrayList<UserLifecycleListener> mUserLifecycleListeners = new ArrayList<>(); + private final UserJourneyLogger mUserJourneyLogger = new UserJourneyLogger(); + private final LockPatternUtils mLockPatternUtils; private final String ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK = @@ -1580,45 +1590,56 @@ public class UserManagerService extends IUserManager.Stub { @Override public void setUserAdmin(@UserIdInt int userId) { checkManageUserAndAcrossUsersFullPermission("set user admin"); - final long sessionId = logGrantAdminJourneyBegin(userId); + mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_GRANT_ADMIN); UserInfo info; synchronized (mPackagesLock) { synchronized (mUsersLock) { info = getUserInfoLU(userId); } - if (info == null || info.isAdmin()) { - // Exit if no user found with that id, or the user is already an Admin. - logUserJourneyError(sessionId, - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN, - userId); + if (info == null) { + // Exit if no user found with that id, + mUserJourneyLogger.logNullUserJourneyError(USER_JOURNEY_GRANT_ADMIN, + getCurrentUserId(), userId, /* userType */ "", /* userFlags */ -1); + return; + } else if (info.isAdmin()) { + // Exit if the user is already an Admin. + mUserJourneyLogger.logUserJourneyFinishWithError(getCurrentUserId(), info, + USER_JOURNEY_GRANT_ADMIN, ERROR_CODE_USER_ALREADY_AN_ADMIN); return; } info.flags ^= UserInfo.FLAG_ADMIN; writeUserLP(getUserDataLU(info.id)); } - logGrantAdminJourneyFinish(sessionId, userId, info.userType, info.flags); + mUserJourneyLogger.logUserJourneyFinishWithError(getCurrentUserId(), info, + USER_JOURNEY_GRANT_ADMIN, ERROR_CODE_UNSPECIFIED); } @Override public void revokeUserAdmin(@UserIdInt int userId) { checkManageUserAndAcrossUsersFullPermission("revoke admin privileges"); - final long sessionId = logRevokeAdminJourneyBegin(userId); + mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_REVOKE_ADMIN); UserData user; synchronized (mPackagesLock) { synchronized (mUsersLock) { user = getUserDataLU(userId); - if (user == null || !user.info.isAdmin()) { - // Exit if no user found with that id, or the user is not an Admin. - logUserJourneyError(sessionId, FrameworkStatsLog - .USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN, - userId); + if (user == null) { + // Exit if no user found with that id + mUserJourneyLogger.logNullUserJourneyError( + USER_JOURNEY_REVOKE_ADMIN, + getCurrentUserId(), userId, "", -1); + return; + } else if (!user.info.isAdmin()) { + // Exit if no user is not an Admin. + mUserJourneyLogger.logUserJourneyFinishWithError(getCurrentUserId(), user.info, + USER_JOURNEY_REVOKE_ADMIN, ERROR_CODE_USER_IS_NOT_AN_ADMIN); return; } user.info.flags ^= UserInfo.FLAG_ADMIN; writeUserLP(user); } } - logRevokeAdminJourneyFinish(sessionId, userId, user.info.userType, user.info.flags); + mUserJourneyLogger.logUserJourneyFinishWithError(getCurrentUserId(), user.info, + USER_JOURNEY_REVOKE_ADMIN, ERROR_CODE_UNSPECIFIED); } /** @@ -4700,16 +4721,20 @@ public class UserManagerService extends IUserManager.Stub { final int noneUserId = -1; final TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("createUser-" + flags); - final long sessionId = logUserCreateJourneyBegin(noneUserId); + mUserJourneyLogger.logUserJourneyBegin(noneUserId, USER_JOURNEY_USER_CREATE); UserInfo newUser = null; try { newUser = createUserInternalUncheckedNoTracing(name, userType, flags, parentId, preCreate, disallowedPackages, t, token); return newUser; } finally { - logUserCreateJourneyFinish(sessionId, - newUser != null ? newUser.id : noneUserId, userType, flags, - newUser != null); + if (newUser != null) { + mUserJourneyLogger.logUserCreateJourneyFinish(getCurrentUserId(), newUser); + } else { + mUserJourneyLogger.logNullUserJourneyError( + USER_JOURNEY_USER_CREATE, + getCurrentUserId(), noneUserId, userType, flags); + } t.traceEnd(); } } @@ -5198,137 +5223,6 @@ public class UserManagerService extends IUserManager.Stub { && !userTypeDetails.getName().equals(UserManager.USER_TYPE_FULL_RESTRICTED); } - private long logUserCreateJourneyBegin(@UserIdInt int userId) { - return logUserJourneyBegin( - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE, - userId); - } - - private void logUserCreateJourneyFinish(long sessionId, @UserIdInt int userId, String userType, - @UserInfoFlag int flags, boolean finish) { - logUserJourneyFinish(sessionId, - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE, - userId, userType, flags, finish); - } - - private long logUserRemoveJourneyBegin(@UserIdInt int userId) { - return logUserJourneyBegin( - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_REMOVE, - userId); - } - - private void logUserRemoveJourneyFinish(long sessionId, @UserIdInt int userId, String userType, - @UserInfoFlag int flags, boolean finish) { - logUserJourneyFinish(sessionId, - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_REMOVE, - userId, userType, flags, finish); - } - - private long logGrantAdminJourneyBegin(@UserIdInt int userId) { - return logUserJourneyBegin( - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN, - userId); - } - - private void logGrantAdminJourneyFinish(long sessionId, @UserIdInt int userId, String userType, - @UserInfoFlag int flags) { - logUserJourneyFinish(sessionId, - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN, - userId, userType, flags, true); - } - - private long logRevokeAdminJourneyBegin(@UserIdInt int userId) { - return logUserJourneyBegin( - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN, - userId); - } - - private void logRevokeAdminJourneyFinish(long sessionId, @UserIdInt int userId, String userType, - @UserInfoFlag int flags) { - logUserJourneyFinish(sessionId, - FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN, - userId, userType, flags, true); - } - - private void logUserJourneyFinish(long sessionId, int journey, @UserIdInt int userId, - String userType, @UserInfoFlag int flags, boolean finish) { - - // log the journey atom with the user metadata - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED, sessionId, - journey, /* origin_user= */ getCurrentUserId(), userId, - UserManager.getUserTypeForStatsd(userType), flags); - - int event; - switch (journey) { - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__CREATE_USER; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_REMOVE: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REMOVE_USER; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__GRANT_ADMIN; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REVOKE_ADMIN; - break; - default: - throw new IllegalArgumentException("Journey " + journey + " not expected."); - } - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, sessionId, userId, - event, - finish ? FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__FINISH - : FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__NONE); - } - - private long logUserJourneyBegin(int journey, @UserIdInt int userId) { - final long sessionId = ThreadLocalRandom.current().nextLong(1, Long.MAX_VALUE); - - // log the event atom to indicate the event start - int event; - switch (journey) { - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_CREATE: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__CREATE_USER; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__USER_REMOVE: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REMOVE_USER; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__GRANT_ADMIN; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REVOKE_ADMIN; - break; - default: - throw new IllegalArgumentException("Journey " + journey + " not expected."); - } - - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, sessionId, userId, - event, FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__BEGIN); - return sessionId; - } - - private void logUserJourneyError(long sessionId, int journey, @UserIdInt int userId) { - - // log the journey atom with the user metadata - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED, sessionId, - journey, /* origin_user= */ getCurrentUserId(), userId); - - int event; - switch (journey) { - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__GRANT_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__GRANT_ADMIN; - break; - case FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__JOURNEY__REVOKE_ADMIN: - event = FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__REVOKE_ADMIN; - break; - default: - throw new IllegalArgumentException("Journey " + journey + " not expected."); - } - FrameworkStatsLog.write(FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED, sessionId, userId, - event, FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__STATE__ERROR); - } - /** Register callbacks for statsd pulled atoms. */ private void registerStatsCallbacks() { final StatsManager statsManager = mContext.getSystemService(StatsManager.class); @@ -5352,7 +5246,8 @@ public class UserManagerService extends IUserManager.Stub { if (size > 1) { for (int idx = 0; idx < size; idx++) { final UserInfo user = users.get(idx); - final int userTypeStandard = UserManager.getUserTypeForStatsd(user.userType); + final int userTypeStandard = mUserJourneyLogger + .getUserTypeForStatsd(user.userType); final String userTypeCustom = (userTypeStandard == FrameworkStatsLog .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN) ? @@ -5635,7 +5530,7 @@ public class UserManagerService extends IUserManager.Stub { writeUserLP(userData); } - final long sessionId = logUserRemoveJourneyBegin(userId); + mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_USER_REMOVE); try { mAppOpsService.removeUser(userId); @@ -5657,13 +5552,17 @@ public class UserManagerService extends IUserManager.Stub { @Override public void userStopped(int userIdParam) { finishRemoveUser(userIdParam); - logUserRemoveJourneyFinish(sessionId, userIdParam, - userData.info.userType, userData.info.flags, true); + int originUserId = UserManagerService.this.getCurrentUserId(); + mUserJourneyLogger.logUserJourneyFinishWithError(originUserId, + userData.info, USER_JOURNEY_USER_REMOVE, + ERROR_CODE_UNSPECIFIED); } @Override public void userStopAborted(int userIdParam) { - logUserRemoveJourneyFinish(sessionId, userIdParam, - userData.info.userType, userData.info.flags, false); + int originUserId = UserManagerService.this.getCurrentUserId(); + mUserJourneyLogger.logUserJourneyFinishWithError(originUserId, + userData.info, USER_JOURNEY_USER_REMOVE, + ERROR_CODE_ABORTED); } }); } catch (RemoteException e) { @@ -7297,9 +7196,9 @@ public class UserManagerService extends IUserManager.Stub { final UserInfo userInfo = getUserInfo(userIds[i]); if (userInfo == null) { // Not possible because the input user ids should all be valid - userTypes[i] = UserManager.getUserTypeForStatsd(""); + userTypes[i] = mUserJourneyLogger.getUserTypeForStatsd(""); } else { - userTypes[i] = UserManager.getUserTypeForStatsd(userInfo.userType); + userTypes[i] = mUserJourneyLogger.getUserTypeForStatsd(userInfo.userType); } } return userTypes; @@ -7536,4 +7435,11 @@ public class UserManagerService extends IUserManager.Stub { .getBoolean(R.bool.config_canSwitchToHeadlessSystemUser); } + /** + * Returns instance of {@link com.android.server.pm.UserJourneyLogger}. + */ + public UserJourneyLogger getUserJourneyLogger() { + return mUserJourneyLogger; + } + } diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 9ff98be6f5cd..f8954b7c7f95 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -5690,8 +5690,14 @@ public final class PowerManagerService extends SystemService } if (eventTime > now) { - Slog.e(TAG, "Event time " + eventTime + " cannot be newer than " + now); - throw new IllegalArgumentException("event time must not be in the future"); + Slog.wtf(TAG, "Event cannot be newer than the current time (" + + "now=" + now + + ", eventTime=" + eventTime + + ", displayId=" + displayId + + ", event=" + PowerManager.userActivityEventToString(event) + + ", flags=" + flags + + ")"); + return; } final int uid = Binder.getCallingUid(); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 24a271f93e29..26b40b4c09ee 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -582,6 +582,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean mPauseSchedulePendingForPip = false; + // Gets set to indicate that the activity is currently being auto-pipped. + boolean mAutoEnteringPip = false; + private void updateEnterpriseThumbnailDrawable(Context context) { DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); mEnterpriseThumbnailDrawable = dpm.getResources().getDrawable( @@ -6099,8 +6102,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A try { mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token, PauseActivityItem.obtain(finishing, false /* userLeaving */, - configChangeFlags, false /* dontReport */, - false /* autoEnteringPip */)); + configChangeFlags, false /* dontReport */, mAutoEnteringPip)); } catch (Exception e) { Slog.w(TAG, "Exception thrown sending pause: " + intent.getComponent(), e); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index a0ea1c3dbdbf..f86df2aa9bed 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3592,15 +3592,21 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } } + boolean enterPictureInPictureMode(@NonNull ActivityRecord r, + @NonNull PictureInPictureParams params, boolean fromClient) { + return enterPictureInPictureMode(r, params, fromClient, false /* isAutoEnter */); + } + /** * Puts the given activity in picture in picture mode if possible. * * @param fromClient true if this comes from a client call (eg. Activity.enterPip). + * @param isAutoEnter true if this comes from an automatic pip-enter. * @return true if the activity is now in picture-in-picture mode, or false if it could not * enter picture-in-picture mode. */ boolean enterPictureInPictureMode(@NonNull ActivityRecord r, - @NonNull PictureInPictureParams params, boolean fromClient) { + @NonNull PictureInPictureParams params, boolean fromClient, boolean isAutoEnter) { // If the activity is already in picture in picture mode, then just return early if (r.inPinnedWindowingMode()) { return true; @@ -3635,6 +3641,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return; } r.setPictureInPictureParams(params); + r.mAutoEnteringPip = isAutoEnter; mRootWindowContainer.moveActivityToPinnedRootTask(r, null /* launchIntoPipHostActivity */, "enterPictureInPictureMode", transition); @@ -3643,6 +3650,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { r.getTask().schedulePauseActivity(r, false /* userLeaving */, false /* pauseImmediately */, true /* autoEnteringPip */, "auto-pip"); } + r.mAutoEnteringPip = false; } }; diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index 08a6358afbc7..5f6d66011768 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -269,6 +269,8 @@ class KeyguardController { TRANSIT_TO_BACK, transitFlags, null /* trigger */, dc); updateKeyguardSleepToken(); + // Make the home wallpaper visible + dc.mWallpaperController.showHomeWallpaperInTransition(); // Some stack visibility might change (e.g. docked stack) mRootWindowContainer.resumeFocusedTasksTopActivities(); mRootWindowContainer.ensureActivitiesVisible(null, 0, !PRESERVE_WINDOWS); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 5caf6636d912..a1e497e1e099 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -963,7 +963,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return false; } return mController.mAtm.enterPictureInPictureMode(ar, ar.pictureInPictureArgs, - false /* fromClient */); + false /* fromClient */, true /* isAutoEnter */); } // Legacy pip-entry (not via isAutoEnterEnabled). @@ -1325,6 +1325,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // ActivityRecord#canShowWindows() may reject to show its window. The visibility also // needs to be updated for STATE_ABORT. commitVisibleActivities(transaction); + commitVisibleWallpapers(); // Fall-back to the default display if there isn't one participating. final DisplayContent primaryDisplay = !mTargetDisplays.isEmpty() ? mTargetDisplays.get(0) @@ -1357,6 +1358,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } // Resolve the animating targets from the participants. mTargets = calculateTargets(mParticipants, mChanges); + // Check whether the participants were animated from back navigation. mController.mAtm.mBackNavigationController.onTransactionReady(this, mTargets); final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, transaction); @@ -1633,6 +1635,30 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } + /** + * Reset waitingToshow for all wallpapers, and commit the visibility of the visible ones + */ + private void commitVisibleWallpapers() { + boolean showWallpaper = shouldWallpaperBeVisible(); + for (int i = mParticipants.size() - 1; i >= 0; --i) { + final WallpaperWindowToken wallpaper = mParticipants.valueAt(i).asWallpaperToken(); + if (wallpaper != null) { + wallpaper.waitingToShow = false; + if (!wallpaper.isVisible() && wallpaper.isVisibleRequested()) { + wallpaper.commitVisibility(showWallpaper); + } + } + } + } + + private boolean shouldWallpaperBeVisible() { + for (int i = mParticipants.size() - 1; i >= 0; --i) { + WindowContainer participant = mParticipants.valueAt(i); + if (participant.showWallpaper()) return true; + } + return false; + } + // TODO(b/188595497): Remove after migrating to shell. /** @see RecentsAnimationController#attachNavigationBarToApp */ private void handleLegacyRecentsStartBehavior(DisplayContent dc, TransitionInfo info) { diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index f41631683e83..edafe0606b13 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -339,6 +339,31 @@ class WallpaperController { } } + /** + * Change the visibility if wallpaper is home screen only. + * This is called during the keyguard unlocking transition + * (see {@link KeyguardController#keyguardGoingAway(int, int)}) and thus assumes that if the + * system wallpaper is shared with lock, then it needs no animation. + */ + public void showHomeWallpaperInTransition() { + updateWallpaperWindowsTarget(mFindResults); + + if (!mFindResults.hasTopShowWhenLockedWallpaper()) { + Slog.w(TAG, "There is no wallpaper for the lock screen"); + return; + } + WindowState hideWhenLocked = mFindResults.mTopWallpaper.mTopHideWhenLockedWallpaper; + WindowState showWhenLocked = mFindResults.mTopWallpaper.mTopShowWhenLockedWallpaper; + if (!mFindResults.hasTopHideWhenLockedWallpaper()) { + // Shared wallpaper, ensure its visibility + showWhenLocked.mToken.asWallpaperToken().updateWallpaperWindows(true); + } else { + // Separate lock and home wallpapers: show home wallpaper and hide lock + hideWhenLocked.mToken.asWallpaperToken().updateWallpaperWindowsInTransition(true); + showWhenLocked.mToken.asWallpaperToken().updateWallpaperWindowsInTransition(false); + } + } + void hideDeferredWallpapersIfNeededLegacy() { for (int i = mWallpaperTokens.size() - 1; i >= 0; i--) { final WallpaperWindowToken token = mWallpaperTokens.get(i); @@ -815,7 +840,6 @@ class WallpaperController { } } - // Keep both wallpapers visible unless the keyguard is locked (then hide private wp) if (!mDisplayContent.isKeyguardGoingAway() || !mIsLockscreenLiveWallpaperEnabled) { // When keyguard goes away, KeyguardController handles the visibility updateWallpaperTokens(visible, mDisplayContent.isKeyguardLocked()); diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 1ffee05d20ec..5ea8f65b759c 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -128,6 +128,20 @@ class WallpaperWindowToken extends WindowToken { } } + /** + * Update the visibility of the token to {@param visible}. If a transition will collect the + * wallpaper, then the visibility will be committed during the execution of the transition. + * + * waitingToShow is reset at the beginning of the transition: + * {@link Transition#onTransactionReady(int, SurfaceControl.Transaction)} + */ + void updateWallpaperWindowsInTransition(boolean visible) { + if (mTransitionController.isCollecting() && mVisibleRequested != visible) { + waitingToShow = true; + } + updateWallpaperWindows(visible); + } + void updateWallpaperWindows(boolean visible) { if (mVisibleRequested != visible) { ProtoLog.d(WM_DEBUG_WALLPAPER, "Wallpaper token %s visible=%b", @@ -199,11 +213,11 @@ class WallpaperWindowToken extends WindowToken { } /** - * Commits the visibility of this token. This will directly update the visibility without - * regard for other state (like being in a transition). + * Commits the visibility of this token. This will directly update the visibility unless the + * wallpaper is in a transition. */ void commitVisibility(boolean visible) { - if (visible == isVisible()) return; + if (visible == isVisible() || waitingToShow) return; ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "commitVisibility: %s: visible=%b mVisibleRequested=%b", this, diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index cf49dcf8004e..0c4830afafcc 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -684,6 +684,38 @@ final class DevicePolicyEngine { } } + <V> void transferPolicies(EnforcingAdmin oldAdmin, EnforcingAdmin newAdmin) { + Set<PolicyKey> globalPolicies = new HashSet<>(mGlobalPolicies.keySet()); + for (PolicyKey policy : globalPolicies) { + PolicyState<?> policyState = mGlobalPolicies.get(policy); + if (policyState.getPoliciesSetByAdmins().containsKey(oldAdmin)) { + PolicyDefinition<V> policyDefinition = + (PolicyDefinition<V>) policyState.getPolicyDefinition(); + PolicyValue<V> policyValue = + (PolicyValue<V>) policyState.getPoliciesSetByAdmins().get(oldAdmin); + setGlobalPolicy(policyDefinition, newAdmin, policyValue); + } + } + + for (int i = 0; i < mLocalPolicies.size(); i++) { + int userId = mLocalPolicies.keyAt(i); + Set<PolicyKey> localPolicies = new HashSet<>( + mLocalPolicies.get(userId).keySet()); + for (PolicyKey policy : localPolicies) { + PolicyState<?> policyState = mLocalPolicies.get(userId).get(policy); + if (policyState.getPoliciesSetByAdmins().containsKey(oldAdmin)) { + PolicyDefinition<V> policyDefinition = + (PolicyDefinition<V>) policyState.getPolicyDefinition(); + PolicyValue<V> policyValue = + (PolicyValue<V>) policyState.getPoliciesSetByAdmins().get(oldAdmin); + setLocalPolicy(policyDefinition, newAdmin, policyValue, userId); + } + } + } + + removePoliciesForAdmin(oldAdmin); + } + private Set<UserRestrictionPolicyKey> getUserRestrictionPolicyKeysForAdminLocked( Map<PolicyKey, PolicyState<?>> policies, EnforcingAdmin admin) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 675ebd3ddd60..3578b16d62c6 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -3882,6 +3882,17 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final ActiveAdmin adminToTransfer = policy.mAdminMap.get(outgoingReceiver); final int oldAdminUid = adminToTransfer.getUid(); + if (isPolicyEngineForFinanceFlagEnabled() || isPermissionCheckFlagEnabled()) { + EnforcingAdmin oldAdmin = + EnforcingAdmin.createEnterpriseEnforcingAdmin( + outgoingReceiver, userHandle, adminToTransfer); + EnforcingAdmin newAdmin = + EnforcingAdmin.createEnterpriseEnforcingAdmin( + incomingReceiver, userHandle, adminToTransfer); + + mDevicePolicyEngine.transferPolicies(oldAdmin, newAdmin); + } + adminToTransfer.transfer(incomingDeviceInfo); policy.mAdminMap.remove(outgoingReceiver); policy.mAdminMap.put(incomingReceiver, adminToTransfer); @@ -6051,7 +6062,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public void lockNow(int flags, String callerPackageName, boolean parent) { CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { caller = getCallerIdentity(callerPackageName); } else { caller = getCallerIdentity(); @@ -6063,7 +6074,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { ActiveAdmin admin; // Make sure the caller has any active admin with the right policy or // the required permission. - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { admin = enforcePermissionAndGetEnforcingAdmin( /* admin= */ null, /* permission= */ MANAGE_DEVICE_POLICY_LOCK, @@ -8917,13 +8928,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { caller = getCallerIdentity(who, callerPackageName); } else { caller = getCallerIdentity(who); } - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { // The effect of this policy is device-wide. enforcePermission(SET_TIME, caller.getPackageName(), UserHandle.USER_ALL); } else { @@ -8951,13 +8962,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return false; } CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { caller = getCallerIdentity(who, callerPackageName); } else { caller = getCallerIdentity(who); } - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { enforceCanQuery(SET_TIME, caller.getPackageName(), UserHandle.USER_ALL); } else { Objects.requireNonNull(who, "ComponentName is null"); @@ -8980,13 +8991,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { caller = getCallerIdentity(who, callerPackageName); } else { caller = getCallerIdentity(who); } - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { // The effect of this policy is device-wide. EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( who, @@ -9026,13 +9037,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { caller = getCallerIdentity(who, callerPackageName); } else { caller = getCallerIdentity(who); } - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { // The effect of this policy is device-wide. enforceCanQuery(SET_TIME_ZONE, caller.getPackageName(), UserHandle.USER_ALL); } else { @@ -9335,7 +9346,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { caller = getCallerIdentity(who, callerPackageName); } else { caller = getCallerIdentity(who); @@ -9345,7 +9356,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final int userHandle = caller.getUserId(); int affectedUserId = parent ? getProfileParentId(userHandle) : userHandle; synchronized (getLockObject()) { - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { // SUPPORT USES_POLICY_DISABLE_KEYGUARD_FEATURES EnforcingAdmin admin = enforcePermissionAndGetEnforcingAdmin( who, MANAGE_DEVICE_POLICY_KEYGUARD, caller.getPackageName(), @@ -9424,13 +9435,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { synchronized (getLockObject()) { if (who != null) { - if (isPermissionCheckFlagEnabled()) { - EnforcingAdmin admin = getEnforcingAdminForCaller( - who, who.getPackageName()); + if (isUnicornFlagEnabled()) { + EnforcingAdmin admin = getEnforcingAdminForPackage( + who, who.getPackageName(), userHandle); Integer features = mDevicePolicyEngine.getLocalPolicySetByAdmin( PolicyDefinition.KEYGUARD_DISABLED_FEATURES, admin, affectedUserId); + return features == null ? 0 : features; } else { ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent); @@ -9438,7 +9450,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { Integer features = mDevicePolicyEngine.getResolvedPolicy( PolicyDefinition.KEYGUARD_DISABLED_FEATURES, affectedUserId); @@ -9999,10 +10011,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { "clearDeviceOwner can only be called by the device owner"); } enforceUserUnlocked(deviceOwnerUserId); - DevicePolicyData policy = getUserData(deviceOwnerUserId); - if (policy.mPasswordTokenHandle != 0) { - mLockPatternUtils.removeEscrowToken(policy.mPasswordTokenHandle, deviceOwnerUserId); - } final ActiveAdmin admin = getDeviceOwnerAdminLocked(); mInjector.binderWithCleanCallingIdentity(() -> { @@ -10057,6 +10065,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } final DevicePolicyData policyData = getUserData(userId); policyData.mCurrentInputMethodSet = false; + if (policyData.mPasswordTokenHandle != 0) { + mLockPatternUtils.removeEscrowToken(policyData.mPasswordTokenHandle, userId); + policyData.mPasswordTokenHandle = 0; + } saveSettingsLocked(userId); mPolicyCache.onUserRemoved(userId); final DevicePolicyData systemPolicyData = getUserData(UserHandle.USER_SYSTEM); @@ -10748,7 +10760,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @VisibleForTesting boolean hasDeviceIdAccessUnchecked(String packageName, int uid) { final int userId = UserHandle.getUserId(uid); - if (isPermissionCheckFlagEnabled()) { + // TODO(b/280048070): Introduce a permission to handle device ID access + if (isPermissionCheckFlagEnabled() + && !(isUidProfileOwnerLocked(uid) || isUidDeviceOwnerLocked(uid))) { return hasPermission(MANAGE_DEVICE_POLICY_CERTIFICATES, packageName, userId); } else { ComponentName deviceOwner = getDeviceOwnerComponent(true); @@ -11635,7 +11649,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final CallerIdentity caller = getCallerIdentity(who, callerPackage); checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_APPLICATION_RESTRICTIONS); - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( who, MANAGE_DEVICE_POLICY_APP_RESTRICTIONS, @@ -13062,7 +13076,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { String packageName) { final CallerIdentity caller = getCallerIdentity(who, callerPackage); - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { EnforcingAdmin enforcingAdmin = enforceCanQueryAndGetEnforcingAdmin( who, MANAGE_DEVICE_POLICY_APP_RESTRICTIONS, @@ -13132,7 +13146,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final CallerIdentity caller = getCallerIdentity(who, callerPackage); ActiveAdmin admin; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( who, MANAGE_DEVICE_POLICY_PACKAGE_STATE, @@ -13229,7 +13243,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { public boolean isPackageSuspended(ComponentName who, String callerPackage, String packageName) { final CallerIdentity caller = getCallerIdentity(who, callerPackage); - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { enforcePermission( MANAGE_DEVICE_POLICY_PACKAGE_STATE, caller.getPackageName(), @@ -13835,7 +13849,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { boolean hidden, boolean parent) { CallerIdentity caller = getCallerIdentity(who, callerPackage); final int userId = parent ? getProfileParentId(caller.getUserId()) : caller.getUserId(); - if (isPermissionCheckFlagEnabled()) { + if (isPolicyEngineForFinanceFlagEnabled()) { // TODO: We need to ensure the delegate with DELEGATION_PACKAGE_ACCESS can do this enforcePermission(MANAGE_DEVICE_POLICY_PACKAGE_STATE, caller.getPackageName(), userId); } else { @@ -13854,7 +13868,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { boolean result; synchronized (getLockObject()) { if (parent) { - if (!isPermissionCheckFlagEnabled()) { + if (!isPolicyEngineForFinanceFlagEnabled()) { Preconditions.checkCallAuthorization( isProfileOwnerOfOrganizationOwnedDevice( caller.getUserId()) && isManagedProfile(caller.getUserId())); @@ -13871,7 +13885,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Slogf.v(LOG_TAG, "calling pm.setApplicationHiddenSettingAsUser(%s, %b, %d)", packageName, hidden, userId); } - if (isPermissionCheckFlagEnabled()) { + if (isPolicyEngineForFinanceFlagEnabled()) { EnforcingAdmin admin = getEnforcingAdminForCaller(who, callerPackage); mDevicePolicyEngine.setLocalPolicy( PolicyDefinition.APPLICATION_HIDDEN(packageName), @@ -13910,7 +13924,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { String packageName, boolean parent) { CallerIdentity caller = getCallerIdentity(who, callerPackage); int userId = parent ? getProfileParentId(caller.getUserId()) : caller.getUserId(); - if (isPermissionCheckFlagEnabled()) { + if (isPolicyEngineForFinanceFlagEnabled()) { // TODO: Also support DELEGATION_PACKAGE_ACCESS enforcePermission(MANAGE_DEVICE_POLICY_PACKAGE_STATE, caller.getPackageName(), userId); } else { @@ -13922,7 +13936,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { synchronized (getLockObject()) { if (parent) { - if (!isPermissionCheckFlagEnabled()) { + if (!isPolicyEngineForFinanceFlagEnabled()) { Preconditions.checkCallAuthorization( isProfileOwnerOfOrganizationOwnedDevice(caller.getUserId()) && isManagedProfile(caller.getUserId())); @@ -14114,13 +14128,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { enforceMaxStringLength(accountType, "account type"); CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isPolicyEngineForFinanceFlagEnabled()) { caller = getCallerIdentity(who, callerPackageName); } else { caller = getCallerIdentity(who); } synchronized (getLockObject()) { - if (isPermissionCheckFlagEnabled()) { + if (isPolicyEngineForFinanceFlagEnabled()) { int affectedUser = getAffectedUser(parent); EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( who, @@ -14183,7 +14197,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { CallerIdentity caller; Preconditions.checkArgumentNonnegative(userId, "Invalid userId"); final ArraySet<String> resultSet = new ArraySet<>(); - if (isPermissionCheckFlagEnabled()) { + if (isPolicyEngineForFinanceFlagEnabled()) { int affectedUser = parent ? getProfileParentId(userId) : userId; caller = getCallerIdentity(callerPackageName); if (!hasPermission(MANAGE_DEVICE_POLICY_ACCOUNT_MANAGEMENT, @@ -15554,12 +15568,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { public boolean setStatusBarDisabled(ComponentName who, String callerPackageName, boolean disabled) { CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { caller = getCallerIdentity(who, callerPackageName); } else { caller = getCallerIdentity(who); } - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { enforcePermission(MANAGE_DEVICE_POLICY_STATUS_BAR, caller.getPackageName(), UserHandle.USER_ALL); } else { @@ -15570,7 +15584,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { int userId = caller.getUserId(); synchronized (getLockObject()) { - if (!isPermissionCheckFlagEnabled()) { + if (!isUnicornFlagEnabled()) { Preconditions.checkCallAuthorization(isUserAffiliatedWithDeviceLocked(userId), "Admin " + who + " is neither the device owner or affiliated " + "user's profile owner."); @@ -15629,7 +15643,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public boolean isStatusBarDisabled(String callerPackage) { final CallerIdentity caller = getCallerIdentity(callerPackage); - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { enforceCanQuery( MANAGE_DEVICE_POLICY_STATUS_BAR, caller.getPackageName(), caller.getUserId()); } else { @@ -15639,7 +15653,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { int userId = caller.getUserId(); synchronized (getLockObject()) { - if (!isPermissionCheckFlagEnabled()) { + if (!isUnicornFlagEnabled()) { Preconditions.checkCallAuthorization(isUserAffiliatedWithDeviceLocked(userId), "Admin " + callerPackage + " is neither the device owner or affiliated user's profile owner."); @@ -16800,7 +16814,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } EnforcingAdmin enforcingAdmin; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( admin, MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS, @@ -16971,7 +16985,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { public int getPermissionGrantState(ComponentName admin, String callerPackage, String packageName, String permission) throws RemoteException { final CallerIdentity caller = getCallerIdentity(admin, callerPackage); - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { enforceCanQuery(MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS, caller.getPackageName(), caller.getUserId()); } else { @@ -19109,14 +19123,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { throw new IllegalArgumentException("token must be at least 32-byte long"); } CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { caller = getCallerIdentity(admin, callerPackageName); } else { caller = getCallerIdentity(admin); } final int userId = caller.getUserId(); - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( admin, MANAGE_DEVICE_POLICY_RESET_PASSWORD, @@ -19172,7 +19186,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return false; } CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { caller = getCallerIdentity(admin, callerPackageName); } else { caller = getCallerIdentity(admin); @@ -19180,7 +19194,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final int userId = caller.getUserId(); boolean result = false; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( admin, MANAGE_DEVICE_POLICY_RESET_PASSWORD, @@ -19219,14 +19233,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return false; } CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { caller = getCallerIdentity(admin, callerPackageName); } else { caller = getCallerIdentity(admin); } int userId = caller.getUserId(); - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( admin, MANAGE_DEVICE_POLICY_RESET_PASSWORD, @@ -19268,7 +19282,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Objects.requireNonNull(token); CallerIdentity caller; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { caller = getCallerIdentity(admin, callerPackageName); } else { caller = getCallerIdentity(admin); @@ -19278,7 +19292,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { boolean result = false; final String password = passwordOrNull != null ? passwordOrNull : ""; - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( admin, MANAGE_DEVICE_POLICY_RESET_PASSWORD, @@ -19309,7 +19323,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } if (result) { - if (isPermissionCheckFlagEnabled()) { + if (isUnicornFlagEnabled()) { DevicePolicyEventLogger .createEvent(DevicePolicyEnums.RESET_PASSWORD_WITH_TOKEN) .setAdmin(callerPackageName) @@ -22930,6 +22944,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_LOCATION, MANAGE_DEVICE_POLICY_LOCK, MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS, + MANAGE_DEVICE_POLICY_CERTIFICATES, MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION, MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY, MANAGE_DEVICE_POLICY_PACKAGE_STATE, @@ -23566,6 +23581,30 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return EnforcingAdmin.createEnforcingAdmin(caller.getPackageName(), userId, admin); } + private EnforcingAdmin getEnforcingAdminForPackage(@Nullable ComponentName who, + String packageName, int userId) { + ActiveAdmin admin; + if (who != null) { + if (isDeviceOwner(who, userId) || isProfileOwner(who, userId)) { + synchronized (getLockObject()) { + admin = getActiveAdminUncheckedLocked(who, userId); + } + if (admin != null) { + return EnforcingAdmin.createEnterpriseEnforcingAdmin(who, userId, admin); + } + } else { + // Check for non-DPC active admins. + admin = getActiveAdminUncheckedLocked(who, userId); + if (admin != null) { + return EnforcingAdmin.createDeviceAdminEnforcingAdmin(who, userId, admin); + } + } + } + + admin = getUserData(userId).createOrGetPermissionBasedAdmin(userId); + return EnforcingAdmin.createEnforcingAdmin(packageName, userId, admin); + } + private int getAffectedUser(boolean calledOnParent) { int callingUserId = mInjector.userHandleGetCallingUserId(); return calledOnParent ? getProfileParentId(callingUserId) : callingUserId; @@ -23621,6 +23660,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { DEFAULT_KEEP_PROFILES_RUNNING_FLAG); } + private boolean isUnicornFlagEnabled() { + return false; + } + private boolean isWorkProfileTelephonyEnabled() { return isWorkProfileTelephonyDevicePolicyManagerFlagEnabled() && isWorkProfileTelephonySubscriptionManagerFlagEnabled(); diff --git a/services/tests/servicestests/src/com/android/server/am/UidObserverControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UidObserverControllerTest.java index f788c92b24b2..46974cf72381 100644 --- a/services/tests/servicestests/src/com/android/server/am/UidObserverControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UidObserverControllerTest.java @@ -28,6 +28,8 @@ import static android.app.ActivityManager.PROCESS_STATE_RECEIVER; import static android.app.ActivityManager.PROCESS_STATE_SERVICE; import static android.app.ActivityManager.PROCESS_STATE_TOP; +import static com.android.server.am.ProcessList.UNKNOWN_ADJ; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; @@ -81,7 +83,7 @@ public class UidObserverControllerTest { public void testEnqueueUidChange() { int change = mUidObserverController.enqueueUidChange(null, TEST_UID1, UidRecord.CHANGE_ACTIVE, PROCESS_STATE_FOREGROUND_SERVICE, - PROCESS_CAPABILITY_ALL, 0, false); + UNKNOWN_ADJ, PROCESS_CAPABILITY_ALL, 0, false); assertEquals("expected=ACTIVE,actual=" + changeToStr(change), UidRecord.CHANGE_ACTIVE, change); assertPendingChange(TEST_UID1, UidRecord.CHANGE_ACTIVE, PROCESS_STATE_FOREGROUND_SERVICE, @@ -91,8 +93,8 @@ public class UidObserverControllerTest { final ChangeRecord record2 = new ChangeRecord(); change = mUidObserverController.enqueueUidChange(record2, TEST_UID2, - UidRecord.CHANGE_CACHED, PROCESS_STATE_CACHED_RECENT, PROCESS_CAPABILITY_NONE, - 99, true); + UidRecord.CHANGE_CACHED, PROCESS_STATE_CACHED_RECENT, UNKNOWN_ADJ, + PROCESS_CAPABILITY_NONE, 99, true); assertEquals("expected=ACTIVE,actual=" + changeToStr(change), UidRecord.CHANGE_CACHED, change); assertPendingChange(TEST_UID1, UidRecord.CHANGE_ACTIVE, PROCESS_STATE_FOREGROUND_SERVICE, @@ -101,7 +103,8 @@ public class UidObserverControllerTest { PROCESS_CAPABILITY_NONE, 99, true, record2); change = mUidObserverController.enqueueUidChange(record1, TEST_UID1, - UidRecord.CHANGE_UNCACHED, PROCESS_STATE_TOP, PROCESS_CAPABILITY_ALL, 0, false); + UidRecord.CHANGE_UNCACHED, PROCESS_STATE_TOP, UNKNOWN_ADJ, + PROCESS_CAPABILITY_ALL, 0, false); assertEquals("expected=ACTIVE|UNCACHED,actual=" + changeToStr(change), UidRecord.CHANGE_ACTIVE | UidRecord.CHANGE_UNCACHED, change); assertPendingChange(TEST_UID1, UidRecord.CHANGE_ACTIVE | UidRecord.CHANGE_UNCACHED, diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index d12741ac8bd6..cbc7dc2f2fe5 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -100,6 +100,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.server.FgThread; import com.android.server.SystemService; import com.android.server.am.UserState.KeyEvictedCallback; +import com.android.server.pm.UserJourneyLogger; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.wm.WindowManagerService; @@ -1074,6 +1075,8 @@ public class UserControllerTest { private final KeyguardManager mKeyguardManagerMock; private final LockPatternUtils mLockPatternUtilsMock; + private final UserJourneyLogger mUserJourneyLoggerMock; + private final Context mCtx; TestInjector(Context ctx) { @@ -1090,6 +1093,7 @@ public class UserControllerTest { mKeyguardManagerMock = mock(KeyguardManager.class); when(mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(true); mLockPatternUtilsMock = mock(LockPatternUtils.class); + mUserJourneyLoggerMock = mock(UserJourneyLogger.class); } @Override @@ -1220,6 +1224,11 @@ public class UserControllerTest { void onSystemUserVisibilityChanged(boolean visible) { Log.i(TAG, "onSystemUserVisibilityChanged(" + visible + ")"); } + + @Override + protected UserJourneyLogger getUserJourneyLogger() { + return mUserJourneyLoggerMock; + } } private static class TestHandler extends Handler { diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 1e342f580745..57755a9525fc 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -1512,6 +1512,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { * Validates that when the device owner is removed, the reset password token is cleared */ @Test + @Ignore("b/277916462") public void testClearDeviceOwner_clearResetPasswordToken() throws Exception { mContext.callerPermissions.add(android.Manifest.permission.MANAGE_DEVICE_ADMINS); mContext.callerPermissions.add(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS); @@ -2602,6 +2603,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void testSetApplicationHiddenWithDO() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; setupDeviceOwner(); @@ -2627,6 +2629,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void testSetApplicationHiddenWithPOOfOrganizationOwnedDevice() throws Exception { final int MANAGED_PROFILE_USER_ID = CALLER_USER_HANDLE; final int MANAGED_PROFILE_ADMIN_UID = @@ -4373,6 +4376,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void testSetAutoTimeZoneEnabledModifiesSetting() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; setupDeviceOwner(); @@ -4384,6 +4388,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void testSetAutoTimeZoneEnabledWithPOOnUser0() throws Exception { mContext.binder.callingUid = DpmMockContext.SYSTEM_UID; setupProfileOwnerOnUser0(); @@ -4395,6 +4400,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void testSetAutoTimeZoneEnabledFailWithPONotOnUser0() throws Exception { setupProfileOwner(); assertExpectException(SecurityException.class, null, @@ -4404,6 +4410,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void testSetAutoTimeZoneEnabledWithPOOfOrganizationOwnedDevice() throws Exception { setupProfileOwner(); configureProfileOwnerOfOrgOwnedDevice(admin1, CALLER_USER_HANDLE); @@ -5377,6 +5384,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void testResetPasswordWithToken() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; setupDeviceOwner(); @@ -5411,6 +5419,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void resetPasswordWithToken_NumericPin() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; setupDeviceOwner(); @@ -5431,6 +5440,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void resetPasswordWithToken_EmptyPassword() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; setupDeviceOwner(); @@ -7251,6 +7261,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void testCanProfileOwnerResetPasswordWhenLocked() throws Exception { setDeviceEncryptionPerUser(); setupProfileOwner(); @@ -7314,6 +7325,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void testSetAccountTypesWithManagementDisabledOnManagedProfile() throws Exception { setupProfileOwner(); @@ -7333,6 +7345,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/277916462") public void testSetAccountTypesWithManagementDisabledOnOrgOwnedManagedProfile() throws Exception { mContext.callerPermissions.add(permission.INTERACT_ACROSS_USERS); diff --git a/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java new file mode 100644 index 000000000000..20e2692cb747 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/UserJourneyLoggerTest.java @@ -0,0 +1,580 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_ABORTED; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_INCOMPLETE_OR_TIMEOUT; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_NULL_USER_INFO; +import static com.android.server.pm.UserJourneyLogger.ERROR_CODE_UNSPECIFIED; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_BEGIN; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_CANCEL; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_ERROR; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_FINISH; +import static com.android.server.pm.UserJourneyLogger.EVENT_STATE_NONE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_GRANT_ADMIN; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_CREATE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_REMOVE; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_START; +import static com.android.server.pm.UserJourneyLogger.USER_JOURNEY_USER_STOP; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_CREATE_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_REMOVE_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_REVOKE_ADMIN; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_START_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_STOP_USER; +import static com.android.server.pm.UserJourneyLogger.USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.pm.UserInfo; +import android.platform.test.annotations.Presubmit; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.FrameworkStatsLog; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class UserJourneyLoggerTest { + + public static final int FULL_USER_ADMIN_FLAG = 0x00000402; + private UserJourneyLogger mUserJourneyLogger; + + @Before + public void setup() throws Exception { + mUserJourneyLogger = spy(new UserJourneyLogger()); + } + + @Test + public void testUserStartLifecycleJourneyReported() { + final UserLifecycleJourneyReportedCaptor report1 = new UserLifecycleJourneyReportedCaptor(); + final UserJourneyLogger.UserJourneySession session = new UserJourneyLogger + .UserJourneySession(10, USER_JOURNEY_USER_START); + + report1.captureLogAndAssert(mUserJourneyLogger, session, + USER_JOURNEY_USER_START, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, 1, + ERROR_CODE_UNSPECIFIED); + } + + + @Test + public void testUserLifecycleEventOccurred() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = new UserJourneyLogger + .UserJourneySession(10, USER_JOURNEY_USER_START); + + report1.captureLogAndAssert(mUserJourneyLogger, session, 0, + USER_LIFECYCLE_EVENT_START_USER, EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED); + } + + @Test + public void testLogUserLifecycleEvent() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_START_USER, EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + mUserJourneyLogger.logUserLifecycleEvent(10, USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, + EVENT_STATE_NONE); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_USER_RUNNING_LOCKED, + EVENT_STATE_NONE, ERROR_CODE_UNSPECIFIED, 2); + } + + + @Test + public void testCreateUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(-1, USER_JOURNEY_USER_CREATE); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, -1, + USER_LIFECYCLE_EVENT_CREATE_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserCreateJourneyFinish(0, targetUser); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_CREATE_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_CREATE, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testRemoveUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_REMOVE); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_REMOVE_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(0, targetUser, + USER_JOURNEY_USER_REMOVE); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_REMOVE_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_REMOVE, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testStartUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(0, targetUser, + USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_START, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testStopUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_STOP); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(0, targetUser, + USER_JOURNEY_USER_STOP); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_STOP, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testAbortStopUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_STOP); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + + mUserJourneyLogger.logUserJourneyFinishWithError(-1, targetUser, + USER_JOURNEY_USER_STOP, ERROR_CODE_ABORTED); + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, + EVENT_STATE_CANCEL, ERROR_CODE_ABORTED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_STOP, -1, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_ABORTED, 1); + } + + @Test + public void testIncompleteStopUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_STOP); + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + mUserJourneyLogger.finishAndClearIncompleteUserJourney(10, USER_JOURNEY_USER_STOP); + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, + EVENT_STATE_ERROR, + ERROR_CODE_INCOMPLETE_OR_TIMEOUT, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_USER_STOP, -1, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN, + -1, // information about user are incomplete + ERROR_CODE_INCOMPLETE_OR_TIMEOUT, 1); + } + + @Test + public void testGrantAdminUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_GRANT_ADMIN); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + UserJourneyLogger.USER_LIFECYCLE_EVENT_GRANT_ADMIN, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(0, targetUser, + USER_JOURNEY_GRANT_ADMIN); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + UserJourneyLogger.USER_LIFECYCLE_EVENT_GRANT_ADMIN, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_GRANT_ADMIN, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testNullUserErrorGrantAdminUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + + UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_GRANT_ADMIN); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + UserJourneyLogger.USER_LIFECYCLE_EVENT_GRANT_ADMIN, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + mUserJourneyLogger.logNullUserJourneyError(USER_JOURNEY_GRANT_ADMIN, + 0, 10, "", -1); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + UserJourneyLogger.USER_LIFECYCLE_EVENT_GRANT_ADMIN, + EVENT_STATE_ERROR, ERROR_CODE_NULL_USER_INFO, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + USER_JOURNEY_GRANT_ADMIN, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_EVENT_OCCURRED__EVENT__UNKNOWN, + -1, ERROR_CODE_NULL_USER_INFO, 1); + } + + @Test + public void testRevokeAdminUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(10, UserJourneyLogger.USER_JOURNEY_REVOKE_ADMIN); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_REVOKE_ADMIN, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(0, targetUser, + UserJourneyLogger.USER_JOURNEY_REVOKE_ADMIN); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 10, + USER_LIFECYCLE_EVENT_REVOKE_ADMIN, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 2); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + UserJourneyLogger.USER_JOURNEY_REVOKE_ADMIN, 0, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000400, ERROR_CODE_UNSPECIFIED, 1); + } + + @Test + public void testSwitchFGUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(11, UserJourneyLogger.USER_JOURNEY_USER_SWITCH_FG); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserJourneyLogger.UserJourneySession session2 = mUserJourneyLogger + .logUserJourneyBegin(11, USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 2); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(11, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(10, targetUser, + USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 3); + + report2.captureAndAssert(mUserJourneyLogger, session2.mSessionId, + USER_JOURNEY_USER_START, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + + mUserJourneyLogger.logUserSwitchJourneyFinish(10, targetUser); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 4); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + UserJourneyLogger.USER_JOURNEY_USER_SWITCH_FG, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 2); + } + + + @Test + public void testSwitchUIUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(11, UserJourneyLogger.USER_JOURNEY_USER_SWITCH_UI); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + final UserJourneyLogger.UserJourneySession session2 = mUserJourneyLogger + .logUserJourneyBegin(11, USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 2); + + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + UserInfo targetUser = new UserInfo(11, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(10, targetUser, + USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 3); + + report2.captureAndAssert(mUserJourneyLogger, session2.mSessionId, + USER_JOURNEY_USER_START, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 1); + + mUserJourneyLogger.logUserSwitchJourneyFinish(10, targetUser); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_FINISH, + ERROR_CODE_UNSPECIFIED, 4); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + UserJourneyLogger.USER_JOURNEY_USER_SWITCH_UI, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 2); + } + + + @Test + public void testSwitchWithStopUIUserJourney() { + final UserLifecycleEventOccurredCaptor report1 = new UserLifecycleEventOccurredCaptor(); + + // BEGIN USER SWITCH + final UserJourneyLogger.UserJourneySession session = mUserJourneyLogger + .logUserJourneyBegin(11, UserJourneyLogger.USER_JOURNEY_USER_SWITCH_UI); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 1); + + // BEGIN USER STOP + final UserJourneyLogger.UserJourneySession session2 = mUserJourneyLogger + .logUserJourneyBegin(10, USER_JOURNEY_USER_STOP); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 2); + + // BEGIN USER START + UserJourneyLogger.UserJourneySession session3 = mUserJourneyLogger + .logUserJourneyBegin(11, USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session3.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_BEGIN, ERROR_CODE_UNSPECIFIED, 3); + + + // FINISH USER STOP + final UserLifecycleJourneyReportedCaptor report2 = new UserLifecycleJourneyReportedCaptor(); + final UserInfo targetUser = new UserInfo(10, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(-1, targetUser, + USER_JOURNEY_USER_STOP); + + report1.captureAndAssert(mUserJourneyLogger, session2.mSessionId, 10, + USER_LIFECYCLE_EVENT_STOP_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 4); + + report2.captureAndAssert(mUserJourneyLogger, session2.mSessionId, + USER_JOURNEY_USER_STOP, -1, 10, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + FULL_USER_ADMIN_FLAG, ERROR_CODE_UNSPECIFIED, 1); + + // FINISH USER START + final UserInfo targetUser2 = new UserInfo(11, "test target user", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_FULL); + mUserJourneyLogger.logUserJourneyFinish(10, targetUser2, + USER_JOURNEY_USER_START); + + report1.captureAndAssert(mUserJourneyLogger, session3.mSessionId, 11, + USER_LIFECYCLE_EVENT_START_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 5); + + report2.captureAndAssert(mUserJourneyLogger, session3.mSessionId, + USER_JOURNEY_USER_START, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + FULL_USER_ADMIN_FLAG, ERROR_CODE_UNSPECIFIED, 2); + + + // FINISH USER SWITCH + mUserJourneyLogger.logUserSwitchJourneyFinish(10, targetUser2); + + report1.captureAndAssert(mUserJourneyLogger, session.mSessionId, 11, + UserJourneyLogger.USER_LIFECYCLE_EVENT_SWITCH_USER, + EVENT_STATE_FINISH, ERROR_CODE_UNSPECIFIED, 6); + + report2.captureAndAssert(mUserJourneyLogger, session.mSessionId, + UserJourneyLogger.USER_JOURNEY_USER_SWITCH_UI, 10, 11, + FrameworkStatsLog.USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__FULL_SECONDARY, + 0x00000402, ERROR_CODE_UNSPECIFIED, 3); + } + + static class UserLifecycleJourneyReportedCaptor { + ArgumentCaptor<Long> mSessionId = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor<Integer> mJourney = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mOriginalUserId = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mTargetUserId = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mUserType = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mUserFlags = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mErrorCode = ArgumentCaptor.forClass(Integer.class); + + public void captureAndAssert(UserJourneyLogger mUserJourneyLogger, + long sessionId, int journey, int originalUserId, + int targetUserId, int userType, int userFlags, int errorCode, int times) { + verify(mUserJourneyLogger, times(times)) + .writeUserLifecycleJourneyReported(mSessionId.capture(), + mJourney.capture(), + mOriginalUserId.capture(), + mTargetUserId.capture(), + mUserType.capture(), + mUserFlags.capture(), + mErrorCode.capture()); + + assertThat(mSessionId.getValue()).isEqualTo(sessionId); + assertThat(mJourney.getValue()).isEqualTo(journey); + assertThat(mOriginalUserId.getValue()).isEqualTo(originalUserId); + assertThat(mTargetUserId.getValue()).isEqualTo(targetUserId); + assertThat(mUserType.getValue()).isEqualTo(userType); + assertThat(mUserFlags.getValue()).isEqualTo(userFlags); + assertThat(mErrorCode.getValue()).isEqualTo(errorCode); + } + + + public void captureLogAndAssert(UserJourneyLogger mUserJourneyLogger, + UserJourneyLogger.UserJourneySession session, int journey, int originalUserId, + int targetUserId, int userType, int userFlags, int errorCode) { + mUserJourneyLogger.logUserLifecycleJourneyReported(session, journey, originalUserId, + targetUserId, userType, userFlags, errorCode); + + captureAndAssert(mUserJourneyLogger, session.mSessionId, journey, originalUserId, + targetUserId, userType, userFlags, errorCode, 1); + } + } + + + static class UserLifecycleEventOccurredCaptor { + ArgumentCaptor<Long> mSessionId = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor<Integer> mTargetUserId = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mEvent = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mStste = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> mErrorCode = ArgumentCaptor.forClass(Integer.class); + + + public void captureAndAssert(UserJourneyLogger mUserJourneyLogger, + long sessionId, int targetUserId, int event, int state, int errorCode, int times) { + verify(mUserJourneyLogger, times(times)) + .writeUserLifecycleEventOccurred(mSessionId.capture(), + mTargetUserId.capture(), + mEvent.capture(), + mStste.capture(), + mErrorCode.capture()); + + assertThat(mSessionId.getValue()).isEqualTo(sessionId); + assertThat(mTargetUserId.getValue()).isEqualTo(targetUserId); + assertThat(mEvent.getValue()).isEqualTo(event); + assertThat(mStste.getValue()).isEqualTo(state); + assertThat(mErrorCode.getValue()).isEqualTo(errorCode); + } + + + public void captureLogAndAssert(UserJourneyLogger mUserJourneyLogger, + UserJourneyLogger.UserJourneySession session, int targetUserId, int event, + int state, int errorCode) { + mUserJourneyLogger.logUserLifecycleEventOccurred(session, targetUserId, event, + state, errorCode); + + captureAndAssert(mUserJourneyLogger, session.mSessionId, targetUserId, event, + state, errorCode, 1); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java index 52bf244e99fb..ae3ceb1203f8 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java @@ -20,6 +20,7 @@ import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP; import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_ERRORED; +import static android.os.PowerManager.USER_ACTIVITY_EVENT_BUTTON; import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP; import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE; import static android.os.PowerManagerInternal.WAKEFULNESS_DOZING; @@ -41,6 +42,7 @@ import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -112,6 +114,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; @@ -2468,4 +2471,18 @@ public class PowerManagerServiceTest { verify(mNotifierMock).onWakeLockReleased(anyInt(), eq(tag), eq(packageName), anyInt(), anyInt(), any(), any(), same(callback2)); } + + @Test + public void testUserActivity_futureEventsAreIgnored() { + createService(); + startSystem(); + // Starting the system triggers a user activity event, so clear that before calling + // userActivity() directly. + clearInvocations(mNotifierMock); + final long eventTime = mClock.now() + Duration.ofHours(10).toMillis(); + mService.getBinderServiceInstance().userActivity(Display.DEFAULT_DISPLAY, eventTime, + USER_ACTIVITY_EVENT_BUTTON, /* flags= */ 0); + verify(mNotifierMock, never()).onUserActivity(anyInt(), anyInt(), anyInt()); + } + } diff --git a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger/DeviceStateHandlerTest.java b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger/DeviceStateHandlerTest.java new file mode 100644 index 000000000000..089bd454bfb8 --- /dev/null +++ b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger/DeviceStateHandlerTest.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger; + +import static android.os.PowerManager.SOUND_TRIGGER_MODE_ALL_DISABLED; +import static android.os.PowerManager.SOUND_TRIGGER_MODE_ALL_ENABLED; +import static android.os.PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY; + +import static com.android.server.soundtrigger.DeviceStateHandler.SoundTriggerDeviceState; +import static com.android.server.soundtrigger.DeviceStateHandler.SoundTriggerDeviceState.*; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.SystemClock; + +import androidx.test.filters.FlakyTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.utils.EventLogger; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@RunWith(AndroidJUnit4.class) +public final class DeviceStateHandlerTest { + private final long CONFIRM_NO_EVENT_WAIT_MS = 1000; + // A wait substantially less than the duration we delay phone notifications by + private final long PHONE_DELAY_BRIEF_WAIT_MS = + DeviceStateHandler.CALL_INACTIVE_MSG_DELAY_MS / 4; + + private DeviceStateHandler mHandler; + private DeviceStateHandler.DeviceStateListener mDeviceStateCallback; + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private SoundTriggerDeviceState mState; + + @GuardedBy("mLock") + private CountDownLatch mLatch; + + private EventLogger mEventLogger; + + @Before + public void setup() { + // Reset the state prior to each test + mEventLogger = new EventLogger(256, "test logger"); + synchronized (mLock) { + mLatch = new CountDownLatch(1); + } + mDeviceStateCallback = + (SoundTriggerDeviceState state) -> { + synchronized (mLock) { + mState = state; + mLatch.countDown(); + } + }; + mHandler = new DeviceStateHandler(Runnable::run, mEventLogger); + mHandler.onPhoneCallStateChanged(false); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED); + mHandler.registerListener(mDeviceStateCallback); + try { + waitAndAssertState(ENABLE); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void waitAndAssertState(SoundTriggerDeviceState state) throws InterruptedException { + CountDownLatch latch; + synchronized (mLock) { + latch = mLatch; + } + latch.await(); + synchronized (mLock) { + assertThat(mState).isEqualTo(state); + mLatch = new CountDownLatch(1); + } + } + + private void waitToConfirmNoEventReceived() throws InterruptedException { + CountDownLatch latch; + synchronized (mLock) { + latch = mLatch; + } + // Check that we time out + assertThat(latch.await(CONFIRM_NO_EVENT_WAIT_MS, TimeUnit.MILLISECONDS)).isFalse(); + } + + @Test + public void onPowerModeChangedCritical_receiveStateChange() throws Exception { + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY); + waitAndAssertState(CRITICAL); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED); + waitAndAssertState(ENABLE); + } + + @Test + public void onPowerModeChangedDisabled_receiveStateChange() throws Exception { + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED); + waitAndAssertState(DISABLE); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED); + waitAndAssertState(ENABLE); + } + + @Test + public void onPowerModeChangedMultiple_receiveStateChange() throws Exception { + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED); + waitAndAssertState(DISABLE); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY); + waitAndAssertState(CRITICAL); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED); + waitAndAssertState(DISABLE); + } + + @Test + public void onPowerModeSameState_noStateChange() throws Exception { + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED); + waitAndAssertState(DISABLE); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED); + waitToConfirmNoEventReceived(); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY); + waitAndAssertState(CRITICAL); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY); + waitToConfirmNoEventReceived(); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED); + waitAndAssertState(ENABLE); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED); + waitToConfirmNoEventReceived(); + } + + @Test + public void onPhoneCall_receiveStateChange() throws Exception { + mHandler.onPhoneCallStateChanged(true); + waitAndAssertState(DISABLE); + mHandler.onPhoneCallStateChanged(false); + waitAndAssertState(ENABLE); + } + + @Test + public void onPhoneCall_receiveStateChangeIsDelayed() throws Exception { + mHandler.onPhoneCallStateChanged(true); + waitAndAssertState(DISABLE); + long beforeTime = SystemClock.uptimeMillis(); + mHandler.onPhoneCallStateChanged(false); + waitAndAssertState(ENABLE); + long afterTime = SystemClock.uptimeMillis(); + assertThat(afterTime - beforeTime).isAtLeast(DeviceStateHandler.CALL_INACTIVE_MSG_DELAY_MS); + } + + @Test + public void onPhoneCallEnterExitEnter_receiveNoStateChange() throws Exception { + mHandler.onPhoneCallStateChanged(true); + waitAndAssertState(DISABLE); + mHandler.onPhoneCallStateChanged(false); + SystemClock.sleep(PHONE_DELAY_BRIEF_WAIT_MS); + mHandler.onPhoneCallStateChanged(true); + waitToConfirmNoEventReceived(); + } + + @Test + public void onBatteryCallbackDuringPhoneWait_receiveStateChangeDelayed() throws Exception { + mHandler.onPhoneCallStateChanged(true); + waitAndAssertState(DISABLE); + mHandler.onPhoneCallStateChanged(false); + SystemClock.sleep(PHONE_DELAY_BRIEF_WAIT_MS); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY); + waitAndAssertState(CRITICAL); + // Ensure we don't get an ENABLE event after + waitToConfirmNoEventReceived(); + } + + @Test + public void onBatteryChangeWhenInPhoneCall_receiveNoStateChange() throws Exception { + mHandler.onPhoneCallStateChanged(true); + waitAndAssertState(DISABLE); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED); + waitToConfirmNoEventReceived(); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY); + waitToConfirmNoEventReceived(); + } + + @Test + public void whenBatteryCriticalChangeDuringCallAfterPhoneCall_receiveCriticalStateChange() + throws Exception { + mHandler.onPhoneCallStateChanged(true); + waitAndAssertState(DISABLE); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY); + waitToConfirmNoEventReceived(); + mHandler.onPhoneCallStateChanged(false); + waitAndAssertState(CRITICAL); + } + + @Test + public void whenBatteryDisableDuringCallAfterPhoneCallBatteryEnable_receiveStateChange() + throws Exception { + mHandler.onPhoneCallStateChanged(true); + waitAndAssertState(DISABLE); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED); + waitToConfirmNoEventReceived(); + mHandler.onPhoneCallStateChanged(false); + waitToConfirmNoEventReceived(); + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY); + waitAndAssertState(CRITICAL); + } + + @Test + public void whenPhoneCallDuringBatteryDisable_receiveNoStateChange() throws Exception { + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED); + waitAndAssertState(DISABLE); + mHandler.onPhoneCallStateChanged(true); + waitToConfirmNoEventReceived(); + mHandler.onPhoneCallStateChanged(false); + waitToConfirmNoEventReceived(); + } + + @Test + public void whenPhoneCallDuringBatteryCritical_receiveStateChange() throws Exception { + mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY); + waitAndAssertState(CRITICAL); + mHandler.onPhoneCallStateChanged(true); + waitAndAssertState(DISABLE); + mHandler.onPhoneCallStateChanged(false); + waitAndAssertState(CRITICAL); + } + + // This test could be flaky, but we want to verify that we only delay notification if + // we are exiting a call, NOT if we are entering a call. + @FlakyTest + @Test + public void whenPhoneCallReceived_receiveStateChangeFast() throws Exception { + mHandler.onPhoneCallStateChanged(true); + CountDownLatch latch; + synchronized (mLock) { + latch = mLatch; + } + assertThat(latch.await(PHONE_DELAY_BRIEF_WAIT_MS, TimeUnit.MILLISECONDS)).isTrue(); + synchronized (mLock) { + assertThat(mState).isEqualTo(DISABLE); + } + } +} diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/DeviceStateHandler.java b/services/voiceinteraction/java/com/android/server/soundtrigger/DeviceStateHandler.java new file mode 100644 index 000000000000..66054494c277 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/DeviceStateHandler.java @@ -0,0 +1,279 @@ +/** + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger; + +import static android.os.PowerManager.SOUND_TRIGGER_MODE_ALL_DISABLED; +import static android.os.PowerManager.SOUND_TRIGGER_MODE_ALL_ENABLED; +import static android.os.PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.utils.EventLogger; + +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * Manages device state events which require pausing SoundTrigger recognition + * + * @hide + */ +public class DeviceStateHandler implements PhoneCallStateHandler.Callback { + + public static final long CALL_INACTIVE_MSG_DELAY_MS = 1000; + + public interface DeviceStateListener { + void onSoundTriggerDeviceStateUpdate(SoundTriggerDeviceState state); + } + + public enum SoundTriggerDeviceState { + DISABLE, // The device state requires all SoundTrigger sessions are disabled + CRITICAL, // The device state requires all non-critical SoundTrigger sessions are disabled + ENABLE // The device state permits all SoundTrigger sessions + } + + private final Object mLock = new Object(); + + private final EventLogger mEventLogger; + + @GuardedBy("mLock") + SoundTriggerDeviceState mSoundTriggerDeviceState = SoundTriggerDeviceState.ENABLE; + + // Individual components of the SoundTriggerDeviceState + @GuardedBy("mLock") + private int mSoundTriggerPowerSaveMode = SOUND_TRIGGER_MODE_ALL_ENABLED; + + @GuardedBy("mLock") + private boolean mIsPhoneCallOngoing = false; + + // There can only be one pending notify at any given time. + // If any phone state change comes in between, we will cancel the previous pending + // task. + @GuardedBy("mLock") + private NotificationTask mPhoneStateChangePendingNotify = null; + + private Set<DeviceStateListener> mCallbackSet = ConcurrentHashMap.newKeySet(4); + + private final Executor mDelayedNotificationExecutor = Executors.newSingleThreadExecutor(); + + private final Executor mCallbackExecutor; + + public void onPowerModeChanged(int soundTriggerPowerSaveMode) { + mEventLogger.enqueue(new SoundTriggerPowerEvent(soundTriggerPowerSaveMode)); + synchronized (mLock) { + if (soundTriggerPowerSaveMode == mSoundTriggerPowerSaveMode) { + // No state change, nothing to do + return; + } + mSoundTriggerPowerSaveMode = soundTriggerPowerSaveMode; + evaluateStateChange(); + } + } + + @Override + public void onPhoneCallStateChanged(boolean isInPhoneCall) { + mEventLogger.enqueue(new PhoneCallEvent(isInPhoneCall)); + synchronized (mLock) { + if (mIsPhoneCallOngoing == isInPhoneCall) { + // no change, nothing to do + return; + } + // Clear any pending notification + if (mPhoneStateChangePendingNotify != null) { + mPhoneStateChangePendingNotify.cancel(); + mPhoneStateChangePendingNotify = null; + } + mIsPhoneCallOngoing = isInPhoneCall; + if (!mIsPhoneCallOngoing) { + // State has changed from call to no call, delay notification + mPhoneStateChangePendingNotify = new NotificationTask( + new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (mPhoneStateChangePendingNotify != null && + mPhoneStateChangePendingNotify.runnableEquals(this)) { + + mPhoneStateChangePendingNotify = null; + evaluateStateChange(); + } + } + } + }, + CALL_INACTIVE_MSG_DELAY_MS); + mDelayedNotificationExecutor.execute(mPhoneStateChangePendingNotify); + } else { + evaluateStateChange(); + } + } + } + + /** Note, we expect initial callbacks immediately following construction */ + public DeviceStateHandler(Executor callbackExecutor, EventLogger eventLogger) { + mCallbackExecutor = Objects.requireNonNull(callbackExecutor); + mEventLogger = Objects.requireNonNull(eventLogger); + } + + public SoundTriggerDeviceState getDeviceState() { + synchronized (mLock) { + return mSoundTriggerDeviceState; + } + } + + public void registerListener(DeviceStateListener callback) { + final var state = getDeviceState(); + mCallbackExecutor.execute( + () -> callback.onSoundTriggerDeviceStateUpdate(state)); + mCallbackSet.add(callback); + } + + public void unregisterListener(DeviceStateListener callback) { + mCallbackSet.remove(callback); + } + + void dump(PrintWriter pw) { + synchronized (mLock) { + pw.println("DeviceState: " + mSoundTriggerDeviceState.name()); + pw.println("PhoneState: " + mIsPhoneCallOngoing); + pw.println("PowerSaveMode: " + mSoundTriggerPowerSaveMode); + } + } + + @GuardedBy("mLock") + private void evaluateStateChange() { + // We should wait until any pending delays are complete to update. + // We will eventually get called by the notification task, or something which + // cancels it. + // Additionally, if there isn't a state change, there is nothing to update. + SoundTriggerDeviceState newState = computeState(); + if (mPhoneStateChangePendingNotify != null || mSoundTriggerDeviceState == newState) { + return; + } + + mSoundTriggerDeviceState = newState; + mEventLogger.enqueue(new DeviceStateEvent(mSoundTriggerDeviceState)); + final var state = mSoundTriggerDeviceState; + for (var callback : mCallbackSet) { + mCallbackExecutor.execute( + () -> callback.onSoundTriggerDeviceStateUpdate(state)); + } + } + + @GuardedBy("mLock") + private SoundTriggerDeviceState computeState() { + if (mIsPhoneCallOngoing) { + return SoundTriggerDeviceState.DISABLE; + } + return switch (mSoundTriggerPowerSaveMode) { + case SOUND_TRIGGER_MODE_ALL_ENABLED -> SoundTriggerDeviceState.ENABLE; + case SOUND_TRIGGER_MODE_CRITICAL_ONLY -> SoundTriggerDeviceState.CRITICAL; + case SOUND_TRIGGER_MODE_ALL_DISABLED -> SoundTriggerDeviceState.DISABLE; + default -> throw new IllegalStateException( + "Received unexpected power state code" + mSoundTriggerPowerSaveMode); + }; + } + + /** + * One-shot, cancellable task which runs after a delay. Run must only be called once, from a + * single thread. Cancel can be called from any other thread. + */ + private static class NotificationTask implements Runnable { + private final Runnable mRunnable; + private final long mWaitInMillis; + + private final CountDownLatch mCancelLatch = new CountDownLatch(1); + + NotificationTask(Runnable r, long waitInMillis) { + mRunnable = r; + mWaitInMillis = waitInMillis; + } + + void cancel() { + mCancelLatch.countDown(); + } + + // Used for determining task equality. + boolean runnableEquals(Runnable runnable) { + return mRunnable == runnable; + } + + public void run() { + try { + if (!mCancelLatch.await(mWaitInMillis, TimeUnit.MILLISECONDS)) { + mRunnable.run(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError("Unexpected InterruptedException", e); + } + } + } + + private static class PhoneCallEvent extends EventLogger.Event { + final boolean mIsInPhoneCall; + + PhoneCallEvent(boolean isInPhoneCall) { + mIsInPhoneCall = isInPhoneCall; + } + + @Override + public String eventToString() { + return "PhoneCallChange - inPhoneCall: " + mIsInPhoneCall; + } + } + + private static class SoundTriggerPowerEvent extends EventLogger.Event { + final int mSoundTriggerPowerState; + + SoundTriggerPowerEvent(int soundTriggerPowerState) { + mSoundTriggerPowerState = soundTriggerPowerState; + } + + @Override + public String eventToString() { + return "SoundTriggerPowerChange: " + stateToString(); + } + + private String stateToString() { + return switch (mSoundTriggerPowerState) { + case SOUND_TRIGGER_MODE_ALL_ENABLED -> "All enabled"; + case SOUND_TRIGGER_MODE_CRITICAL_ONLY -> "Critical only"; + case SOUND_TRIGGER_MODE_ALL_DISABLED -> "All disabled"; + default -> "Unknown power state: " + mSoundTriggerPowerState; + }; + } + } + + private static class DeviceStateEvent extends EventLogger.Event { + final SoundTriggerDeviceState mSoundTriggerDeviceState; + + DeviceStateEvent(SoundTriggerDeviceState soundTriggerDeviceState) { + mSoundTriggerDeviceState = soundTriggerDeviceState; + } + + @Override + public String eventToString() { + return "DeviceStateChange: " + mSoundTriggerDeviceState.name(); + } + } +} diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java new file mode 100644 index 000000000000..8773cabeeb92 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java @@ -0,0 +1,158 @@ +/** + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger; + +import android.telephony.Annotation; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Handles monitoring telephony call state across active subscriptions. + * + * @hide + */ +public class PhoneCallStateHandler { + + public interface Callback { + void onPhoneCallStateChanged(boolean isInPhoneCall); + } + + private final Object mLock = new Object(); + + // Actually never contended due to executor. + @GuardedBy("mLock") + private final List<MyCallStateListener> mListenerList = new ArrayList<>(); + + private final AtomicBoolean mIsPhoneCallOngoing = new AtomicBoolean(false); + + private final SubscriptionManager mSubscriptionManager; + private final TelephonyManager mTelephonyManager; + private final Callback mCallback; + + private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + + public PhoneCallStateHandler( + SubscriptionManager subscriptionManager, + TelephonyManager telephonyManager, + Callback callback) { + mSubscriptionManager = Objects.requireNonNull(subscriptionManager); + mTelephonyManager = Objects.requireNonNull(telephonyManager); + mCallback = Objects.requireNonNull(callback); + mSubscriptionManager.addOnSubscriptionsChangedListener( + mExecutor, + new SubscriptionManager.OnSubscriptionsChangedListener() { + @Override + public void onSubscriptionsChanged() { + updateTelephonyListeners(); + } + + @Override + public void onAddListenerFailed() { + Slog.wtf( + "SoundTriggerPhoneCallStateHandler", + "Failed to add a telephony listener"); + } + }); + } + + private final class MyCallStateListener extends TelephonyCallback + implements TelephonyCallback.CallStateListener { + + final TelephonyManager mTelephonyManagerForSubId; + + // Manager corresponding to the sub-id + MyCallStateListener(TelephonyManager telephonyManager) { + mTelephonyManagerForSubId = telephonyManager; + } + + void cleanup() { + mExecutor.execute(() -> mTelephonyManagerForSubId.unregisterTelephonyCallback(this)); + } + + @Override + public void onCallStateChanged(int unused) { + updateCallStatus(); + } + } + + /** Compute the current call status, and dispatch callback if it has changed. */ + private void updateCallStatus() { + boolean callStatus = checkCallStatus(); + if (mIsPhoneCallOngoing.compareAndSet(!callStatus, callStatus)) { + mCallback.onPhoneCallStateChanged(callStatus); + } + } + + /** + * Synchronously query the current telephony call state across all subscriptions + * + * @return - {@code true} if in call, {@code false} if not in call. + */ + private boolean checkCallStatus() { + List<SubscriptionInfo> infoList = mSubscriptionManager.getActiveSubscriptionInfoList(); + if (infoList == null) return false; + return infoList.stream() + .filter(s -> (s.getSubscriptionId() != SubscriptionManager.INVALID_SUBSCRIPTION_ID)) + .anyMatch(s -> isCallOngoingFromState( + mTelephonyManager + .createForSubscriptionId(s.getSubscriptionId()) + .getCallStateForSubscription())); + } + + private void updateTelephonyListeners() { + synchronized (mLock) { + for (var listener : mListenerList) { + listener.cleanup(); + } + mListenerList.clear(); + List<SubscriptionInfo> infoList = mSubscriptionManager.getActiveSubscriptionInfoList(); + if (infoList == null) return; + infoList.stream() + .filter(s -> s.getSubscriptionId() + != SubscriptionManager.INVALID_SUBSCRIPTION_ID) + .map(s -> mTelephonyManager.createForSubscriptionId(s.getSubscriptionId())) + .forEach(manager -> { + synchronized (mLock) { + var listener = new MyCallStateListener(manager); + mListenerList.add(listener); + manager.registerTelephonyCallback(mExecutor, listener); + } + }); + } + } + + private static boolean isCallOngoingFromState(@Annotation.CallState int callState) { + return switch (callState) { + case TelephonyManager.CALL_STATE_IDLE, TelephonyManager.CALL_STATE_RINGING -> false; + case TelephonyManager.CALL_STATE_OFFHOOK -> true; + default -> throw new IllegalStateException( + "Received unexpected call state from Telephony Manager: " + callState); + }; + } +} diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java index 255db1e4db83..b4066ab1ff39 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java @@ -16,15 +16,12 @@ package com.android.server.soundtrigger; +import static com.android.server.soundtrigger.DeviceStateHandler.SoundTriggerDeviceState; import static com.android.server.soundtrigger.SoundTriggerEvent.SessionEvent.Type; import static com.android.server.utils.EventLogger.Event.ALOGW; - import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.hardware.soundtrigger.IRecognitionStatusCallback; import android.hardware.soundtrigger.ModelParams; import android.hardware.soundtrigger.SoundTrigger; @@ -45,11 +42,7 @@ import android.os.DeadObjectException; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.PowerManager; -import android.os.PowerManager.SoundTriggerPowerSaveMode; import android.os.RemoteException; -import android.telephony.PhoneStateListener; -import android.telephony.TelephonyManager; import android.util.Slog; import com.android.internal.annotations.GuardedBy; @@ -99,37 +92,20 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { private SoundTriggerModule mModule; private final Object mLock = new Object(); private final Context mContext; - private final TelephonyManager mTelephonyManager; - private final PhoneStateListener mPhoneStateListener; - private final PowerManager mPowerManager; // The SoundTriggerManager layer handles multiple recognition models of type generic and // keyphrase. We store the ModelData here in a hashmap. - private final HashMap<UUID, ModelData> mModelDataMap; + private final HashMap<UUID, ModelData> mModelDataMap = new HashMap<>(); // An index of keyphrase sound models so that we can reach them easily. We support indexing // keyphrase sound models with a keyphrase ID. Sound model with the same keyphrase ID will // replace an existing model, thus there is a 1:1 mapping from keyphrase ID to a voice // sound model. - private HashMap<Integer, UUID> mKeyphraseUuidMap; - - private boolean mCallActive = false; - private @SoundTriggerPowerSaveMode int mSoundTriggerPowerSaveMode = - PowerManager.SOUND_TRIGGER_MODE_ALL_ENABLED; + private final HashMap<Integer, UUID> mKeyphraseUuidMap = new HashMap<>(); // Whether ANY recognition (keyphrase or generic) has been requested. private boolean mRecognitionRequested = false; - private PowerSaveModeListener mPowerSaveModeListener; - - - // Handler to process call state changes will delay to allow time for the audio - // and sound trigger HALs to process the end of call notifications - // before we re enable pending recognition requests. - private final Handler mHandler; - private static final int MSG_CALL_STATE_CHANGED = 0; - private static final int CALL_INACTIVE_MSG_DELAY_MS = 1000; - // TODO(b/269366605) Temporary solution to query correct moduleProperties private final int mModuleId; private final Function<SoundTrigger.StatusListener, SoundTriggerModule> mModuleProvider; @@ -139,16 +115,15 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { @GuardedBy("mLock") private boolean mIsDetached = false; + @GuardedBy("mLock") + private SoundTriggerDeviceState mDeviceState = SoundTriggerDeviceState.DISABLE; + SoundTriggerHelper(Context context, EventLogger eventLogger, @NonNull Function<SoundTrigger.StatusListener, SoundTriggerModule> moduleProvider, int moduleId, @NonNull Supplier<List<ModuleProperties>> modulePropertiesProvider) { mModuleId = moduleId; mContext = context; - mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - mModelDataMap = new HashMap<UUID, ModelData>(); - mKeyphraseUuidMap = new HashMap<Integer, UUID>(); mModuleProvider = moduleProvider; mEventLogger = eventLogger; mModulePropertiesProvider = modulePropertiesProvider; @@ -157,31 +132,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { } else { mModule = mModuleProvider.apply(this); } - Looper looper = Looper.myLooper(); - if (looper == null) { - looper = Looper.getMainLooper(); - } - mPhoneStateListener = new MyCallStateListener(looper); - if (looper != null) { - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_CALL_STATE_CHANGED: - synchronized (mLock) { - onCallStateChangedLocked( - TelephonyManager.CALL_STATE_OFFHOOK == msg.arg1); - } - break; - default: - Slog.e(TAG, "unknown message in handler:" + msg.what); - break; - } - } - }; - } else { - mHandler = null; - } } /** @@ -373,7 +323,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { modelData.setSoundModel(soundModel); if (!isRecognitionAllowedByDeviceState(modelData)) { - initializeDeviceStateListeners(); return STATUS_OK; } @@ -497,11 +446,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { modelData.setLoaded(); modelData.clearCallback(); modelData.setRecognitionConfig(null); - - if (!computeRecognitionRequestedLocked()) { - internalClearGlobalStateLocked(); - } - return status; } } @@ -638,6 +582,17 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { } } + public void onDeviceStateChanged(SoundTriggerDeviceState state) { + synchronized (mLock) { + if (mIsDetached || mDeviceState == state) { + // Nothing to update + return; + } + mDeviceState = state; + updateAllRecognitionsLocked(); + } + } + public int getGenericModelState(UUID modelId) { synchronized (mLock) { MetricsLogger.count(mContext, "sth_get_generic_model_state", 1); @@ -880,25 +835,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { } } - private void onCallStateChangedLocked(boolean callActive) { - if (mCallActive == callActive) { - // We consider multiple call states as being active - // so we check if something really changed or not here. - return; - } - mCallActive = callActive; - updateAllRecognitionsLocked(); - } - - private void onPowerSaveModeChangedLocked( - @SoundTriggerPowerSaveMode int soundTriggerPowerSaveMode) { - if (mSoundTriggerPowerSaveMode == soundTriggerPowerSaveMode) { - return; - } - mSoundTriggerPowerSaveMode = soundTriggerPowerSaveMode; - updateAllRecognitionsLocked(); - } - private void onModelUnloadedLocked(int modelHandle) { ModelData modelData = getModelDataForLocked(modelHandle); if (modelData != null) { @@ -1011,10 +947,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { return status; } status = startRecognitionLocked(model, notifyClientOnError); - // Initialize power save, call active state monitoring logic. - if (status == STATUS_OK) { - initializeDeviceStateListeners(); - } return status; } else { return stopRecognitionLocked(model, notifyClientOnError); @@ -1040,7 +972,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { } } finally { internalClearModelStateLocked(); - internalClearGlobalStateLocked(); if (mModule != null) { mModule.detach(); try { @@ -1054,24 +985,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { } } - // internalClearGlobalStateLocked() cleans up the telephony and power save listeners. - private void internalClearGlobalStateLocked() { - // Unregister from call state changes. - final long token = Binder.clearCallingIdentity(); - try { - mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); - } finally { - Binder.restoreCallingIdentity(token); - } - - // Unregister from power save mode changes. - if (mPowerSaveModeListener != null) { - mContext.unregisterReceiver(mPowerSaveModeListener); - mPowerSaveModeListener = null; - } - mRecognitionRequested = false; - } - // Clears state for all models (generic and keyphrase). private void internalClearModelStateLocked() { for (ModelData modelData : mModelDataMap.values()) { @@ -1079,67 +992,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { } } - class MyCallStateListener extends PhoneStateListener { - MyCallStateListener(@NonNull Looper looper) { - super(Objects.requireNonNull(looper)); - } - - @Override - public void onCallStateChanged(int state, String arg1) { - - if (mHandler != null) { - synchronized (mLock) { - mHandler.removeMessages(MSG_CALL_STATE_CHANGED); - Message msg = mHandler.obtainMessage(MSG_CALL_STATE_CHANGED, state, 0); - mHandler.sendMessageDelayed( - msg, (TelephonyManager.CALL_STATE_OFFHOOK == state) ? 0 - : CALL_INACTIVE_MSG_DELAY_MS); - } - } - } - } - - class PowerSaveModeListener extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (!PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(intent.getAction())) { - return; - } - @SoundTriggerPowerSaveMode int soundTriggerPowerSaveMode = - mPowerManager.getSoundTriggerPowerSaveMode(); - synchronized (mLock) { - onPowerSaveModeChangedLocked(soundTriggerPowerSaveMode); - } - } - } - - private void initializeDeviceStateListeners() { - if (mRecognitionRequested) { - return; - } - final long token = Binder.clearCallingIdentity(); - try { - // Get the current call state synchronously for the first recognition. - mCallActive = mTelephonyManager.getCallState() == TelephonyManager.CALL_STATE_OFFHOOK; - - // Register for call state changes when the first call to start recognition occurs. - mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - - // Register for power saver mode changes when the first call to start recognition - // occurs. - if (mPowerSaveModeListener == null) { - mPowerSaveModeListener = new PowerSaveModeListener(); - mContext.registerReceiver(mPowerSaveModeListener, - new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)); - } - mSoundTriggerPowerSaveMode = mPowerManager.getSoundTriggerPowerSaveMode(); - - mRecognitionRequested = true; - } finally { - Binder.restoreCallingIdentity(token); - } - } - /** * Stops and unloads all models. This is intended as a clean-up call with the expectation that * this instance is not used after. @@ -1153,7 +1005,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { forceStopAndUnloadModelLocked(model, null); } mModelDataMap.clear(); - internalClearGlobalStateLocked(); if (mModule != null) { mModule.detach(); mModule = null; @@ -1305,28 +1156,14 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { * @param modelData Model data to be used for recognition * @return True if recognition is allowed to run at this time. False if not. */ + @GuardedBy("mLock") private boolean isRecognitionAllowedByDeviceState(ModelData modelData) { - // if mRecognitionRequested is false, call and power state listeners are not registered so - // we read current state directly from services - if (!mRecognitionRequested) { - mCallActive = mTelephonyManager.getCallState() == TelephonyManager.CALL_STATE_OFFHOOK; - mSoundTriggerPowerSaveMode = mPowerManager.getSoundTriggerPowerSaveMode(); - } - - return !mCallActive && isRecognitionAllowedByPowerState( - modelData); - } - - /** - * Helper function to validate if a recognition should run based on the current power state - * - * @param modelData Model data to be used for recognition - * @return True if device state allows recognition to run, false if not. - */ - private boolean isRecognitionAllowedByPowerState(ModelData modelData) { - return mSoundTriggerPowerSaveMode == PowerManager.SOUND_TRIGGER_MODE_ALL_ENABLED - || (mSoundTriggerPowerSaveMode == PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY - && modelData.shouldRunInBatterySaverMode()); + return switch (mDeviceState) { + case DISABLE -> false; + case CRITICAL -> modelData.shouldRunInBatterySaverMode(); + case ENABLE -> true; + default -> throw new AssertionError("Enum changed between compile and runtime"); + }; } // A single routine that implements the start recognition logic for both generic and keyphrase diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java index 913535e06a21..3151781ff7ba 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java @@ -35,14 +35,18 @@ import static com.android.server.soundtrigger.SoundTriggerEvent.SessionEvent.Typ import static com.android.server.utils.EventLogger.Event.ALOGW; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; +import static com.android.server.soundtrigger.DeviceStateHandler.DeviceStateListener; +import static com.android.server.soundtrigger.DeviceStateHandler.SoundTriggerDeviceState; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityThread; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.PermissionChecker; import android.content.ServiceConnection; import android.content.pm.PackageManager; @@ -85,6 +89,8 @@ import android.os.ServiceSpecificException; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; import android.util.ArrayMap; import android.util.ArraySet; import android.util.SparseArray; @@ -112,6 +118,8 @@ import java.util.Objects; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -131,8 +139,8 @@ public class SoundTriggerService extends SystemService { private static final boolean DEBUG = true; private static final int SESSION_MAX_EVENT_SIZE = 128; - final Context mContext; - private Object mLock; + private final Context mContext; + private final Object mLock = new Object(); private final SoundTriggerServiceStub mServiceStub; private final LocalSoundTriggerService mLocalSoundTriggerService; @@ -140,6 +148,7 @@ public class SoundTriggerService extends SystemService { private SoundTriggerDbHelper mDbHelper; private final EventLogger mServiceEventLogger = new EventLogger(256, "Service"); + private final EventLogger mDeviceEventLogger = new EventLogger(256, "Device Event"); private final Set<EventLogger> mSessionEventLoggers = ConcurrentHashMap.newKeySet(4); private final Deque<EventLogger> mDetachedSessionEventLoggers = new LinkedBlockingDeque<>(4); @@ -223,13 +232,18 @@ public class SoundTriggerService extends SystemService { @GuardedBy("mLock") private final ArrayMap<String, NumOps> mNumOpsPerPackage = new ArrayMap<>(); + private final DeviceStateHandler mDeviceStateHandler; + private final Executor mDeviceStateHandlerExecutor = Executors.newSingleThreadExecutor(); + private PhoneCallStateHandler mPhoneCallStateHandler; + public SoundTriggerService(Context context) { super(context); mContext = context; mServiceStub = new SoundTriggerServiceStub(); mLocalSoundTriggerService = new LocalSoundTriggerService(context); - mLock = new Object(); mSoundModelStatTracker = new SoundModelStatTracker(); + mDeviceStateHandler = new DeviceStateHandler(mDeviceStateHandlerExecutor, + mDeviceEventLogger); } @Override @@ -243,6 +257,29 @@ public class SoundTriggerService extends SystemService { Slog.d(TAG, "onBootPhase: " + phase + " : " + isSafeMode()); if (PHASE_THIRD_PARTY_APPS_CAN_START == phase) { mDbHelper = new SoundTriggerDbHelper(mContext); + final PowerManager powerManager = mContext.getSystemService(PowerManager.class); + // Hook up power state listener + mContext.registerReceiver( + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!PowerManager.ACTION_POWER_SAVE_MODE_CHANGED + .equals(intent.getAction())) { + return; + } + mDeviceStateHandler.onPowerModeChanged( + powerManager.getSoundTriggerPowerSaveMode()); + } + }, new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)); + // Initialize the initial power state + // Do so after registering the listener so we ensure that we don't drop any events + mDeviceStateHandler.onPowerModeChanged(powerManager.getSoundTriggerPowerSaveMode()); + + // PhoneCallStateHandler initializes the original call state + mPhoneCallStateHandler = new PhoneCallStateHandler( + mContext.getSystemService(SubscriptionManager.class), + mContext.getSystemService(TelephonyManager.class), + mDeviceStateHandler); } mMiddlewareService = ISoundTriggerMiddlewareService.Stub.asInterface( ServiceManager.waitForService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE)); @@ -380,6 +417,9 @@ public class SoundTriggerService extends SystemService { // Event loggers pw.println("##Service-Wide logs:"); mServiceEventLogger.dump(pw, /* indent = */ " "); + pw.println("\n##Device state logs:"); + mDeviceStateHandler.dump(pw); + mDeviceEventLogger.dump(pw, /* indent = */ " "); pw.println("\n##Active Session dumps:\n"); for (var sessionLogger : mSessionEventLoggers) { @@ -403,6 +443,7 @@ public class SoundTriggerService extends SystemService { class SoundTriggerSessionStub extends ISoundTriggerSession.Stub { private final SoundTriggerHelper mSoundTriggerHelper; + private final DeviceStateListener mListener; // Used to detect client death. private final IBinder mClient; private final Identity mOriginatorIdentity; @@ -424,6 +465,9 @@ public class SoundTriggerService extends SystemService { } catch (RemoteException e) { clientDied(); } + mListener = (SoundTriggerDeviceState state) + -> mSoundTriggerHelper.onDeviceStateChanged(state); + mDeviceStateHandler.registerListener(mListener); } @Override @@ -874,6 +918,7 @@ public class SoundTriggerService extends SystemService { } private void detach() { + mDeviceStateHandler.unregisterListener(mListener); mSoundTriggerHelper.detach(); detachSessionLogger(mEventLogger); } @@ -890,7 +935,8 @@ public class SoundTriggerService extends SystemService { private void enforceDetectionPermissions(ComponentName detectionService) { PackageManager packageManager = mContext.getPackageManager(); String packageName = detectionService.getPackageName(); - if (packageManager.checkPermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD, packageName) + if (packageManager.checkPermission( + Manifest.permission.CAPTURE_AUDIO_HOTWORD, packageName) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException(detectionService.getPackageName() + " does not have" + " permission " + Manifest.permission.CAPTURE_AUDIO_HOTWORD); @@ -1576,6 +1622,7 @@ public class SoundTriggerService extends SystemService { private final @NonNull IBinder mClient; private final EventLogger mEventLogger; private final Identity mOriginatorIdentity; + private final @NonNull DeviceStateListener mListener; private final SparseArray<UUID> mModelUuid = new SparseArray<>(1); @@ -1594,6 +1641,9 @@ public class SoundTriggerService extends SystemService { } catch (RemoteException e) { clientDied(); } + mListener = (SoundTriggerDeviceState state) + -> mSoundTriggerHelper.onDeviceStateChanged(state); + mDeviceStateHandler.registerListener(mListener); } @Override @@ -1662,6 +1712,7 @@ public class SoundTriggerService extends SystemService { private void detachInternal() { mEventLogger.enqueue(new SessionEvent(Type.DETACH, null)); detachSessionLogger(mEventLogger); + mDeviceStateHandler.unregisterListener(mListener); mSoundTriggerHelper.detach(); } } |