diff options
50 files changed, 1617 insertions, 260 deletions
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index baed7f9b2a38..39800f73058b 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -637,15 +637,15 @@ public final class VirtualDeviceManager { /** * Specifies a component name to be exempt from the current activity launch policy. * - * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVIY} allows activity - * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}, + * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity + * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}), * then the specified component will be blocked from launching. - * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity - * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}, then - * the specified component will be allowed to launch.</p> + * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches + * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then the + * specified component will be allowed to launch.</p> * - * <p>Note that changing the activity launch policy will not affect current set of exempt - * components and it needs to be updated separately.</p> + * <p>Note that changing the activity launch policy will clear current set of exempt + * components.</p> * * @see #removeActivityPolicyExemption * @see #setDevicePolicy @@ -660,15 +660,15 @@ public final class VirtualDeviceManager { /** * Makes the specified component name to adhere to the default activity launch policy. * - * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVIY} allows activity - * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}, + * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} allows activity + * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT}), * then the specified component will be allowed to launch. - * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity - * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}, then - * the specified component will be blocked from launching.</p> + * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity launches + * by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}), then the + * specified component will be blocked from launching.</p> * - * <p>Note that changing the activity launch policy will not affect current set of exempt - * components and it needs to be updated separately.</p> + * <p>Note that changing the activity launch policy will clear current set of exempt + * components.</p> * * @see #addActivityPolicyExemption * @see #setDevicePolicy diff --git a/core/java/android/service/controls/Control.java b/core/java/android/service/controls/Control.java index 3b757d6e3dd3..33978be9fb82 100644 --- a/core/java/android/service/controls/Control.java +++ b/core/java/android/service/controls/Control.java @@ -50,7 +50,7 @@ import java.lang.annotation.RetentionPolicy; * and zone. Some of these values are defined by the user and/or the {@link ControlsProviderService} * and will be used to display the control as well as group them for management. * <p> - * Each object will have an associated {@link DeviceTypes.DeviceType}. This will determine the icons and colors + * Each object will have an associated {@link DeviceTypes}. This will determine the icons and colors * used to display it. * <p> * An {@link Intent} linking to the provider Activity that expands on this {@link Control} and @@ -420,7 +420,7 @@ public final class Control implements Parcelable { * This fixes the values relating to state of the {@link Control} as required by * {@link ControlsProviderService#createPublisherForAllAvailable}: * <ul> - * <li> Status: {@link Status#STATUS_UNKNOWN} + * <li> Status: {@link #STATUS_UNKNOWN} * <li> Control template: {@link ControlTemplate#getNoTemplateObject} * <li> Status text: {@code ""} * <li> Auth Required: {@code true} @@ -620,7 +620,7 @@ public final class Control implements Parcelable { * <li> Device type: {@link DeviceTypes#TYPE_UNKNOWN} * <li> Title: {@code ""} * <li> Subtitle: {@code ""} - * <li> Status: {@link Status#STATUS_UNKNOWN} + * <li> Status: {@link #STATUS_UNKNOWN} * <li> Control template: {@link ControlTemplate#getNoTemplateObject} * <li> Status text: {@code ""} * <li> Auth Required: {@code true} diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java index fce87db2bbf0..0272bb9842d6 100644 --- a/core/java/android/service/controls/ControlsProviderService.java +++ b/core/java/android/service/controls/ControlsProviderService.java @@ -155,7 +155,7 @@ public abstract class ControlsProviderService extends Service { * The user has interacted with a Control. The action is dictated by the type of * {@link ControlAction} that was sent. A response can be sent via * {@link Consumer#accept}, with the Integer argument being one of the provided - * {@link ControlAction.ResponseResult}. The Integer should indicate whether the action + * {@link ControlAction} response results. The Integer should indicate whether the action * was received successfully, or if additional prompts should be presented to * the user. Any visual control updates should be sent via the Publisher. diff --git a/core/java/android/service/controls/actions/ControlAction.java b/core/java/android/service/controls/actions/ControlAction.java index 10f526d6565c..4e382222547d 100644 --- a/core/java/android/service/controls/actions/ControlAction.java +++ b/core/java/android/service/controls/actions/ControlAction.java @@ -154,7 +154,7 @@ public abstract class ControlAction { public static final @ResponseResult int RESPONSE_CHALLENGE_PASSPHRASE = 5; /** - * The {@link ActionType} associated with this class. + * The action type associated with this class. */ public abstract @ActionType int getActionType(); diff --git a/core/java/android/service/controls/templates/ControlTemplate.java b/core/java/android/service/controls/templates/ControlTemplate.java index 3902d6af69e7..0dd950d596f6 100644 --- a/core/java/android/service/controls/templates/ControlTemplate.java +++ b/core/java/android/service/controls/templates/ControlTemplate.java @@ -137,7 +137,7 @@ public abstract class ControlTemplate { } /** - * The {@link TemplateType} associated with this class. + * The template type associated with this class. */ public abstract @TemplateType int getTemplateType(); diff --git a/core/java/android/service/watchdog/ExplicitHealthCheckService.java b/core/java/android/service/watchdog/ExplicitHealthCheckService.java index 49e00d6f6328..7befbfb0f370 100644 --- a/core/java/android/service/watchdog/ExplicitHealthCheckService.java +++ b/core/java/android/service/watchdog/ExplicitHealthCheckService.java @@ -151,7 +151,7 @@ public abstract class ExplicitHealthCheckService extends Service { */ @NonNull public abstract List<String> onGetRequestedPackages(); - private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true); + private final Handler mHandler = Handler.createAsync(Looper.getMainLooper()); @Nullable private RemoteCallback mCallback; @Override diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index cdf5eec32fec..70e18963d921 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -2197,6 +2197,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { float xOffset, float yOffset, float xPrecision, float yPrecision, long downTimeNanos, long eventTimeNanos, int pointerCount, PointerProperties[] pointerIds, PointerCoords[] pointerCoords) { + if (action == ACTION_CANCEL) { + flags |= FLAG_CANCELED; + } mNativePtr = nativeInitialize(mNativePtr, deviceId, source, displayId, action, flags, edgeFlags, metaState, buttonState, classification, xOffset, yOffset, xPrecision, yPrecision, downTimeNanos, eventTimeNanos, pointerCount, pointerIds, @@ -2387,6 +2390,11 @@ public final class MotionEvent extends InputEvent implements Parcelable { nativeSetFlags(mNativePtr, tainted ? flags | FLAG_TAINTED : flags & ~FLAG_TAINTED); } + private void setCanceled(boolean canceled) { + final int flags = getFlags(); + nativeSetFlags(mNativePtr, canceled ? flags | FLAG_CANCELED : flags & ~FLAG_CANCELED); + } + /** @hide */ public boolean isTargetAccessibilityFocus() { final int flags = getFlags(); @@ -3510,6 +3518,14 @@ public final class MotionEvent extends InputEvent implements Parcelable { * Sets this event's action. */ public final void setAction(int action) { + final int actionMasked = action & ACTION_MASK; + if (actionMasked == ACTION_CANCEL) { + setCanceled(true); + } else if (actionMasked == ACTION_POINTER_UP) { + // Do nothing - we don't know what the real intent here is + } else { + setCanceled(false); + } nativeSetAction(mNativePtr, action); } @@ -4157,6 +4173,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { /** @hide */ @Override public final void cancel() { + setCanceled(true); setAction(ACTION_CANCEL); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt index 67dc642a2deb..e1dea3babbc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt @@ -25,7 +25,8 @@ object ProdBubbleProperties : BubbleProperties { private var _isBubbleBarEnabled = SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false) - override val isBubbleBarEnabled = _isBubbleBarEnabled + override val isBubbleBarEnabled + get() = _isBubbleBarEnabled override fun refresh() { _isBubbleBarEnabled = SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false) diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 1943b340b9b1..67531ad9926a 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -30,6 +30,8 @@ import com.android.systemui.log.LogcatEchoTrackerProd; import com.android.systemui.log.table.TableLogBuffer; import com.android.systemui.log.table.TableLogBufferFactory; import com.android.systemui.qs.QSFragmentLegacy; +import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository; +import com.android.systemui.qs.pipeline.shared.TileSpec; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.util.Compile; import com.android.systemui.util.wakelock.WakeLockLog; @@ -37,6 +39,9 @@ import com.android.systemui.util.wakelock.WakeLockLog; import dagger.Module; import dagger.Provides; +import java.util.HashMap; +import java.util.Map; + /** * Dagger module for providing instances of {@link LogBuffer}. */ @@ -173,8 +178,35 @@ public class LogModule { @Provides @SysUISingleton @QSLog - public static LogBuffer provideQuickSettingsLogBuffer(LogBufferFactory factory) { - return factory.create("QSLog", 700 /* maxSize */, false /* systrace */); + public static LogBuffer provideQuickSettingsLogBuffer( + LogBufferFactory factory, + QSPipelineFlagsRepository flags + ) { + if (flags.getPipelineTilesEnabled()) { + // we use + return factory.create("QSLog", 450 /* maxSize */, false /* systrace */); + } else { + return factory.create("QSLog", 700 /* maxSize */, false /* systrace */); + } + } + + /** + * Provides a logging buffer for all logs related to Quick Settings tiles. This LogBuffer is + * unique for each tile. + * go/qs-tile-refactor + */ + @Provides + @QSTilesDefaultLog + public static LogBuffer provideQuickSettingsTilesLogBuffer(LogBufferFactory factory) { + return factory.create("QSTileLog", 25 /* maxSize */, false /* systrace */); + } + + @Provides + @QSTilesLogBuffers + public static Map<TileSpec, LogBuffer> provideQuickSettingsTilesLogBufferCache() { + final Map<TileSpec, LogBuffer> buffers = new HashMap<>(); + // Add chatty buffers here + return buffers; } /** Provides a logging buffer for logs related to Quick Settings configuration. */ @@ -420,7 +452,7 @@ public class LogModule { /** * Provides a {@link LogBuffer} for use by - * {@link com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepositoryImpl}. + * {@link com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepositoryImpl}. */ @Provides @SysUISingleton @@ -431,7 +463,7 @@ public class LogModule { /** * Provides a {@link LogBuffer} for use by classes in the - * {@link com.android.systemui.keyguard.bouncer} package. + * {@link com.android.systemui.keyguard.bouncer} package. */ @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesDefaultLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesDefaultLog.kt new file mode 100644 index 000000000000..6575cdd69c93 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesDefaultLog.kt @@ -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.systemui.log.dagger + +import javax.inject.Qualifier + +/** + * A default [com.android.systemui.log.LogBuffer] for QS tiles messages. It's used exclusively in + * [com.android.systemui.qs.tiles.base.logging.QSTileLogger]. If you need to increase it for you + * tile, add one to the map provided by the [QSTilesLogBuffers] + */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class QSTilesDefaultLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesLogBuffers.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesLogBuffers.kt new file mode 100644 index 000000000000..62d49fefeb6a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesLogBuffers.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.log.dagger + +import javax.inject.Qualifier + +/** + * Provides a map with custom [com.android.systemui.log.LogBuffer] for QS tiles messages. Add + * buffers to it when the tile needs to be more verbose and the default buffer provided by + * [QSTilesDefaultLog] is not enough. + * + * This is not a multibinding. Add new logs directly to [LogModule] + */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class QSTilesLogBuffers diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesVerboseLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesVerboseLog.java new file mode 100644 index 000000000000..b0c2f8c59deb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSTilesVerboseLog.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.dagger; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.android.systemui.log.LogBuffer; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +/** + * A {@link LogBuffer} for QS tiles messages. It's used exclusively in + * {@link com.android.systemui.qs.tiles.base.logging.QSTileLogger} + */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface QSTilesVerboseLog { +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 19012e29b184..fa18b35b215e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -27,11 +27,11 @@ import androidx.viewpager.widget.ViewPager; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; -import com.android.systemui.res.R; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.QSPanel.QSTileLayout; import com.android.systemui.qs.QSPanelControllerBase.TileRecord; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.res.R; import java.util.ArrayList; import java.util.List; @@ -562,6 +562,12 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { if (shouldNotRunAnimation(tilesToReveal)) { return; } + // This method has side effects (beings the fake drag, if it returns true). If we have + // decided that we want to do a tile reveal, we do a last check to verify that we can + // actually perform a fake drag. + if (!beginFakeDrag()) { + return; + } final int lastPageNumber = mPages.size() - 1; final TileLayout lastPage = mPages.get(lastPageNumber); @@ -596,8 +602,10 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { } private boolean shouldNotRunAnimation(Set<String> tilesToReveal) { + // None of these have side effects. That way, we don't need to rely on short-circuiting + // behavior boolean noAnimationNeeded = tilesToReveal.isEmpty() || mPages.size() < 2; - boolean scrollingInProgress = getScrollX() != 0 || !beginFakeDrag(); + boolean scrollingInProgress = getScrollX() != 0 || !isFakeDragging(); // checking mRunningInTestHarness to disable animation in functional testing as it caused // flakiness and is not needed there. Alternative solutions were more complex and would // still be either potentially flaky or modify internal data. diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index 128c23745e5f..051eeb0cfaf1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -72,7 +72,8 @@ import dagger.assisted.Assisted; import dagger.assisted.AssistedFactory; import dagger.assisted.AssistedInject; -public class CustomTile extends QSTileImpl<State> implements TileChangeListener { +public class CustomTile extends QSTileImpl<State> implements TileChangeListener, + CustomTileInterface { public static final String PREFIX = "custom("; private static final long CUSTOM_STALE_TIMEOUT = DateUtils.HOUR_IN_MILLIS; @@ -181,7 +182,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener private void updateDefaultTileAndIcon() { try { PackageManager pm = mUserContext.getPackageManager(); - int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_AWARE; + int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_DIRECT_BOOT_AWARE; if (isSystemApp(pm)) { flags |= PackageManager.MATCH_DISABLED_COMPONENTS; } @@ -213,7 +215,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener * Compare two icons, only works for resources. */ private boolean iconEquals(@Nullable android.graphics.drawable.Icon icon1, - @Nullable android.graphics.drawable.Icon icon2) { + @Nullable android.graphics.drawable.Icon icon2) { if (icon1 == icon2) { return true; } @@ -252,10 +254,12 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener } } + @Override public int getUser() { return mUser; } + @Override public ComponentName getComponent() { return mComponent; } @@ -265,6 +269,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener return super.populate(logMaker).setComponentName(mComponent); } + @Override public Tile getQsTile() { // TODO(b/191145007) Move to background thread safely updateDefaultTileAndIcon(); @@ -276,6 +281,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener * * @param tile tile populated with state to apply */ + @Override public void updateTileState(Tile tile, int appUid) { mServiceUid = appUid; // This comes from a binder call IQSService.updateQsTile @@ -310,10 +316,12 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener mTile.setState(tile.getState()); } + @Override public void onDialogShown() { mIsShowingDialog = true; } + @Override public void onDialogHidden() { mIsShowingDialog = false; try { @@ -507,6 +515,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener return mComponent.getPackageName(); } + @Override public void startUnlockAndRun() { mActivityStarter.postQSRunnableDismissingKeyguard(() -> { try { @@ -518,8 +527,10 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener /** * Starts an {@link android.app.Activity} + * * @param pendingIntent A PendingIntent for an Activity to be launched immediately. */ + @Override public void startActivityAndCollapse(PendingIntent pendingIntent) { if (!pendingIntent.isActivity()) { Log.i(TAG, "Intent not for activity."); diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileInterface.kt b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileInterface.kt new file mode 100644 index 000000000000..9e023205c9fb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileInterface.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.external + +import android.app.PendingIntent +import android.content.ComponentName +import android.service.quicksettings.Tile + +interface CustomTileInterface { + + val user: Int + val qsTile: Tile + val component: ComponentName + + fun getTileSpec(): String + + fun refreshState() + fun updateTileState(tile: Tile, uid: Int) + + fun onDialogShown() + fun onDialogHidden() + + fun startActivityAndCollapse(pendingIntent: PendingIntent) + + fun startUnlockAndRun() +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java index fc2402258009..acee8e9ad2eb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java @@ -67,9 +67,10 @@ public class TileServices extends IQSService.Stub { static final int REDUCED_MAX_BOUND = 1; private static final String TAG = "TileServices"; - private final ArrayMap<CustomTile, TileServiceManager> mServices = new ArrayMap<>(); - private final SparseArrayMap<ComponentName, CustomTile> mTiles = new SparseArrayMap<>(); - private final ArrayMap<IBinder, CustomTile> mTokenMap = new ArrayMap<>(); + private final ArrayMap<CustomTileInterface, TileServiceManager> mServices = new ArrayMap<>(); + private final SparseArrayMap<ComponentName, CustomTileInterface> mTiles = + new SparseArrayMap<>(); + private final ArrayMap<IBinder, CustomTileInterface> mTokenMap = new ArrayMap<>(); private final Context mContext; private final Handler mMainHandler; private final Provider<Handler> mHandlerProvider; @@ -120,7 +121,7 @@ public class TileServices extends IQSService.Stub { return mHost; } - public TileServiceManager getTileWrapper(CustomTile tile) { + public TileServiceManager getTileWrapper(CustomTileInterface tile) { ComponentName component = tile.getComponent(); int userId = tile.getUser(); TileServiceManager service = onCreateTileService(component, mBroadcastDispatcher); @@ -140,7 +141,7 @@ public class TileServices extends IQSService.Stub { broadcastDispatcher, mUserTracker, mCustomTileAddedRepository, mBackgroundExecutor); } - public void freeService(CustomTile tile, TileServiceManager service) { + public void freeService(CustomTileInterface tile, TileServiceManager service) { synchronized (mServices) { service.setBindAllowed(false); service.handleDestroy(); @@ -184,7 +185,7 @@ public class TileServices extends IQSService.Stub { } } - private int verifyCaller(CustomTile tile) { + private int verifyCaller(CustomTileInterface tile) { try { String packageName = tile.getComponent().getPackageName(); int uid = mContext.getPackageManager().getPackageUidAsUser(packageName, @@ -201,7 +202,7 @@ public class TileServices extends IQSService.Stub { private void requestListening(ComponentName component) { synchronized (mServices) { int userId = mUserTracker.getUserId(); - CustomTile customTile = getTileForUserAndComponent(userId, component); + CustomTileInterface customTile = getTileForUserAndComponent(userId, component); if (customTile == null) { Log.d(TAG, "Couldn't find tile for " + component + "(" + userId + ")"); return; @@ -227,7 +228,7 @@ public class TileServices extends IQSService.Stub { @Override public void updateQsTile(Tile tile, IBinder token) { - CustomTile customTile = getTileForToken(token); + CustomTileInterface customTile = getTileForToken(token); if (customTile != null) { int uid = verifyCaller(customTile); synchronized (mServices) { @@ -247,7 +248,7 @@ public class TileServices extends IQSService.Stub { @Override public void onStartSuccessful(IBinder token) { - CustomTile customTile = getTileForToken(token); + CustomTileInterface customTile = getTileForToken(token); if (customTile != null) { verifyCaller(customTile); synchronized (mServices) { @@ -267,7 +268,7 @@ public class TileServices extends IQSService.Stub { @Override public void onShowDialog(IBinder token) { - CustomTile customTile = getTileForToken(token); + CustomTileInterface customTile = getTileForToken(token); if (customTile != null) { verifyCaller(customTile); customTile.onDialogShown(); @@ -278,7 +279,7 @@ public class TileServices extends IQSService.Stub { @Override public void onDialogHidden(IBinder token) { - CustomTile customTile = getTileForToken(token); + CustomTileInterface customTile = getTileForToken(token); if (customTile != null) { verifyCaller(customTile); Objects.requireNonNull(mServices.get(customTile)).setShowingDialog(false); @@ -288,7 +289,7 @@ public class TileServices extends IQSService.Stub { @Override public void onStartActivity(IBinder token) { - CustomTile customTile = getTileForToken(token); + CustomTileInterface customTile = getTileForToken(token); if (customTile != null) { verifyCaller(customTile); mPanelInteractor.forceCollapsePanels(); @@ -301,7 +302,7 @@ public class TileServices extends IQSService.Stub { } @VisibleForTesting - protected void startActivity(CustomTile customTile, PendingIntent pendingIntent) { + protected void startActivity(CustomTileInterface customTile, PendingIntent pendingIntent) { if (customTile != null) { verifyCaller(customTile); customTile.startActivityAndCollapse(pendingIntent); @@ -310,7 +311,7 @@ public class TileServices extends IQSService.Stub { @Override public void updateStatusIcon(IBinder token, Icon icon, String contentDescription) { - CustomTile customTile = getTileForToken(token); + CustomTileInterface customTile = getTileForToken(token); if (customTile != null) { verifyCaller(customTile); try { @@ -340,7 +341,7 @@ public class TileServices extends IQSService.Stub { @Nullable @Override public Tile getTile(IBinder token) { - CustomTile customTile = getTileForToken(token); + CustomTileInterface customTile = getTileForToken(token); if (customTile != null) { verifyCaller(customTile); return customTile.getQsTile(); @@ -367,7 +368,7 @@ public class TileServices extends IQSService.Stub { @Override public void startUnlockAndRun(IBinder token) { - CustomTile customTile = getTileForToken(token); + CustomTileInterface customTile = getTileForToken(token); if (customTile != null) { verifyCaller(customTile); customTile.startUnlockAndRun(); @@ -385,14 +386,14 @@ public class TileServices extends IQSService.Stub { } @Nullable - public CustomTile getTileForToken(IBinder token) { + public CustomTileInterface getTileForToken(IBinder token) { synchronized (mServices) { return mTokenMap.get(token); } } @Nullable - private CustomTile getTileForUserAndComponent(int userId, ComponentName component) { + private CustomTileInterface getTileForUserAndComponent(int userId, ComponentName component) { synchronized (mServices) { return mTiles.get(userId, component); } @@ -419,11 +420,6 @@ public class TileServices extends IQSService.Stub { }; private static final Comparator<TileServiceManager> SERVICE_SORT = - new Comparator<TileServiceManager>() { - @Override - public int compare(TileServiceManager left, TileServiceManager right) { - return -Integer.compare(left.getBindPriority(), right.getBindPriority()); - } - }; + (left, right) -> -Integer.compare(left.getBindPriority(), right.getBindPriority()); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt index 38fe34eb8f9f..42d3f81ceed6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt @@ -18,8 +18,6 @@ package com.android.systemui.qs.footer.dagger import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepositoryImpl -import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository -import com.android.systemui.qs.footer.data.repository.UserSwitcherRepositoryImpl import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl import dagger.Binds @@ -28,7 +26,6 @@ import dagger.Module /** Dagger module to provide/bind footer actions singletons. */ @Module interface FooterActionsModule { - @Binds fun userSwitcherRepository(impl: UserSwitcherRepositoryImpl): UserSwitcherRepository @Binds fun foregroundServicesRepository( diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt index 8b2c3de18469..c91ed133a11e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt @@ -38,10 +38,10 @@ import com.android.systemui.qs.FgsManagerController import com.android.systemui.qs.QSSecurityFooterUtils import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository -import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig import com.android.systemui.security.data.repository.SecurityRepository import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.user.data.repository.UserSwitcherRepository import com.android.systemui.user.domain.interactor.UserInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt new file mode 100644 index 000000000000..0d15a5b6b4d4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalytics.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.base.analytics + +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.QSEvent +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import javax.inject.Inject + +/** Tracks QS tiles analytic events to [UiEventLogger]. */ +@SysUISingleton +class QSTileAnalytics +@Inject +constructor( + private val uiEventLogger: UiEventLogger, +) { + + fun trackUserAction(config: QSTileConfig, action: QSTileUserAction) { + logAction(config, action) + } + + private fun logAction(config: QSTileConfig, action: QSTileUserAction) { + uiEventLogger.logWithInstanceId( + action.getQSEvent(), + 0, + config.metricsSpec, + config.instanceId, + ) + } + + private fun QSTileUserAction.getQSEvent(): QSEvent = + when (this) { + is QSTileUserAction.Click -> QSEvent.QS_ACTION_CLICK + is QSTileUserAction.LongClick -> QSEvent.QS_ACTION_LONG_PRESS + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt new file mode 100644 index 000000000000..70a683b81f75 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.base.logging + +import androidx.annotation.GuardedBy +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.QSTilesDefaultLog +import com.android.systemui.log.dagger.QSTilesLogBuffers +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.base.interactor.StateUpdateTrigger +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import com.android.systemui.statusbar.StatusBarState +import javax.inject.Inject +import javax.inject.Provider + +@SysUISingleton +class QSTileLogger +@Inject +constructor( + @QSTilesLogBuffers logBuffers: Map<TileSpec, LogBuffer>, + @QSTilesDefaultLog private val defaultLogBufferProvider: Provider<LogBuffer>, + private val mStatusBarStateController: StatusBarStateController, +) { + @GuardedBy("logBufferCache") private val logBufferCache = logBuffers.toMutableMap() + + /** + * Tracks user action when it's first received by the ViewModel and before it reaches the + * pipeline + */ + fun logUserAction( + userAction: QSTileUserAction, + tileSpec: TileSpec, + hasData: Boolean, + hasTileState: Boolean, + ) { + tileSpec + .getLogBuffer() + .log( + tileSpec.getLogTag(), + LogLevel.DEBUG, + { + str1 = userAction.toLogString() + int1 = mStatusBarStateController.state + bool1 = hasTileState + bool2 = hasData + }, + { + "tile $str1: " + + "statusBarState=${StatusBarState.toString(int1)}, " + + "hasState=$bool1, " + + "hasData=$bool2" + } + ) + } + + /** Tracks user action when it's rejected by false gestures */ + fun logUserActionRejectedByFalsing( + userAction: QSTileUserAction, + tileSpec: TileSpec, + ) { + tileSpec + .getLogBuffer() + .log( + tileSpec.getLogTag(), + LogLevel.DEBUG, + { str1 = userAction.toLogString() }, + { "tile $str1: rejected by falsing" } + ) + } + + /** Tracks user action when it's rejected according to the policy */ + fun logUserActionRejectedByPolicy( + userAction: QSTileUserAction, + tileSpec: TileSpec, + ) { + tileSpec + .getLogBuffer() + .log( + tileSpec.getLogTag(), + LogLevel.DEBUG, + { str1 = userAction.toLogString() }, + { "tile $str1: rejected by policy" } + ) + } + + /** + * Tracks user actions when it reaches the pipeline and mixes with the last tile state and data + */ + fun <T> logUserActionPipeline( + tileSpec: TileSpec, + userAction: QSTileUserAction, + tileState: QSTileState, + data: T, + ) { + tileSpec + .getLogBuffer() + .log( + tileSpec.getLogTag(), + LogLevel.DEBUG, + { + str1 = userAction.toLogString() + str2 = tileState.toLogString() + str3 = data.toString().take(DATA_MAX_LENGTH) + }, + { + "tile $str1 pipeline: " + + "statusBarState=${StatusBarState.toString(int1)}, " + + "state=$str2, " + + "data=$str3" + } + ) + } + + /** Tracks state changes based on the data and trigger event. */ + fun <T> logStateUpdate( + tileSpec: TileSpec, + trigger: StateUpdateTrigger, + tileState: QSTileState, + data: T, + ) { + tileSpec + .getLogBuffer() + .log( + tileSpec.getLogTag(), + LogLevel.DEBUG, + { + str1 = trigger.toLogString() + str2 = tileState.toLogString() + str3 = data.toString().take(DATA_MAX_LENGTH) + }, + { "tile state update: trigger=$str1, state=$str2, data=$str3" } + ) + } + + private fun TileSpec.getLogTag(): String = "${TAG_FORMAT_PREFIX}_${this.spec}" + + private fun TileSpec.getLogBuffer(): LogBuffer = + synchronized(logBufferCache) { + logBufferCache.getOrPut(this) { defaultLogBufferProvider.get() } + } + + private fun StateUpdateTrigger.toLogString(): String = + when (this) { + is StateUpdateTrigger.ForceUpdate -> "force" + is StateUpdateTrigger.InitialRequest -> "init" + is StateUpdateTrigger.UserAction<*> -> action.toLogString() + } + + private fun QSTileUserAction.toLogString(): String = + when (this) { + is QSTileUserAction.Click -> "click" + is QSTileUserAction.LongClick -> "long click" + } + + /* Shortened version of a data class toString() */ + private fun QSTileState.toLogString(): String = + "[label=$label, " + + "state=$activationState, " + + "s_label=$secondaryLabel, " + + "cd=$contentDescription, " + + "sd=$stateDescription, " + + "svi=$sideViewIcon, " + + "enabled=$enabledState, " + + "a11y=$expandedAccessibilityClassName" + + "]" + + private companion object { + const val TAG_FORMAT_PREFIX = "QSLog" + const val DATA_MAX_LENGTH = 50 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt index 58a335e462a1..2114751ef57b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt @@ -20,12 +20,15 @@ import androidx.annotation.CallSuper import androidx.annotation.VisibleForTesting import com.android.internal.util.Preconditions import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.base.interactor.QSTileDataRequest import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor import com.android.systemui.qs.tiles.base.interactor.StateUpdateTrigger +import com.android.systemui.qs.tiles.base.logging.QSTileLogger import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileLifecycle import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy @@ -33,6 +36,7 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel import com.android.systemui.util.kotlin.sample +import com.android.systemui.util.kotlin.throttle import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher @@ -70,6 +74,9 @@ constructor( private val tileDataInteractor: QSTileDataInteractor<DATA_TYPE>, private val mapper: QSTileDataToStateMapper<DATA_TYPE>, private val disabledByPolicyInteractor: DisabledByPolicyInteractor, + private val falsingManager: FalsingManager, + private val qsTileAnalytics: QSTileAnalytics, + private val qsTileLogger: QSTileLogger, private val backgroundDispatcher: CoroutineDispatcher, private val tileScope: CoroutineScope, ) : QSTileViewModel { @@ -81,6 +88,9 @@ constructor( @Assisted tileDataInteractor: QSTileDataInteractor<DATA_TYPE>, @Assisted mapper: QSTileDataToStateMapper<DATA_TYPE>, disabledByPolicyInteractor: DisabledByPolicyInteractor, + falsingManager: FalsingManager, + qsTileAnalytics: QSTileAnalytics, + qsTileLogger: QSTileLogger, @Background backgroundDispatcher: CoroutineDispatcher, ) : this( config, @@ -88,6 +98,9 @@ constructor( tileDataInteractor, mapper, disabledByPolicyInteractor, + falsingManager, + qsTileAnalytics, + qsTileLogger, backgroundDispatcher, CoroutineScope(SupervisorJob()) ) @@ -98,8 +111,10 @@ constructor( MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val forceUpdates: MutableSharedFlow<Unit> = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val spec + get() = config.tileSpec - private lateinit var tileData: SharedFlow<DATA_TYPE> + private lateinit var tileData: SharedFlow<DataWithTrigger<DATA_TYPE>> override lateinit var state: SharedFlow<QSTileState> override val isAvailable: StateFlow<Boolean> = @@ -128,8 +143,14 @@ constructor( @CallSuper override fun onActionPerformed(userAction: QSTileUserAction) { - Preconditions.checkState(tileData.replayCache.isNotEmpty()) Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE) + + qsTileLogger.logUserAction( + userAction, + spec, + tileData.replayCache.isNotEmpty(), + state.replayCache.isNotEmpty() + ) userInputs.tryEmit(userAction) } @@ -142,7 +163,16 @@ constructor( state = tileData // TODO(b/299908705): log data and corresponding tile state - .map { mapper.map(config, it) } + .map { dataWithTrigger -> + mapper.map(config, dataWithTrigger.data).also { state -> + qsTileLogger.logStateUpdate( + spec, + dataWithTrigger.trigger, + state, + dataWithTrigger.data + ) + } + } .flowOn(backgroundDispatcher) .shareIn( tileScope, @@ -158,7 +188,7 @@ constructor( currentLifeState = lifecycle } - private fun createTileDataFlow(): SharedFlow<DATA_TYPE> = + private fun createTileDataFlow(): SharedFlow<DataWithTrigger<DATA_TYPE>> = userIds .flatMapLatest { userId -> merge( @@ -180,7 +210,7 @@ constructor( request.trigger.tileData as DATA_TYPE, ) } - dataFlow + dataFlow.map { DataWithTrigger(it, request.trigger) } } .flowOn(backgroundDispatcher) .shareIn( @@ -193,21 +223,53 @@ constructor( data class StateWithData<T>(val state: QSTileState, val data: T) return when (config.policy) { - is QSTilePolicy.NoRestrictions -> userInputs - is QSTilePolicy.Restricted -> - userInputs.filter { - val result = - disabledByPolicyInteractor.isDisabled(userId, config.policy.userRestriction) - !disabledByPolicyInteractor.handlePolicyResult(result) + is QSTilePolicy.NoRestrictions -> userInputs + is QSTilePolicy.Restricted -> + userInputs.filter { action -> + val result = + disabledByPolicyInteractor.isDisabled( + userId, + config.policy.userRestriction + ) + !disabledByPolicyInteractor.handlePolicyResult(result).also { isDisabled -> + if (isDisabled) { + qsTileLogger.logUserActionRejectedByPolicy(action, spec) + } + } + } + } + .filter { action -> + val isFalseAction = + when (action) { + is QSTileUserAction.Click -> + falsingManager.isFalseTap(FalsingManager.LOW_PENALTY) + is QSTileUserAction.LongClick -> + falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY) + } + if (isFalseAction) { + qsTileLogger.logUserActionRejectedByFalsing(action, spec) } - // Skip the input until there is some data - }.sample(state.combine(tileData) { state, data -> StateWithData(state, data) }) { - input, - stateWithData -> - StateUpdateTrigger.UserAction(input, stateWithData.state, stateWithData.data) - } + !isFalseAction + } + .throttle(500) + // Skip the input until there is some data + .sample(state.combine(tileData) { state, data -> StateWithData(state, data) }) { + input, + stateWithData -> + StateUpdateTrigger.UserAction(input, stateWithData.state, stateWithData.data).also { + qsTileLogger.logUserActionPipeline( + spec, + it.action, + stateWithData.state, + stateWithData.data + ) + qsTileAnalytics.trackUserAction(config, it.action) + } + } } + private data class DataWithTrigger<T>(val data: T, val trigger: StateUpdateTrigger) + interface Factory<T> { /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt index 1a6cf99ab810..4a3bcae17fd0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt @@ -26,6 +26,7 @@ data class QSTileConfig( val tileIcon: Icon, @StringRes val tileLabelRes: Int, val instanceId: InstanceId, + val metricsSpec: String = tileSpec.spec, val policy: QSTilePolicy = QSTilePolicy.NoRestrictions, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt index 249c83166c51..e2de37fcbcbe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.dagger import com.android.systemui.CoreStartable import com.android.systemui.statusbar.core.StatusBarInitializer +import com.android.systemui.statusbar.data.repository.KeyguardStatusBarRepository +import com.android.systemui.statusbar.data.repository.KeyguardStatusBarRepositoryImpl import com.android.systemui.statusbar.data.repository.StatusBarModeRepository import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryImpl import com.android.systemui.statusbar.phone.LightBarController @@ -49,6 +51,11 @@ abstract class StatusBarModule { abstract fun bindStatusBarModeRepositoryStart(impl: StatusBarModeRepositoryImpl): CoreStartable @Binds + abstract fun bindKeyguardStatusBarRepository( + impl: KeyguardStatusBarRepositoryImpl + ): KeyguardStatusBarRepository + + @Binds @IntoMap @ClassKey(OngoingCallController::class) abstract fun bindOngoingCallController(impl: OngoingCallController): CoreStartable diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepository.kt new file mode 100644 index 000000000000..8136de9b7ac4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepository.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.data.repository + +import android.content.Context +import com.android.internal.R +import com.android.systemui.common.coroutine.ConflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.user.data.repository.UserSwitcherRepository +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** + * Repository for data that's specific to the status bar **on keyguard**. For data that applies to + * all status bars, use [StatusBarModeRepository]. + */ +interface KeyguardStatusBarRepository { + /** True if we can show the user switcher on keyguard and false otherwise. */ + val isKeyguardUserSwitcherEnabled: Flow<Boolean> +} + +@SysUISingleton +class KeyguardStatusBarRepositoryImpl +@Inject +constructor( + context: Context, + configurationController: ConfigurationController, + userSwitcherRepository: UserSwitcherRepository, +) : KeyguardStatusBarRepository { + private val relevantConfigChanges: Flow<Unit> = + ConflatedCallbackFlow.conflatedCallbackFlow { + val callback = + object : ConfigurationController.ConfigurationListener { + override fun onSmallestScreenWidthChanged() { + trySend(Unit) + } + + override fun onDensityOrFontScaleChanged() { + trySend(Unit) + } + } + configurationController.addCallback(callback) + awaitClose { configurationController.removeCallback(callback) } + } + + private val isKeyguardUserSwitcherConfigEnabled: Flow<Boolean> = + // The config depends on screen size and user enabled settings, so re-fetch whenever any of + // those change. + merge(userSwitcherRepository.isEnabled.map {}, relevantConfigChanges).map { + context.resources.getBoolean(R.bool.config_keyguardUserSwitcher) + } + + /** True if we can show the user switcher on keyguard and false otherwise. */ + override val isKeyguardUserSwitcherEnabled: Flow<Boolean> = + combine( + userSwitcherRepository.isEnabled, + isKeyguardUserSwitcherConfigEnabled, + ) { isEnabled, isKeyguardEnabled -> + isEnabled && isKeyguardEnabled + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/KeyguardStatusBarInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/KeyguardStatusBarInteractor.kt new file mode 100644 index 000000000000..e0c30e5ec5c4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/KeyguardStatusBarInteractor.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.data.repository.KeyguardStatusBarRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +@SysUISingleton +class KeyguardStatusBarInteractor +@Inject +constructor( + keyguardStatusBarRepository: KeyguardStatusBarRepository, +) { + /** True if we can show the user switcher on keyguard and false otherwise. */ + val isKeyguardUserSwitcherEnabled: Flow<Boolean> = + keyguardStatusBarRepository.isKeyguardUserSwitcherEnabled +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 7efa705b7929..58126ae41a1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -43,9 +43,9 @@ import android.widget.TextView; import androidx.annotation.VisibleForTesting; import com.android.settingslib.Utils; -import com.android.systemui.res.R; import com.android.systemui.battery.BatteryMeterView; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; @@ -367,15 +367,22 @@ public class KeyguardStatusBarView extends RelativeLayout { mMultiUserAvatar.setImageDrawable(picture); } - /** Should only be called from {@link KeyguardStatusBarViewController}. */ - void onBatteryLevelChanged(boolean charging) { + /** + * Should only be called from {@link KeyguardStatusBarViewController} or + * {@link com.android.systemui.statusbar.ui.binder.KeyguardStatusBarViewBinder}. + */ + public void onBatteryChargingChanged(boolean charging) { if (mBatteryCharging != charging) { mBatteryCharging = charging; updateVisibilities(); } } - void setKeyguardUserSwitcherEnabled(boolean enabled) { + /** + * Should only be called from {@link KeyguardStatusBarViewController} or + * {@link com.android.systemui.statusbar.ui.binder.KeyguardStatusBarViewBinder}. + */ + public void setKeyguardUserSwitcherEnabled(boolean enabled) { mKeyguardUserSwitcherEnabled = enabled; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 9cf9714c274c..2960520f00b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -172,7 +172,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat new BatteryController.BatteryStateChangeCallback() { @Override public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { - mView.onBatteryLevelChanged(charging); + mView.onBatteryChargingChanged(charging); } }; @@ -430,11 +430,18 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Sets whether user switcher is enabled. */ public void setKeyguardUserSwitcherEnabled(boolean enabled) { + if (isMigrationEnabled()) { + return; + } mView.setKeyguardUserSwitcherEnabled(enabled); } /** Sets whether this controller should listen to battery updates. */ public void setBatteryListening(boolean listening) { + if (isMigrationEnabled()) { + return; + } + if (listening == mBatteryListening) { return; } @@ -472,6 +479,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar in. */ public void animateKeyguardStatusBarIn() { + if (isMigrationEnabled()) { + return; + } + mLogger.log(TAG, LogLevel.DEBUG, "animating status bar in"); if (mDisableStateTracker.isDisabled()) { // If our view is disabled, don't allow us to animate in. @@ -488,6 +499,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar out. */ public void animateKeyguardStatusBarOut(long startDelay, long duration) { + if (isMigrationEnabled()) { + return; + } + mLogger.log(TAG, LogLevel.DEBUG, "animating status bar out"); ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f); anim.addUpdateListener(mAnimatorUpdateListener); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/binder/KeyguardStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/binder/KeyguardStatusBarViewBinder.kt index c63ef9e5e012..6988e211855b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/binder/KeyguardStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/binder/KeyguardStatusBarViewBinder.kt @@ -22,6 +22,8 @@ import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.phone.KeyguardStatusBarView import com.android.systemui.statusbar.ui.viewmodel.KeyguardStatusBarViewModel +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch /** Binds [KeyguardStatusBarViewModel] to [KeyguardStatusBarView]. */ object KeyguardStatusBarViewBinder { @@ -32,8 +34,18 @@ object KeyguardStatusBarViewBinder { ) { view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.isVisible.collect { isVisible -> - view.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE + launch { + viewModel.isVisible.collect { isVisible -> + view.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE + } + } + + launch { viewModel.isBatteryCharging.collect { view.onBatteryChargingChanged(it) } } + + launch { + viewModel.isKeyguardUserSwitcherEnabled.distinctUntilChanged().collect { + view.setKeyguardUserSwitcherEnabled(it) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt index ddfed8795fcf..5da01e23e268 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt @@ -16,12 +16,18 @@ package com.android.systemui.statusbar.ui.viewmodel +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.statusbar.domain.interactor.KeyguardStatusBarInteractor +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -41,6 +47,8 @@ class KeyguardStatusBarViewModel constructor( @Application scope: CoroutineScope, keyguardInteractor: KeyguardInteractor, + keyguardStatusBarInteractor: KeyguardStatusBarInteractor, + batteryController: BatteryController, ) { /** True if this view should be visible and false otherwise. */ val isVisible: StateFlow<Boolean> = @@ -51,4 +59,26 @@ constructor( !isDozing && statusBarState == StatusBarState.KEYGUARD } .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + /** True if the device's battery is currently charging and false otherwise. */ + // Note: Never make this an eagerly-started state flow so that the callback is removed when the + // keyguard status bar view isn't attached. + val isBatteryCharging: Flow<Boolean> = conflatedCallbackFlow { + val callback = + object : BatteryStateChangeCallback { + override fun onBatteryLevelChanged( + level: Int, + pluggedIn: Boolean, + charging: Boolean, + ) { + trySend(charging) + } + } + batteryController.addCallback(callback) + awaitClose { batteryController.removeCallback(callback) } + } + + /** True if we can show the user switcher on keyguard and false otherwise. */ + val isKeyguardUserSwitcherEnabled: Flow<Boolean> = + keyguardStatusBarInteractor.isKeyguardUserSwitcherEnabled } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepositoryModule.kt index 18ae1070e1bb..71352eff026c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepositoryModule.kt @@ -23,4 +23,6 @@ import dagger.Module @Module interface UserRepositoryModule { @Binds fun bindRepository(impl: UserRepositoryImpl): UserRepository + + @Binds fun userSwitcherRepository(impl: UserSwitcherRepositoryImpl): UserSwitcherRepository } diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/UserSwitcherRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt index 5fa75ad68165..dc7fadd5eb14 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/UserSwitcherRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.footer.data.repository +package com.android.systemui.user.data.repository import android.content.Context import android.graphics.drawable.Drawable @@ -22,7 +22,6 @@ import android.os.Handler import android.os.UserManager import android.provider.Settings.Global.USER_SWITCHER_ENABLED import com.android.keyguard.KeyguardUpdateMonitor -import com.android.systemui.res.R import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -30,6 +29,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.SettingObserver import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel +import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.UserInfoController import com.android.systemui.statusbar.policy.UserSwitcherController @@ -48,6 +48,9 @@ import kotlinx.coroutines.withContext interface UserSwitcherRepository { /** The current [UserSwitcherStatusModel]. */ val userSwitcherStatus: Flow<UserSwitcherStatusModel> + + /** Whether the user switcher is currently enabled. */ + val isEnabled: Flow<Boolean> } @SysUISingleton @@ -66,8 +69,7 @@ constructor( private val showUserSwitcherForSingleUser = context.resources.getBoolean(R.bool.qs_show_user_switcher_for_single_user) - /** Whether the user switcher is currently enabled. */ - private val isEnabled: Flow<Boolean> = conflatedCallbackFlow { + override val isEnabled: Flow<Boolean> = conflatedCallbackFlow { suspend fun updateState() { trySendWithFailureLogging(isUserSwitcherEnabled(), TAG) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalyticsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalyticsTest.kt new file mode 100644 index 000000000000..2c4e10eadfc9 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/analytics/QSTileAnalyticsTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.base.analytics + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.InstanceId +import com.android.internal.logging.UiEventLogger +import com.android.systemui.RoboPilotTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.QSEvent +import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.eq +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RoboPilotTest +@RunWith(AndroidJUnit4::class) +class QSTileAnalyticsTest : SysuiTestCase() { + + @Mock private lateinit var uiEventLogger: UiEventLogger + + private lateinit var underTest: QSTileAnalytics + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + underTest = QSTileAnalytics(uiEventLogger) + } + + @Test + fun testClickIsLogged() { + underTest.trackUserAction(config, QSTileUserAction.Click(null)) + + verify(uiEventLogger) + .logWithInstanceId( + eq(QSEvent.QS_ACTION_CLICK), + eq(0), + eq("test_spec"), + eq(InstanceId.fakeInstanceId(0)) + ) + } + + @Test + fun testLongClickIsLogged() { + underTest.trackUserAction(config, QSTileUserAction.LongClick(null)) + + verify(uiEventLogger) + .logWithInstanceId( + eq(QSEvent.QS_ACTION_LONG_PRESS), + eq(0), + eq("test_spec"), + eq(InstanceId.fakeInstanceId(0)) + ) + } + + private companion object { + + val config = QSTileConfigTestBuilder.build() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt new file mode 100644 index 000000000000..4401e0d60da6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.base.logging + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.RoboPilotTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dump.LogcatEchoTrackerAlways +import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.base.interactor.StateUpdateTrigger +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import com.google.common.truth.Truth.assertThat +import java.io.PrintWriter +import java.io.StringWriter +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RoboPilotTest +@RunWith(AndroidJUnit4::class) +class QSTileLoggerTest : SysuiTestCase() { + + @Mock private lateinit var statusBarController: StatusBarStateController + + private val chattyLogBuffer = LogBuffer("TestChatty", 5, LogcatEchoTrackerAlways()) + private val logBuffer = LogBuffer("Test", 1, LogcatEchoTrackerAlways()) + + private lateinit var underTest: QSTileLogger + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + underTest = + QSTileLogger( + mapOf(TileSpec.create("chatty_tile") to chattyLogBuffer), + { logBuffer }, + statusBarController + ) + } + + @Test + fun testChattyLog() { + underTest.logUserActionRejectedByFalsing( + QSTileUserAction.Click(null), + TileSpec.create("chatty_tile"), + ) + underTest.logUserActionRejectedByFalsing( + QSTileUserAction.Click(null), + TileSpec.create("chatty_tile"), + ) + + val logs = chattyLogBuffer.getStringBuffer().lines().filter { it.isNotBlank() } + assertThat(logs).hasSize(2) + logs.forEach { assertThat(it).contains("tile click: rejected by falsing") } + } + + @Test + fun testLogUserAction() { + underTest.logUserAction( + QSTileUserAction.Click(null), + TileSpec.create("test_spec"), + hasData = false, + hasTileState = false, + ) + + assertThat(logBuffer.getStringBuffer()) + .contains("tile click: statusBarState=SHADE, hasState=false, hasData=false") + } + + @Test + fun testLogUserActionRejectedByFalsing() { + underTest.logUserActionRejectedByFalsing( + QSTileUserAction.Click(null), + TileSpec.create("test_spec"), + ) + + assertThat(logBuffer.getStringBuffer()).contains("tile click: rejected by falsing") + } + + @Test + fun testLogUserActionRejectedByPolicy() { + underTest.logUserActionRejectedByPolicy( + QSTileUserAction.Click(null), + TileSpec.create("test_spec"), + ) + + assertThat(logBuffer.getStringBuffer()).contains("tile click: rejected by policy") + } + + @Test + fun testLogUserActionPipeline() { + underTest.logUserActionPipeline( + TileSpec.create("test_spec"), + QSTileUserAction.Click(null), + QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {}, + "test_data", + ) + + assertThat(logBuffer.getStringBuffer()) + .contains( + "tile click pipeline: " + + "statusBarState=SHADE, " + + "state=[" + + "label=, " + + "state=INACTIVE, " + + "s_label=null, " + + "cd=null, " + + "sd=null, " + + "svi=None, " + + "enabled=ENABLED, " + + "a11y=null" + + "], " + + "data=test_data" + ) + } + + @Test + fun testLogStateUpdate() { + underTest.logStateUpdate( + TileSpec.create("test_spec"), + StateUpdateTrigger.ForceUpdate, + QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {}, + "test_data", + ) + + assertThat(logBuffer.getStringBuffer()) + .contains( + "tile state update: " + + "trigger=force, " + + "state=[" + + "label=, " + + "state=INACTIVE, " + + "s_label=null, " + + "cd=null, " + + "sd=null, " + + "svi=None, " + + "enabled=ENABLED, " + + "a11y=null" + + "], " + + "data=test_data" + ) + } + + private fun LogBuffer.getStringBuffer(): String { + val stringWriter = StringWriter() + dump(PrintWriter(stringWriter), 0) + return stringWriter.buffer.toString() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt index 9024c6c5576b..4760dfa3561d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt @@ -1,21 +1,39 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.android.systemui.qs.tiles.viewmodel -import android.graphics.drawable.ShapeDrawable import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.android.internal.logging.InstanceId import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor import com.android.systemui.qs.tiles.base.interactor.QSTileDataRequest import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper import com.android.systemui.qs.tiles.base.interactor.StateUpdateTrigger +import com.android.systemui.qs.tiles.base.logging.QSTileLogger import com.android.systemui.qs.tiles.base.viewmodel.BaseQSTileViewModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn @@ -26,6 +44,8 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations // TODO(b/299909368): Add more tests @MediumTest @@ -34,9 +54,13 @@ import org.junit.runner.RunWith @TestableLooper.RunWithLooper(setAsMainLooper = true) class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { + @Mock private lateinit var qsTileLogger: QSTileLogger + @Mock private lateinit var qsTileAnalytics: QSTileAnalytics + private val fakeQSTileDataInteractor = FakeQSTileDataInteractor<Any>() private val fakeQSTileUserActionInteractor = FakeQSTileUserActionInteractor<Any>() private val fakeDisabledByPolicyInteractor = FakeDisabledByPolicyInteractor() + private val fakeFalsingManager = FalsingManagerFake() private val testCoroutineDispatcher = StandardTestDispatcher() private val testScope = TestScope(testCoroutineDispatcher) @@ -45,6 +69,7 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { @Before fun setup() { + MockitoAnnotations.initMocks(this) underTest = createViewModel(testScope) } @@ -79,6 +104,9 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {} }, fakeDisabledByPolicyInteractor, + fakeFalsingManager, + qsTileAnalytics, + qsTileLogger, testCoroutineDispatcher, scope.backgroundScope, ) @@ -88,7 +116,7 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { val TEST_QS_TILE_CONFIG = QSTileConfig( TileSpec.create("default"), - Icon.Loaded(ShapeDrawable(), null), + Icon.Resource(0, null), 0, InstanceId.fakeInstanceId(0), ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/FakeKeyguardStatusBarRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/FakeKeyguardStatusBarRepository.kt new file mode 100644 index 000000000000..f1e6a053643f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/FakeKeyguardStatusBarRepository.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeKeyguardStatusBarRepository : KeyguardStatusBarRepository { + override val isKeyguardUserSwitcherEnabled = MutableStateFlow(false) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepositoryImplTest.kt new file mode 100644 index 000000000000..b1c994c2374e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepositoryImplTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.data.repository + +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.user.data.repository.FakeUserSwitcherRepository +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.mockito.Mockito.verify + +@SmallTest +class KeyguardStatusBarRepositoryImplTest : SysuiTestCase() { + private val testScope = TestScope() + private val configurationController = mock<ConfigurationController>() + private val userSwitcherRepository = FakeUserSwitcherRepository() + + val underTest = + KeyguardStatusBarRepositoryImpl( + context, + configurationController, + userSwitcherRepository, + ) + + private val configurationListener: ConfigurationController.ConfigurationListener + get() { + val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() + verify(configurationController).addCallback(capture(captor)) + return captor.value + } + + @Test + fun isKeyguardUserSwitcherEnabled_switcherNotEnabled_false() = + testScope.runTest { + val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled) + + userSwitcherRepository.isEnabled.value = false + + assertThat(latest).isFalse() + } + + @Test + fun isKeyguardUserSwitcherEnabled_keyguardConfigNotEnabled_false() = + testScope.runTest { + val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled) + userSwitcherRepository.isEnabled.value = true + + context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, false) + + assertThat(latest).isFalse() + } + + @Test + fun isKeyguardUserSwitcherEnabled_switchEnabledAndKeyguardConfigEnabled_true() = + testScope.runTest { + val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled) + + userSwitcherRepository.isEnabled.value = true + context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, true) + + assertThat(latest).isTrue() + } + + @Test + fun isKeyguardUserSwitcherEnabled_refetchedOnSmallestWidthChanged() = + testScope.runTest { + val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled) + userSwitcherRepository.isEnabled.value = true + context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, true) + assertThat(latest).isTrue() + + context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, false) + configurationListener.onSmallestScreenWidthChanged() + + assertThat(latest).isFalse() + } + + @Test + fun isKeyguardUserSwitcherEnabled_refetchedOnDensityChanged() = + testScope.runTest { + val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled) + userSwitcherRepository.isEnabled.value = true + context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, true) + assertThat(latest).isTrue() + + context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, false) + configurationListener.onDensityOrFontScaleChanged() + + assertThat(latest).isFalse() + } + + @Test + fun isKeyguardUserSwitcherEnabled_refetchedOnEnabledChanged() = + testScope.runTest { + val latest by collectLastValue(underTest.isKeyguardUserSwitcherEnabled) + + userSwitcherRepository.isEnabled.value = false + context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, true) + assertThat(latest).isFalse() + + // WHEN the switcher becomes enabled but the keyguard switcher becomes disabled + context.orCreateTestableResources.addOverride(R.bool.config_keyguardUserSwitcher, false) + userSwitcherRepository.isEnabled.value = true + + // THEN the value is still false because the keyguard config is refetched + assertThat(latest).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index c0d248ea15e7..648438997166 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -64,12 +64,13 @@ import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.power.domain.interactor.PowerInteractorFactory; import com.android.systemui.res.R; import com.android.systemui.scene.SceneTestUtils; -import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags; import com.android.systemui.shade.ShadeViewStateProvider; import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.data.repository.FakeKeyguardStatusBarRepository; +import com.android.systemui.statusbar.domain.interactor.KeyguardStatusBarInteractor; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -156,7 +157,6 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { public void setup() throws Exception { mFeatureFlags.set(Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW, false); mShadeViewStateProvider = new TestShadeViewStateProvider(); - mShadeViewStateProvider = new TestShadeViewStateProvider(); MockitoAnnotations.initMocks(this); @@ -176,7 +176,9 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mViewModel = new KeyguardStatusBarViewModel( mTestScope.getBackgroundScope(), - mKeyguardInteractor); + mKeyguardInteractor, + new KeyguardStatusBarInteractor(new FakeKeyguardStatusBarRepository()), + mBatteryController); allowTestableLooperAsMainThread(); TestableLooper.get(this).runWithLooper(() -> { @@ -320,6 +322,15 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { } @Test + public void setBatteryListening_true_flagOn_callbackNotAdded() { + mFeatureFlags.set(Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW, true); + + mController.setBatteryListening(true); + + verify(mBatteryController, never()).addCallback(any()); + } + + @Test public void updateTopClipping_viewClippingUpdated() { int viewTop = 20; mKeyguardStatusBarView.setTop(viewTop); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt index f4078d59bbdb..1bc346de1568 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt @@ -29,13 +29,23 @@ import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.scene.SceneTestUtils import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.data.repository.FakeKeyguardStatusBarRepository +import com.android.systemui.statusbar.domain.interactor.KeyguardStatusBarInteractor +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test +import org.mockito.Mockito.verify @SmallTest +@OptIn(ExperimentalCoroutinesApi::class) class KeyguardStatusBarViewModelTest : SysuiTestCase() { private val testScope = TestScope() private val sceneTestUtils = SceneTestUtils(this) @@ -54,11 +64,18 @@ class KeyguardStatusBarViewModelTest : SysuiTestCase() { ) { sceneTestUtils.sceneInteractor() } + private val keyguardStatusBarInteractor = + KeyguardStatusBarInteractor( + FakeKeyguardStatusBarRepository(), + ) + private val batteryController = mock<BatteryController>() private val underTest = KeyguardStatusBarViewModel( testScope.backgroundScope, keyguardInteractor, + keyguardStatusBarInteractor, + batteryController, ) @Test @@ -102,4 +119,46 @@ class KeyguardStatusBarViewModelTest : SysuiTestCase() { assertThat(latest).isTrue() } + + @Test + fun isBatteryCharging_matchesCallback() = + testScope.runTest { + val latest by collectLastValue(underTest.isBatteryCharging) + runCurrent() + + val captor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(captor)) + val callback = captor.value + + callback.onBatteryLevelChanged( + /* level= */ 2, + /* pluggedIn= */ false, + /* charging= */ true, + ) + + assertThat(latest).isTrue() + + callback.onBatteryLevelChanged( + /* level= */ 2, + /* pluggedIn= */ true, + /* charging= */ false, + ) + + assertThat(latest).isFalse() + } + + @Test + fun isBatteryCharging_unregistersWhenNotListening() = + testScope.runTest { + val job = underTest.isBatteryCharging.launchIn(this) + runCurrent() + + val captor = argumentCaptor<BatteryController.BatteryStateChangeCallback>() + verify(batteryController).addCallback(capture(captor)) + + job.cancel() + runCurrent() + + verify(batteryController).removeCallback(captor.value) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/FakeUserSwitcherRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/FakeUserSwitcherRepository.kt new file mode 100644 index 000000000000..758fe93a658c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/FakeUserSwitcherRepository.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.user.data.repository + +import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeUserSwitcherRepository : UserSwitcherRepository { + override val isEnabled = MutableStateFlow(false) + override val userSwitcherStatus = + MutableStateFlow<UserSwitcherStatusModel>(UserSwitcherStatusModel.Disabled) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java index cd009dff27ff..d6632a3c5ea7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java @@ -38,16 +38,12 @@ import androidx.test.InstrumentationRegistry; import androidx.test.uiautomator.UiDevice; import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.FakeBroadcastDispatcher; import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger; -import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.FalsingManager; import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.phone.SystemUIDialogManager; import org.junit.After; @@ -128,20 +124,8 @@ public abstract class SysuiTestCase { // reference and are never sent to the Context. This will also prevent a real // BroadcastDispatcher from actually registering receivers. mDependency.injectTestDependency(BroadcastDispatcher.class, mFakeBroadcastDispatcher); - // A lot of tests get the FalsingManager, often via several layers of indirection. - // None of them actually need it. - mDependency.injectTestDependency(FalsingManager.class, new FalsingManagerFake()); mDependency.injectMockDependency(KeyguardUpdateMonitor.class); - // A lot of tests get the LocalBluetoothManager, often via several layers of indirection. - // None of them actually need it. - mDependency.injectMockDependency(LocalBluetoothManager.class); - - // Notifications tests are injecting one of these, causing many classes (including - // KeyguardUpdateMonitor to be created (injected). - // TODO(b/1531701009) Clean up NotificationContentView creation to prevent this - mDependency.injectMockDependency(SmartReplyController.class); - // Make sure that all tests on any SystemUIDialog does not crash because this dependency // is missing (constructing the actual one would throw). // TODO(b/219008720): Remove this. diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt index 1a893f8c523c..bf77b1a050cd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -36,8 +36,6 @@ import com.android.systemui.qs.FgsManagerController import com.android.systemui.qs.QSSecurityFooterUtils import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepositoryImpl -import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository -import com.android.systemui.qs.footer.data.repository.UserSwitcherRepositoryImpl import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel @@ -51,6 +49,8 @@ import com.android.systemui.statusbar.policy.FakeUserInfoController import com.android.systemui.statusbar.policy.SecurityController import com.android.systemui.statusbar.policy.UserInfoController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.repository.UserSwitcherRepository +import com.android.systemui.user.data.repository.UserSwitcherRepositoryImpl import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.FakeSettings diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt new file mode 100644 index 000000000000..201926df5a5c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.viewmodel + +import androidx.annotation.StringRes +import com.android.internal.logging.InstanceId +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.qs.pipeline.shared.TileSpec + +object QSTileConfigTestBuilder { + + fun build(configure: BuildingScope.() -> Unit = {}): QSTileConfig = + BuildingScope().apply(configure).build() + + class BuildingScope { + var tileSpec: TileSpec = TileSpec.create("test_spec") + var tileIcon: Icon = Icon.Resource(0, ContentDescription.Resource(0)) + @StringRes var tileLabel: Int = 0 + var instanceId: InstanceId = InstanceId.fakeInstanceId(0) + var metricsSpec: String = tileSpec.spec + var policy: QSTilePolicy = QSTilePolicy.NoRestrictions + + fun build() = + QSTileConfig( + tileSpec, + tileIcon, + tileLabel, + instanceId, + metricsSpec, + policy, + ) + } +} diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java index 9dd0dca47f0e..852e36dcc605 100644 --- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java +++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java @@ -218,6 +218,9 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController void setActivityLaunchDefaultAllowed(boolean activityLaunchDefaultAllowed) { synchronized (mGenericWindowPolicyControllerLock) { + if (mActivityLaunchAllowedByDefault != activityLaunchDefaultAllowed) { + mActivityPolicyExemptions.clear(); + } mActivityLaunchAllowedByDefault = activityLaunchDefaultAllowed; } } diff --git a/services/core/java/com/android/server/ExplicitHealthCheckController.java b/services/core/java/com/android/server/ExplicitHealthCheckController.java index 20de40e73130..3d610d3747c9 100644 --- a/services/core/java/com/android/server/ExplicitHealthCheckController.java +++ b/services/core/java/com/android/server/ExplicitHealthCheckController.java @@ -343,7 +343,7 @@ class ExplicitHealthCheckController { }; mContext.bindServiceAsUser(intent, mConnection, - Context.BIND_AUTO_CREATE, UserHandle.of(UserHandle.USER_SYSTEM)); + Context.BIND_AUTO_CREATE, UserHandle.SYSTEM); Slog.i(TAG, "Explicit health check service is bound"); } } diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java index c094c12cc6d8..dd543349fc4d 100644 --- a/services/core/java/com/android/server/RescueParty.java +++ b/services/core/java/com/android/server/RescueParty.java @@ -522,7 +522,8 @@ public class RescueParty { Exception res = null; final ContentResolver resolver = context.getContentResolver(); try { - Settings.Global.resetToDefaultsAsUser(resolver, null, mode, UserHandle.USER_SYSTEM); + Settings.Global.resetToDefaultsAsUser(resolver, null, mode, + UserHandle.SYSTEM.getIdentifier()); } catch (Exception e) { res = new RuntimeException("Failed to reset global settings", e); } @@ -779,12 +780,13 @@ public class RescueParty { } private static int[] getAllUserIds() { - int[] userIds = { UserHandle.USER_SYSTEM }; + int systemUserId = UserHandle.SYSTEM.getIdentifier(); + int[] userIds = { systemUserId }; try { for (File file : FileUtils.listFilesOrEmpty(Environment.getDataSystemDeDirectory())) { try { final int userId = Integer.parseInt(file.getName()); - if (userId != UserHandle.USER_SYSTEM) { + if (userId != systemUserId) { userIds = ArrayUtils.appendInt(userIds, userId); } } catch (NumberFormatException ignored) { diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java index 6e984bb4bb7b..b05b397a45de 100644 --- a/services/core/java/com/android/server/Watchdog.java +++ b/services/core/java/com/android/server/Watchdog.java @@ -330,7 +330,7 @@ public class Watchdog implements Dumpable { String describeBlockedStateLocked() { final String prefix; if (mCurrentMonitor == null) { - prefix = "Blocked in handler on "; + prefix = "Blocked in handler"; } else { prefix = "Blocked in monitor " + mCurrentMonitor.getClass().getName(); } diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 83a3125884ac..c9528d8257c4 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -707,7 +707,8 @@ class MediaRouter2ServiceImpl { } private boolean checkCallerHasSystemRoutingPermissions(int pid, int uid) { - return checkCallerHasModifyAudioRoutingPermission(pid, uid); + return checkCallerHasModifyAudioRoutingPermission(pid, uid) + || checkCallerHasBluetoothPermissions(pid, uid); } private boolean checkCallerHasModifyAudioRoutingPermission(int pid, int uid) { diff --git a/services/tests/servicestests/src/com/android/server/timezone/OWNERS b/services/tests/servicestests/src/com/android/server/timezone/OWNERS index 61652604ee9f..d64cbcdc2814 100644 --- a/services/tests/servicestests/src/com/android/server/timezone/OWNERS +++ b/services/tests/servicestests/src/com/android/server/timezone/OWNERS @@ -1,2 +1,2 @@ -# Bug component: 24949 +# Bug component: 847766 include /services/core/java/com/android/server/timezone/OWNERS diff --git a/services/usage/java/com/android/server/usage/UsageStatsHandlerThread.java b/services/usage/java/com/android/server/usage/UsageStatsHandlerThread.java new file mode 100644 index 000000000000..6801c9402538 --- /dev/null +++ b/services/usage/java/com/android/server/usage/UsageStatsHandlerThread.java @@ -0,0 +1,80 @@ +/* + * 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.usage; + +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Process; +import android.os.Trace; + +import java.util.concurrent.Executor; + +/** + * Shared singleton default priority thread for usage stats message handling. + * + * @see com.android.internal.os.BackgroundThread + */ +public final class UsageStatsHandlerThread extends HandlerThread { + private static final long SLOW_DISPATCH_THRESHOLD_MS = 10_000; + private static final long SLOW_DELIVERY_THRESHOLD_MS = 30_000; + private static UsageStatsHandlerThread sInstance; + private static Handler sHandler; + private static Executor sHandlerExecutor; + + private UsageStatsHandlerThread() { + super("usagestats.default", Process.THREAD_PRIORITY_DEFAULT); + } + + private static void ensureThreadLocked() { + if (sInstance == null) { + sInstance = new UsageStatsHandlerThread(); + sInstance.start(); + final Looper looper = sInstance.getLooper(); + looper.setTraceTag(Trace.TRACE_TAG_SYSTEM_SERVER); + looper.setSlowLogThresholdMs( + SLOW_DISPATCH_THRESHOLD_MS, SLOW_DELIVERY_THRESHOLD_MS); + sHandler = new Handler(sInstance.getLooper()); + sHandlerExecutor = new HandlerExecutor(sHandler); + } + } + + /** Returns the UsageStatsHandlerThread singleton */ + public static UsageStatsHandlerThread get() { + synchronized (UsageStatsHandlerThread.class) { + ensureThreadLocked(); + return sInstance; + } + } + + /** Returns the singleton handler for UsageStatsHandlerThread */ + public static Handler getHandler() { + synchronized (UsageStatsHandlerThread.class) { + ensureThreadLocked(); + return sHandler; + } + } + + /** Returns the singleton handler executor for UsageStatsHandlerThread */ + public static Executor getExecutor() { + synchronized (UsageStatsHandlerThread.class) { + ensureThreadLocked(); + return sHandlerExecutor; + } + } +} diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 58b5ae556f19..18c960e3ebbc 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -106,6 +106,7 @@ import com.android.internal.util.CollectionUtils; import com.android.internal.util.DumpUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.IndentingPrintWriter; +import com.android.server.IoThread; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; @@ -201,7 +202,8 @@ public class UsageStatsService extends SystemService implements static final int MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED = 9; private final Object mLock = new Object(); - Handler mHandler; + private Handler mHandler; + private Handler mIoHandler; AppOpsManager mAppOps; UserManager mUserManager; PackageManager mPackageManager; @@ -233,7 +235,7 @@ public class UsageStatsService extends SystemService implements private final SparseArray<LinkedList<Event>> mReportedEvents = new SparseArray<>(); final SparseArray<ArraySet<String>> mUsageReporters = new SparseArray(); final SparseArray<ActivityData> mVisibleActivities = new SparseArray(); - @GuardedBy("mLock") + @GuardedBy("mLaunchTimeAlarmQueues") // Don't hold the main lock private final SparseArray<LaunchTimeAlarmQueue> mLaunchTimeAlarmQueues = new SparseArray<>(); @GuardedBy("mUsageEventListeners") // Don't hold the main lock when calling out private final ArraySet<UsageStatsManagerInternal.UsageEventListener> mUsageEventListeners = @@ -279,6 +281,38 @@ public class UsageStatsService extends SystemService implements } } + private final Handler.Callback mIoHandlerCallback = (msg) -> { + switch (msg.what) { + case MSG_UID_STATE_CHANGED: { + final int uid = msg.arg1; + final int procState = msg.arg2; + + final int newCounter = (procState <= ActivityManager.PROCESS_STATE_TOP) ? 0 : 1; + synchronized (mUidToKernelCounter) { + final int oldCounter = mUidToKernelCounter.get(uid, 0); + if (newCounter != oldCounter) { + mUidToKernelCounter.put(uid, newCounter); + try { + FileUtils.stringToFile(KERNEL_COUNTER_FILE, uid + " " + newCounter); + } catch (IOException e) { + Slog.w(TAG, "Failed to update counter set: " + e); + } + } + } + return true; + } + case MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK: { + final int userId = msg.arg1; + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, + "usageStatsHandleEstimatedLaunchTimesOnUser(" + userId + ")"); + handleEstimatedLaunchTimesOnUserUnlock(userId); + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + return true; + } + } + return false; + }; + private final Injector mInjector; public UsageStatsService(Context context) { @@ -298,7 +332,9 @@ public class UsageStatsService extends SystemService implements mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE); mPackageManager = getContext().getPackageManager(); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); - mHandler = new H(BackgroundThread.get().getLooper()); + + mHandler = new H(UsageStatsHandlerThread.get().getLooper()); + mIoHandler = new Handler(IoThread.get().getLooper(), mIoHandlerCallback); mAppStandby = mInjector.getAppStandbyController(getContext()); mResponseStatsTracker = new BroadcastResponseStatsTracker(mAppStandby, getContext()); @@ -424,6 +460,9 @@ public class UsageStatsService extends SystemService implements } mUserUnlockedStates.remove(userId); mUserState.put(userId, null); // release the service (mainly for GC) + } + + synchronized (mLaunchTimeAlarmQueues) { LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId); if (alarmQueue != null) { alarmQueue.removeAllAlarms(); @@ -476,11 +515,13 @@ public class UsageStatsService extends SystemService implements } reportEvent(unlockEvent, userId); - mHandler.obtainMessage(MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK, userId, 0).sendToTarget(); + mIoHandler.obtainMessage(MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK, + userId, 0).sendToTarget(); // Remove all the stats stored in memory and in system DE. mReportedEvents.remove(userId); deleteRecursively(new File(Environment.getDataSystemDeDirectory(userId), "usagestats")); + // Force a flush to disk for the current user to ensure important events are persisted. // Note: there is a very very small chance that the system crashes between deleting // the stats above from DE and persisting them to CE here in which case we will lose @@ -599,7 +640,7 @@ public class UsageStatsService extends SystemService implements private final IUidObserver mUidObserver = new UidObserver() { @Override public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) { - mHandler.obtainMessage(MSG_UID_STATE_CHANGED, uid, procState).sendToTarget(); + mIoHandler.obtainMessage(MSG_UID_STATE_CHANGED, uid, procState).sendToTarget(); } @Override @@ -671,16 +712,18 @@ public class UsageStatsService extends SystemService implements callingPid, callingUid) == PackageManager.PERMISSION_GRANTED); } - private static void deleteRecursively(File f) { - File[] files = f.listFiles(); - if (files != null) { - for (File subFile : files) { - deleteRecursively(subFile); + private static void deleteRecursively(final File path) { + if (path.isDirectory()) { + final File[] files = path.listFiles(); + if (files != null) { + for (File subFile : files) { + deleteRecursively(subFile); + } } } - if (f.exists() && !f.delete()) { - Slog.e(TAG, "Failed to delete " + f); + if (path.exists() && !path.delete()) { + Slog.e(TAG, "Failed to delete " + path); } } @@ -1241,6 +1284,9 @@ public class UsageStatsService extends SystemService implements Slog.i(TAG, "Removing user " + userId + " and all data."); mUserState.remove(userId); mAppTimeLimit.onUserRemoved(userId); + } + + synchronized (mLaunchTimeAlarmQueues) { final LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId); if (alarmQueue != null) { alarmQueue.removeAllAlarms(); @@ -1271,6 +1317,13 @@ public class UsageStatsService extends SystemService implements } } + synchronized (mLaunchTimeAlarmQueues) { + final LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId); + if (alarmQueue != null) { + alarmQueue.removeAlarmForKey(packageName); + } + } + final int tokenRemoved; synchronized (mLock) { final long timeRemoved = System.currentTimeMillis(); @@ -1279,10 +1332,7 @@ public class UsageStatsService extends SystemService implements // when the user service is initialized and package manager is queried. return; } - final LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId); - if (alarmQueue != null) { - alarmQueue.removeAlarmForKey(packageName); - } + final UserUsageStatsService userService = mUserState.get(userId); if (userService == null) { return; @@ -1492,60 +1542,63 @@ public class UsageStatsService extends SystemService implements estimatedLaunchTime = calculateEstimatedPackageLaunchTime(userId, packageName); mAppStandby.setEstimatedLaunchTime(packageName, userId, estimatedLaunchTime); - synchronized (mLock) { - LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId); - if (alarmQueue == null) { - alarmQueue = new LaunchTimeAlarmQueue( - userId, getContext(), BackgroundThread.get().getLooper()); - mLaunchTimeAlarmQueues.put(userId, alarmQueue); - } - alarmQueue.addAlarm(packageName, - SystemClock.elapsedRealtime() + (estimatedLaunchTime - now)); - } + getOrCreateLaunchTimeAlarmQueue(userId).addAlarm(packageName, + SystemClock.elapsedRealtime() + (estimatedLaunchTime - now)); } return estimatedLaunchTime; } + private LaunchTimeAlarmQueue getOrCreateLaunchTimeAlarmQueue(int userId) { + synchronized (mLaunchTimeAlarmQueues) { + LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId); + if (alarmQueue == null) { + alarmQueue = new LaunchTimeAlarmQueue( + userId, getContext(), BackgroundThread.get().getLooper()); + mLaunchTimeAlarmQueues.put(userId, alarmQueue); + } + + return alarmQueue; + } + } + @CurrentTimeMillisLong private long calculateEstimatedPackageLaunchTime(int userId, String packageName) { - synchronized (mLock) { - final long endTime = System.currentTimeMillis(); - final long beginTime = endTime - ONE_WEEK; - final long unknownTime = endTime + UNKNOWN_LAUNCH_TIME_DELAY_MS; - final UsageEvents events = queryEarliestEventsForPackage( - userId, beginTime, endTime, packageName, Event.ACTIVITY_RESUMED); - if (events == null) { - if (DEBUG) { - Slog.d(TAG, "No events for " + userId + ":" + packageName); - } - return unknownTime; + final long endTime = System.currentTimeMillis(); + final long beginTime = endTime - ONE_WEEK; + final long unknownTime = endTime + UNKNOWN_LAUNCH_TIME_DELAY_MS; + final UsageEvents events = queryEarliestEventsForPackage( + userId, beginTime, endTime, packageName, Event.ACTIVITY_RESUMED); + if (events == null) { + if (DEBUG) { + Slog.d(TAG, "No events for " + userId + ":" + packageName); } - final UsageEvents.Event event = new UsageEvents.Event(); - final boolean hasMoreThan24HoursOfHistory; - if (events.getNextEvent(event)) { - hasMoreThan24HoursOfHistory = endTime - event.getTimeStamp() > ONE_DAY; - if (DEBUG) { - Slog.d(TAG, userId + ":" + packageName + " history > 24 hours=" - + hasMoreThan24HoursOfHistory); - } - } else { - if (DEBUG) { - Slog.d(TAG, userId + ":" + packageName + " has no events"); - } - return unknownTime; - } - do { - if (event.getEventType() == Event.ACTIVITY_RESUMED) { - final long timestamp = event.getTimeStamp(); - final long nextLaunch = - calculateNextLaunchTime(hasMoreThan24HoursOfHistory, timestamp); - if (nextLaunch > endTime) { - return nextLaunch; - } - } - } while (events.getNextEvent(event)); return unknownTime; } + final UsageEvents.Event event = new UsageEvents.Event(); + final boolean hasMoreThan24HoursOfHistory; + if (events.getNextEvent(event)) { + hasMoreThan24HoursOfHistory = endTime - event.getTimeStamp() > ONE_DAY; + if (DEBUG) { + Slog.d(TAG, userId + ":" + packageName + " history > 24 hours=" + + hasMoreThan24HoursOfHistory); + } + } else { + if (DEBUG) { + Slog.d(TAG, userId + ":" + packageName + " has no events"); + } + return unknownTime; + } + do { + if (event.getEventType() == Event.ACTIVITY_RESUMED) { + final long timestamp = event.getTimeStamp(); + final long nextLaunch = + calculateNextLaunchTime(hasMoreThan24HoursOfHistory, timestamp); + if (nextLaunch > endTime) { + return nextLaunch; + } + } + } while (events.getNextEvent(event)); + return unknownTime; } @CurrentTimeMillisLong @@ -1566,62 +1619,55 @@ public class UsageStatsService extends SystemService implements } private void handleEstimatedLaunchTimesOnUserUnlock(int userId) { - synchronized (mLock) { - final long nowElapsed = SystemClock.elapsedRealtime(); - final long now = System.currentTimeMillis(); - final long beginTime = now - ONE_WEEK; - final UsageEvents events = queryEarliestAppEvents( - userId, beginTime, now, Event.ACTIVITY_RESUMED); - if (events == null) { - return; - } - final ArrayMap<String, Boolean> hasMoreThan24HoursOfHistory = new ArrayMap<>(); - final UsageEvents.Event event = new UsageEvents.Event(); - LaunchTimeAlarmQueue alarmQueue = mLaunchTimeAlarmQueues.get(userId); - if (alarmQueue == null) { - alarmQueue = new LaunchTimeAlarmQueue( - userId, getContext(), BackgroundThread.get().getLooper()); - mLaunchTimeAlarmQueues.put(userId, alarmQueue); - } - boolean changedTimes = false; - for (boolean unprocessedEvent = events.getNextEvent(event); unprocessedEvent; - unprocessedEvent = events.getNextEvent(event)) { - final String packageName = event.getPackageName(); - if (!hasMoreThan24HoursOfHistory.containsKey(packageName)) { - boolean hasHistory = now - event.getTimeStamp() > ONE_DAY; - if (DEBUG) { - Slog.d(TAG, - userId + ":" + packageName + " history > 24 hours=" + hasHistory); - } - hasMoreThan24HoursOfHistory.put(packageName, hasHistory); + final long nowElapsed = SystemClock.elapsedRealtime(); + final long now = System.currentTimeMillis(); + final long beginTime = now - ONE_WEEK; + final UsageEvents events = queryEarliestAppEvents( + userId, beginTime, now, Event.ACTIVITY_RESUMED); + if (events == null) { + return; + } + final ArrayMap<String, Boolean> hasMoreThan24HoursOfHistory = new ArrayMap<>(); + final UsageEvents.Event event = new UsageEvents.Event(); + boolean changedTimes = false; + final LaunchTimeAlarmQueue alarmQueue = getOrCreateLaunchTimeAlarmQueue(userId); + for (boolean unprocessedEvent = events.getNextEvent(event); unprocessedEvent; + unprocessedEvent = events.getNextEvent(event)) { + final String packageName = event.getPackageName(); + if (!hasMoreThan24HoursOfHistory.containsKey(packageName)) { + boolean hasHistory = now - event.getTimeStamp() > ONE_DAY; + if (DEBUG) { + Slog.d(TAG, + userId + ":" + packageName + " history > 24 hours=" + hasHistory); } - if (event.getEventType() == Event.ACTIVITY_RESUMED) { - long estimatedLaunchTime = - mAppStandby.getEstimatedLaunchTime(packageName, userId); - if (estimatedLaunchTime < now || estimatedLaunchTime == Long.MAX_VALUE) { - //noinspection ConstantConditions - estimatedLaunchTime = calculateNextLaunchTime( - hasMoreThan24HoursOfHistory.get(packageName), event.getTimeStamp()); - mAppStandby.setEstimatedLaunchTime( - packageName, userId, estimatedLaunchTime); - } - if (estimatedLaunchTime < now + ONE_WEEK) { - // Before a user is unlocked, we don't know when the app will be launched, - // so we give callers the UNKNOWN time. Now that we have a better estimate, - // we should notify them of the change. - if (DEBUG) { - Slog.d(TAG, "User " + userId + " unlock resulting in" - + " estimated launch time change for " + packageName); - } - changedTimes |= stageChangedEstimatedLaunchTime(userId, packageName); + hasMoreThan24HoursOfHistory.put(packageName, hasHistory); + } + if (event.getEventType() == Event.ACTIVITY_RESUMED) { + long estimatedLaunchTime = + mAppStandby.getEstimatedLaunchTime(packageName, userId); + if (estimatedLaunchTime < now || estimatedLaunchTime == Long.MAX_VALUE) { + //noinspection ConstantConditions + estimatedLaunchTime = calculateNextLaunchTime( + hasMoreThan24HoursOfHistory.get(packageName), event.getTimeStamp()); + mAppStandby.setEstimatedLaunchTime( + packageName, userId, estimatedLaunchTime); + } + if (estimatedLaunchTime < now + ONE_WEEK) { + // Before a user is unlocked, we don't know when the app will be launched, + // so we give callers the UNKNOWN time. Now that we have a better estimate, + // we should notify them of the change. + if (DEBUG) { + Slog.d(TAG, "User " + userId + " unlock resulting in" + + " estimated launch time change for " + packageName); } - alarmQueue.addAlarm(packageName, nowElapsed + (estimatedLaunchTime - now)); + changedTimes |= stageChangedEstimatedLaunchTime(userId, packageName); } - } - if (changedTimes) { - mHandler.sendEmptyMessage(MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED); + alarmQueue.addAlarm(packageName, nowElapsed + (estimatedLaunchTime - now)); } } + if (changedTimes) { + mHandler.sendEmptyMessage(MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED); + } } private void setEstimatedLaunchTime(int userId, String packageName, @@ -1989,37 +2035,11 @@ public class UsageStatsService extends SystemService implements case MSG_PACKAGE_REMOVED: onPackageRemoved(msg.arg1, (String) msg.obj); break; - case MSG_UID_STATE_CHANGED: { - final int uid = msg.arg1; - final int procState = msg.arg2; - - final int newCounter = (procState <= ActivityManager.PROCESS_STATE_TOP) ? 0 : 1; - synchronized (mUidToKernelCounter) { - final int oldCounter = mUidToKernelCounter.get(uid, 0); - if (newCounter != oldCounter) { - mUidToKernelCounter.put(uid, newCounter); - try { - FileUtils.stringToFile(KERNEL_COUNTER_FILE, uid + " " + newCounter); - } catch (IOException e) { - Slog.w(TAG, "Failed to update counter set: " + e); - } - } - } - break; - } case MSG_ON_START: synchronized (mLock) { loadGlobalComponentUsageLocked(); } break; - case MSG_HANDLE_LAUNCH_TIME_ON_USER_UNLOCK: { - final int userId = msg.arg1; - Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, - "usageStatsHandleEstimatedLaunchTimesOnUser(" + userId + ")"); - handleEstimatedLaunchTimesOnUserUnlock(userId); - Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); - } - break; case MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED: { removeMessages(MSG_NOTIFY_ESTIMATED_LAUNCH_TIMES_CHANGED); @@ -2587,11 +2607,12 @@ public class UsageStatsService extends SystemService implements @Override public void reportChooserSelection(@NonNull String packageName, int userId, @NonNull String contentType, String[] annotations, @NonNull String action) { - // A valid package name, content type, and action must be provided for these events - Objects.requireNonNull(packageName); - Objects.requireNonNull(contentType); - Objects.requireNonNull(action); - if (contentType.isBlank() || action.isBlank()) { + if (packageName == null) { + throw new IllegalArgumentException("Package selection must not be null."); + } + // A valid contentType and action must be provided for chooser selection events. + if (contentType == null || contentType.isBlank() + || action == null || action.isBlank()) { return; } |