diff options
46 files changed, 1920 insertions, 687 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index c1894f0f795f..a37779e681fb 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -3568,7 +3568,7 @@ public class DeviceIdleController extends SystemService Slog.i(TAG, "becomeActiveLocked, reason=" + activeReason + ", changeLightIdle=" + changeLightIdle); } - if (mState != STATE_ACTIVE || mLightState != STATE_ACTIVE) { + if (mState != STATE_ACTIVE || mLightState != LIGHT_STATE_ACTIVE) { moveToStateLocked(STATE_ACTIVE, activeReason); mInactiveTimeout = newInactiveTimeout; resetIdleManagementLocked(); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 4ef5b5163fef..64aa705447aa 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -11176,7 +11176,7 @@ public class Notification implements Parcelable } /** - * A Notification Style used to to define a notification whose expanded state includes + * A Notification Style used to define a notification whose expanded state includes * a highly customizable progress bar with segments, points, a custom tracker icon, * and custom icons at the start and end of the progress bar. * diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index 4a2b016456f7..ebe7b3a4fc51 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -38,6 +38,7 @@ import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.text.TextUtils; import android.util.Log; +import android.util.Slog; import android.util.proto.ProtoOutputStream; import com.android.internal.util.Preconditions; @@ -1369,12 +1370,17 @@ public final class NotificationChannel implements Parcelable { if (sound == null || Uri.EMPTY.equals(sound)) { return null; } - Uri canonicalSound = getCanonicalizedSoundUri(context.getContentResolver(), sound); - if (canonicalSound == null) { - // The content provider does not support canonical uris so we backup the default + try { + Uri canonicalSound = getCanonicalizedSoundUri(context.getContentResolver(), sound); + if (canonicalSound == null) { + // The content provider does not support canonical uris so we backup the default + return Settings.System.DEFAULT_NOTIFICATION_URI; + } + return canonicalSound; + } catch (Exception e) { + Slog.e(TAG, "Cannot find file for sound " + sound + " using default"); return Settings.System.DEFAULT_NOTIFICATION_URI; } - return canonicalSound; } /** diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 97f6899ff141..b0ea92d140a5 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -18,6 +18,7 @@ package android.hardware.display; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.HdrCapabilities.HdrType; +import static android.view.Display.INVALID_DISPLAY; import android.Manifest; import android.annotation.FlaggedApi; @@ -47,6 +48,7 @@ import android.os.Looper; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserManager; import android.util.Log; import android.util.Pair; import android.util.Slog; @@ -96,6 +98,8 @@ public final class DisplayManager { @GuardedBy("mLock") private final WeakDisplayCache mDisplayCache = new WeakDisplayCache(); + private int mDisplayIdToMirror = INVALID_DISPLAY; + /** * Broadcast receiver that indicates when the Wifi display status changes. * <p> @@ -1086,6 +1090,7 @@ public final class DisplayManager { if (surface != null) { builder.setSurface(surface); } + builder.setDisplayIdToMirror(getDisplayIdToMirror()); return createVirtualDisplay(builder.build(), handler, callback); } @@ -1163,6 +1168,7 @@ public final class DisplayManager { if (surface != null) { builder.setSurface(surface); } + builder.setDisplayIdToMirror(getDisplayIdToMirror()); return createVirtualDisplay(projection, builder.build(), callback, handler); } @@ -1708,6 +1714,16 @@ public final class DisplayManager { return mGlobal.getDefaultDozeBrightness(displayId); } + private int getDisplayIdToMirror() { + if (mDisplayIdToMirror == INVALID_DISPLAY) { + final UserManager userManager = mContext.getSystemService(UserManager.class); + mDisplayIdToMirror = userManager.isVisibleBackgroundUsersSupported() + ? userManager.getMainDisplayIdAssignedToUser() + : DEFAULT_DISPLAY; + } + return mDisplayIdToMirror; + } + /** * Listens for changes in available display devices. */ diff --git a/core/java/android/os/StatsBootstrapAtomValue.aidl b/core/java/android/os/StatsBootstrapAtomValue.aidl index b59bc062648f..b31eb6f78355 100644 --- a/core/java/android/os/StatsBootstrapAtomValue.aidl +++ b/core/java/android/os/StatsBootstrapAtomValue.aidl @@ -19,12 +19,36 @@ package android.os; * * @hide */ -union StatsBootstrapAtomValue { - boolean boolValue; - int intValue; - long longValue; - float floatValue; - String stringValue; - byte[] bytesValue; - String[] stringArrayValue; -}
\ No newline at end of file +parcelable StatsBootstrapAtomValue { + union Primitive { + boolean boolValue; + int intValue; + long longValue; + float floatValue; + String stringValue; + byte[] bytesValue; + String[] stringArrayValue; + } + + Primitive value; + + parcelable Annotation { + // Match the definitions in + // packages/modules/StatsD/framework/java/android/util/StatsLog.java + // Only supports UIDs for now. + @Backing(type="byte") + enum Id { + NONE, + IS_UID, + } + Id id; + + union Primitive { + boolean boolValue; + int intValue; + } + Primitive value; + } + + Annotation[] annotations; +} diff --git a/core/java/android/view/RoundScrollbarRenderer.java b/core/java/android/view/RoundScrollbarRenderer.java index 5f6d5e29570e..59c2598f00f0 100644 --- a/core/java/android/view/RoundScrollbarRenderer.java +++ b/core/java/android/view/RoundScrollbarRenderer.java @@ -16,17 +16,26 @@ package android.view; +import static android.util.MathUtils.acos; + +import static java.lang.Math.sin; + import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; +import android.os.SystemProperties; import android.util.DisplayMetrics; +import android.view.flags.Flags; /** * Helper class for drawing round scroll bars on round Wear devices. + * + * @hide */ -class RoundScrollbarRenderer { +public class RoundScrollbarRenderer { + private static final String BLUECHIP_ENABLED_SYSPROP = "persist.cw_build.bluechip.enabled"; // The range of the scrollbar position represented as an angle in degrees. private static final float SCROLLBAR_ANGLE_RANGE = 28.8f; private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 26.3f; // 90% @@ -45,12 +54,15 @@ class RoundScrollbarRenderer { private final Paint mTrackPaint = new Paint(); private final RectF mRect = new RectF(); private final View mParent; - private final int mMaskThickness; + private final float mInset; private float mPreviousMaxScroll = 0; private float mMaxScrollDiff = 0; private float mPreviousCurrentScroll = 0; private float mCurrentScrollDiff = 0; + private float mThumbStrokeWidthAsDegrees = 0; + private boolean mDrawToLeft; + private boolean mUseRefactoredRoundScrollbar; public RoundScrollbarRenderer(View parent) { // Paints for the round scrollbar. @@ -69,29 +81,36 @@ class RoundScrollbarRenderer { // Fetch the resource indicating the thickness of CircularDisplayMask, rounding in the same // way WindowManagerService.showCircularMask does. The scroll bar is inset by this amount so // that it doesn't get clipped. - mMaskThickness = parent.getContext().getResources().getDimensionPixelSize( - com.android.internal.R.dimen.circular_display_mask_thickness); - } + int maskThickness = + parent.getContext() + .getResources() + .getDimensionPixelSize( + com.android.internal.R.dimen.circular_display_mask_thickness); - public void drawRoundScrollbars(Canvas canvas, float alpha, Rect bounds, boolean drawToLeft) { - if (alpha == 0) { - return; - } - // Get information about the current scroll state of the parent view. - float maxScroll = mParent.computeVerticalScrollRange(); - float scrollExtent = mParent.computeVerticalScrollExtent(); - float newScroll = mParent.computeVerticalScrollOffset(); + float thumbWidth = dpToPx(THUMB_WIDTH_DP); + mThumbPaint.setStrokeWidth(thumbWidth); + mTrackPaint.setStrokeWidth(thumbWidth); + mInset = thumbWidth / 2 + maskThickness; + + mUseRefactoredRoundScrollbar = + Flags.useRefactoredRoundScrollbar() + && SystemProperties.getBoolean(BLUECHIP_ENABLED_SYSPROP, false); + } + private float computeScrollExtent(float scrollExtent, float maxScroll) { if (scrollExtent <= 0) { if (!mParent.canScrollVertically(1) && !mParent.canScrollVertically(-1)) { - return; + return -1f; } else { - scrollExtent = 0; + return 0f; } } else if (maxScroll <= scrollExtent) { - return; + return -1f; } + return scrollExtent; + } + private void resizeGradually(float maxScroll, float newScroll) { // Make changes to the VerticalScrollRange happen gradually if (Math.abs(maxScroll - mPreviousMaxScroll) > RESIZING_THRESHOLD_PX && mPreviousMaxScroll != 0) { @@ -106,51 +125,81 @@ class RoundScrollbarRenderer { || Math.abs(mCurrentScrollDiff) > RESIZING_THRESHOLD_PX) { mMaxScrollDiff *= RESIZING_RATE; mCurrentScrollDiff *= RESIZING_RATE; - - maxScroll -= mMaxScrollDiff; - newScroll -= mCurrentScrollDiff; } else { mMaxScrollDiff = 0; mCurrentScrollDiff = 0; } + } - float currentScroll = Math.max(0, newScroll); - float linearThumbLength = scrollExtent; - float thumbWidth = dpToPx(THUMB_WIDTH_DP); - mThumbPaint.setStrokeWidth(thumbWidth); - mTrackPaint.setStrokeWidth(thumbWidth); + public void drawRoundScrollbars(Canvas canvas, float alpha, Rect bounds, boolean drawToLeft) { + if (alpha == 0) { + return; + } + // Get information about the current scroll state of the parent view. + float maxScroll = mParent.computeVerticalScrollRange(); + float scrollExtent = mParent.computeVerticalScrollExtent(); + float newScroll = mParent.computeVerticalScrollOffset(); - setThumbColor(applyAlpha(DEFAULT_THUMB_COLOR, alpha)); - setTrackColor(applyAlpha(DEFAULT_TRACK_COLOR, alpha)); + scrollExtent = computeScrollExtent(scrollExtent, maxScroll); + if (scrollExtent < 0f) { + return; + } - // Normalize the sweep angle for the scroll bar. - float sweepAngle = (linearThumbLength / maxScroll) * SCROLLBAR_ANGLE_RANGE; - sweepAngle = clamp(sweepAngle, MIN_SCROLLBAR_ANGLE_SWIPE, MAX_SCROLLBAR_ANGLE_SWIPE); - // Normalize the start angle so that it falls on the track. - float startAngle = (currentScroll * (SCROLLBAR_ANGLE_RANGE - sweepAngle)) - / (maxScroll - linearThumbLength) - SCROLLBAR_ANGLE_RANGE / 2f; - startAngle = clamp(startAngle, -SCROLLBAR_ANGLE_RANGE / 2f, - SCROLLBAR_ANGLE_RANGE / 2f - sweepAngle); + // Make changes to the VerticalScrollRange happen gradually + resizeGradually(maxScroll, newScroll); + maxScroll -= mMaxScrollDiff; + newScroll -= mCurrentScrollDiff; - // Draw the track and the thumb. - float inset = thumbWidth / 2 + mMaskThickness; - mRect.set( - bounds.left + inset, - bounds.top + inset, - bounds.right - inset, - bounds.bottom - inset); - - if (drawToLeft) { - canvas.drawArc(mRect, 180 + SCROLLBAR_ANGLE_RANGE / 2f, -SCROLLBAR_ANGLE_RANGE, false, - mTrackPaint); - canvas.drawArc(mRect, 180 - startAngle, -sweepAngle, false, mThumbPaint); + applyThumbColor(alpha); + + float sweepAngle = computeSweepAngle(scrollExtent, maxScroll); + float startAngle = + computeStartAngle(Math.max(0, newScroll), sweepAngle, maxScroll, scrollExtent); + + updateBounds(bounds); + + mDrawToLeft = drawToLeft; + drawRoundScrollbars(canvas, startAngle, sweepAngle, alpha); + } + + private void drawRoundScrollbars( + Canvas canvas, float startAngle, float sweepAngle, float alpha) { + if (mUseRefactoredRoundScrollbar) { + draw(canvas, startAngle, sweepAngle, alpha); } else { - canvas.drawArc(mRect, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE, false, - mTrackPaint); - canvas.drawArc(mRect, startAngle, sweepAngle, false, mThumbPaint); + applyTrackColor(alpha); + drawArc(canvas, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE, mTrackPaint); + drawArc(canvas, startAngle, sweepAngle, mThumbPaint); } } + /** Returns true if horizontal bounds are updated */ + private void updateBounds(Rect bounds) { + mRect.set( + bounds.left + mInset, + bounds.top + mInset, + bounds.right - mInset, + bounds.bottom - mInset); + mThumbStrokeWidthAsDegrees = + getVertexAngle((mRect.right - mRect.left) / 2f, mThumbPaint.getStrokeWidth() / 2f); + } + + private float computeSweepAngle(float scrollExtent, float maxScroll) { + // Normalize the sweep angle for the scroll bar. + float sweepAngle = (scrollExtent / maxScroll) * SCROLLBAR_ANGLE_RANGE; + return clamp(sweepAngle, MIN_SCROLLBAR_ANGLE_SWIPE, MAX_SCROLLBAR_ANGLE_SWIPE); + } + + private float computeStartAngle( + float currentScroll, float sweepAngle, float maxScroll, float scrollExtent) { + // Normalize the start angle so that it falls on the track. + float startAngle = + (currentScroll * (SCROLLBAR_ANGLE_RANGE - sweepAngle)) / (maxScroll - scrollExtent) + - SCROLLBAR_ANGLE_RANGE / 2f; + return clamp( + startAngle, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE / 2f - sweepAngle); + } + void getRoundVerticalScrollBarBounds(Rect bounds) { float padding = dpToPx(OUTER_PADDING_DP); final int width = mParent.mRight - mParent.mLeft; @@ -164,10 +213,8 @@ class RoundScrollbarRenderer { private static float clamp(float val, float min, float max) { if (val < min) { return min; - } else if (val > max) { - return max; } else { - return val; + return Math.min(val, max); } } @@ -176,15 +223,17 @@ class RoundScrollbarRenderer { return Color.argb(alphaByte, Color.red(color), Color.green(color), Color.blue(color)); } - private void setThumbColor(int thumbColor) { - if (mThumbPaint.getColor() != thumbColor) { - mThumbPaint.setColor(thumbColor); + private void applyThumbColor(float alpha) { + int color = applyAlpha(DEFAULT_THUMB_COLOR, alpha); + if (mThumbPaint.getColor() != color) { + mThumbPaint.setColor(color); } } - private void setTrackColor(int trackColor) { - if (mTrackPaint.getColor() != trackColor) { - mTrackPaint.setColor(trackColor); + private void applyTrackColor(float alpha) { + int color = applyAlpha(DEFAULT_TRACK_COLOR, alpha); + if (mTrackPaint.getColor() != color) { + mTrackPaint.setColor(color); } } @@ -192,4 +241,88 @@ class RoundScrollbarRenderer { return dp * ((float) mParent.getContext().getResources().getDisplayMetrics().densityDpi) / DisplayMetrics.DENSITY_DEFAULT; } + + private static float getVertexAngle(float edge, float base) { + float edgeSquare = edge * edge * 2; + float baseSquare = base * base; + float gapInRadians = acos(((edgeSquare - baseSquare) / edgeSquare)); + return (float) Math.toDegrees(gapInRadians); + } + + private static float getKiteEdge(float knownEdge, float angleBetweenKnownEdgesInDegrees) { + return (float) (2 * knownEdge * sin(Math.toRadians(angleBetweenKnownEdgesInDegrees / 2))); + } + + private void draw(Canvas canvas, float thumbStartAngle, float thumbSweepAngle, float alpha) { + // Draws the top arc + drawTrack( + canvas, + // The highest point of the top track on a vertical scale. Here the thumb width is + // reduced to account for the arc formed by ROUND stroke style + -SCROLLBAR_ANGLE_RANGE / 2f - mThumbStrokeWidthAsDegrees, + // The lowest point of the top track on a vertical scale. Here the thumb width is + // reduced twice to (a) account for the arc formed by ROUND stroke style (b) gap + // between thumb and top track + thumbStartAngle - mThumbStrokeWidthAsDegrees * 2, + alpha); + // Draws the thumb + drawArc(canvas, thumbStartAngle, thumbSweepAngle, mThumbPaint); + // Draws the bottom arc + drawTrack( + canvas, + // The highest point of the bottom track on a vertical scale. Here the thumb width + // is added twice to (a) account for the arc formed by ROUND stroke style (b) gap + // between thumb and bottom track + (thumbStartAngle + thumbSweepAngle) + mThumbStrokeWidthAsDegrees * 2, + // The lowest point of the top track on a vertical scale. Here the thumb width is + // added to account for the arc formed by ROUND stroke style + SCROLLBAR_ANGLE_RANGE / 2f + mThumbStrokeWidthAsDegrees, + alpha); + } + + private void drawTrack(Canvas canvas, float beginAngle, float endAngle, float alpha) { + // Angular distance between end and begin + float angleBetweenEndAndBegin = endAngle - beginAngle; + // The sweep angle for the track is the angular distance between end and begin less the + // thumb width twice to account for top and bottom arc formed by the ROUND stroke style + float sweepAngle = angleBetweenEndAndBegin - 2 * mThumbStrokeWidthAsDegrees; + + float startAngle = -1f; + float strokeWidth = -1f; + if (sweepAngle > 0f) { + // The angle is greater than 0 which means a normal arc should be drawn with stroke + // width same as the thumb. The ROUND stroke style will cover the top/bottom arc of the + // track + startAngle = beginAngle + mThumbStrokeWidthAsDegrees; + strokeWidth = mThumbPaint.getStrokeWidth(); + } else if (Math.abs(sweepAngle) < 2 * mThumbStrokeWidthAsDegrees) { + // The sweep angle is less than 0 but is still relevant in creating a circle for the + // top/bottom track. The start angle is adjusted to account for being the mid point of + // begin / end angle. + startAngle = beginAngle + angleBetweenEndAndBegin / 2; + // The radius of this circle forms a kite with the radius of the arc drawn for the rect + // with the given angular difference between the arc radius which is used to compute the + // new stroke width. + strokeWidth = getKiteEdge(((mRect.right - mRect.left) / 2), angleBetweenEndAndBegin); + // The opacity is decreased proportionally, if the stroke width of the track is 50% or + // less that that of the thumb + alpha = alpha * Math.min(1f, 2 * strokeWidth / mThumbPaint.getStrokeWidth()); + // As we desire a circle to be drawn, the sweep angle is set to a minimal value + sweepAngle = Float.MIN_NORMAL; + } else { + return; + } + + applyTrackColor(alpha); + mTrackPaint.setStrokeWidth(strokeWidth); + drawArc(canvas, startAngle, sweepAngle, mTrackPaint); + } + + private void drawArc(Canvas canvas, float startAngle, float sweepAngle, Paint paint) { + if (mDrawToLeft) { + canvas.drawArc(mRect, /* startAngle= */ 180 - startAngle, -sweepAngle, false, paint); + } else { + canvas.drawArc(mRect, startAngle, sweepAngle, /* useCenter= */ false, paint); + } + } } diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig index 1cf26ab64c09..bb61ae49259c 100644 --- a/core/java/android/view/flags/view_flags.aconfig +++ b/core/java/android/view/flags/view_flags.aconfig @@ -116,4 +116,12 @@ flag { description: "Add a SurfaceView composition order control API." bug: "341021569" is_fixed_read_only: true +} + +flag { + name: "use_refactored_round_scrollbar" + namespace: "wear_frameworks" + description: "Use refactored round scrollbar." + bug: "333417898" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/jni/android_view_InputDevice.cpp b/core/jni/android_view_InputDevice.cpp index f5992d906323..ce40e51bee99 100644 --- a/core/jni/android_view_InputDevice.cpp +++ b/core/jni/android_view_InputDevice.cpp @@ -60,7 +60,7 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi ? layoutInfo->layoutType.c_str() : NULL)); - std::shared_ptr<KeyCharacterMap> map = deviceInfo.getKeyCharacterMap(); + const KeyCharacterMap* map = deviceInfo.getKeyCharacterMap(); std::unique_ptr<KeyCharacterMap> mapCopy; if (map != nullptr) { mapCopy = std::make_unique<KeyCharacterMap>(*map); diff --git a/core/tests/coretests/src/android/app/NotificationChannelTest.java b/core/tests/coretests/src/android/app/NotificationChannelTest.java index e19f887c1284..e4b54071e892 100644 --- a/core/tests/coretests/src/android/app/NotificationChannelTest.java +++ b/core/tests/coretests/src/android/app/NotificationChannelTest.java @@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -51,6 +52,7 @@ import android.platform.test.annotations.UsesFlags; import android.platform.test.flag.junit.FlagsParameterization; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.MediaStore.Audio.AudioColumns; +import android.provider.Settings; import android.test.mock.MockContentResolver; import android.util.Xml; @@ -399,6 +401,29 @@ public class NotificationChannelTest { } @Test + public void testWriteXmlForBackup_noAccessToFile() throws Exception { + Uri uri = Uri.parse("content://media/1"); + + AudioAttributes mAudioAttributes = + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + .build(); + + NotificationChannel channel = new NotificationChannel("id", "name", 3); + channel.setSound(uri, mAudioAttributes); + + when(mIContentProvider.canonicalize(any(), any())).thenThrow(new SecurityException("")); + doThrow(new SecurityException("")).when(mIContentProvider) + .canonicalizeAsync(any(), any(), any()); + + NotificationChannel restoredChannel = backUpAndRestore(channel); + assertThat(restoredChannel.getSound()) + .isEqualTo(Settings.System.DEFAULT_NOTIFICATION_URI); + } + + @Test public void testVibrationGetters_nonPatternBasedVibrationEffect_waveform() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API); NotificationChannel channel = new NotificationChannel("id", "name", 3); diff --git a/core/tests/coretests/src/android/view/RoundScrollbarRendererTest.java b/core/tests/coretests/src/android/view/RoundScrollbarRendererTest.java new file mode 100644 index 000000000000..262bd5cd6c01 --- /dev/null +++ b/core/tests/coretests/src/android/view/RoundScrollbarRendererTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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 android.view; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.flags.Flags; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link RoundScrollbarRenderer}. + * + * <p>Build/Install/Run: atest FrameworksCoreTests:android.view.RoundScrollbarRendererTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public class RoundScrollbarRendererTest { + + private static final int DEFAULT_VERTICAL_SCROLL_RANGE = 100; + private static final int DEFAULT_VERTICAL_SCROLL_EXTENT = 20; + private static final int DEFAULT_VERTICAL_SCROLL_OFFSET = 40; + private static final float DEFAULT_ALPHA = 0.5f; + private static final Rect BOUNDS = new Rect(0, 0, 200, 200); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Mock private Canvas mCanvas; + @Captor private ArgumentCaptor<Paint> mPaintCaptor; + private RoundScrollbarRenderer mScrollbar; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + MockView view = spy(new MockView(ApplicationProvider.getApplicationContext())); + when(view.canScrollVertically(anyInt())).thenReturn(true); + when(view.computeVerticalScrollRange()).thenReturn(DEFAULT_VERTICAL_SCROLL_RANGE); + when(view.computeVerticalScrollExtent()).thenReturn(DEFAULT_VERTICAL_SCROLL_EXTENT); + when(view.computeVerticalScrollOffset()).thenReturn(DEFAULT_VERTICAL_SCROLL_OFFSET); + mPaintCaptor = ArgumentCaptor.forClass(Paint.class); + + mScrollbar = new RoundScrollbarRenderer(view); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_USE_REFACTORED_ROUND_SCROLLBAR) + public void testScrollbarDrawn_legacy() { + mScrollbar.drawRoundScrollbars(mCanvas, DEFAULT_ALPHA, BOUNDS, /* drawToLeft= */ false); + + // The arc will be drawn twice, i.e. once for track and once for thumb + verify(mCanvas, times(2)) + .drawArc(any(), anyFloat(), anyFloat(), eq(false), mPaintCaptor.capture()); + + Paint thumbPaint = mPaintCaptor.getAllValues().getFirst(); + assertEquals(Paint.Cap.ROUND, thumbPaint.getStrokeCap()); + assertEquals(Paint.Style.STROKE, thumbPaint.getStyle()); + Paint trackPaint = mPaintCaptor.getAllValues().get(1); + assertEquals(Paint.Cap.ROUND, trackPaint.getStrokeCap()); + assertEquals(Paint.Style.STROKE, trackPaint.getStyle()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_USE_REFACTORED_ROUND_SCROLLBAR) + public void testScrollbarDrawn() { + mScrollbar.drawRoundScrollbars(mCanvas, DEFAULT_ALPHA, BOUNDS, /* drawToLeft= */ false); + + // The arc will be drawn thrice, i.e. twice for track and once for thumb + verify(mCanvas, times(3)) + .drawArc(any(), anyFloat(), anyFloat(), eq(false), mPaintCaptor.capture()); + + // Verify paint styles + Paint thumbPaint = mPaintCaptor.getAllValues().getFirst(); + assertEquals(Paint.Cap.ROUND, thumbPaint.getStrokeCap()); + assertEquals(Paint.Style.STROKE, thumbPaint.getStyle()); + Paint trackPaint = mPaintCaptor.getAllValues().get(1); + assertEquals(Paint.Cap.ROUND, trackPaint.getStrokeCap()); + assertEquals(Paint.Style.STROKE, trackPaint.getStyle()); + } + + public static class MockView extends View { + + public MockView(Context context) { + super(context); + } + + @Override + public int computeVerticalScrollRange() { + return super.getHeight(); + } + + @Override + public int computeVerticalScrollOffset() { + return super.computeVerticalScrollOffset(); + } + + @Override + public int computeVerticalScrollExtent() { + return super.computeVerticalScrollExtent(); + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt index 8201bbe4dc47..e7d2ef10b4ee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt @@ -30,8 +30,11 @@ import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.data.repository.contextualEducationRepository import com.android.systemui.education.data.repository.fakeEduClock import com.android.systemui.education.shared.model.EducationUiType +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType +import com.android.systemui.inputdevice.tutorial.tutorialSchedulerRepository import com.android.systemui.keyboard.data.repository.keyboardRepository import com.android.systemui.kosmos.testScope +import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener import com.android.systemui.testKosmos import com.android.systemui.touchpad.data.repository.touchpadRepository import com.android.systemui.user.data.repository.fakeUserRepository @@ -42,10 +45,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.verify import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @@ -56,14 +62,19 @@ class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : private val kosmos = testKosmos() private val testScope = kosmos.testScope private val contextualEduInteractor = kosmos.contextualEducationInteractor + private val repository = kosmos.contextualEducationRepository private val touchpadRepository = kosmos.touchpadRepository private val keyboardRepository = kosmos.keyboardRepository + private val tutorialSchedulerRepository = kosmos.tutorialSchedulerRepository private val userRepository = kosmos.fakeUserRepository + private val overviewProxyService = kosmos.mockOverviewProxyService private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor private val eduClock = kosmos.fakeEduClock private val minDurationForNextEdu = KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds + private val initialDelayElapsedDuration = + KeyboardTouchpadEduInteractor.initialDelayDuration + 1.seconds @Before fun setup() { @@ -271,6 +282,131 @@ class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : assertThat(model?.lastShortcutTriggeredTime).isEqualTo(eduClock.instant()) } + @Test + fun dataUpdatedOnIncrementSignalCountWhenTouchpadConnected() = + testScope.runTest { + assumeTrue(gestureType != ALL_APPS) + setUpForInitialDelayElapse() + touchpadRepository.setIsAnyTouchpadConnected(true) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountWhenTouchpadDisconnected() = + testScope.runTest { + setUpForInitialDelayElapse() + touchpadRepository.setIsAnyTouchpadConnected(false) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUpdatedOnIncrementSignalCountWhenKeyboardConnected() = + testScope.runTest { + assumeTrue(gestureType == ALL_APPS) + setUpForInitialDelayElapse() + keyboardRepository.setIsAnyKeyboardConnected(true) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountWhenKeyboardDisconnected() = + testScope.runTest { + setUpForInitialDelayElapse() + keyboardRepository.setIsAnyKeyboardConnected(false) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataAddedOnUpdateShortcutTriggerTime() = + testScope.runTest { + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + assertThat(model?.lastShortcutTriggeredTime).isNull() + + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType) + + assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant()) + } + + @Test + fun dataUpdatedOnIncrementSignalCountAfterInitialDelay() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant()) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + eduClock.offset(initialDelayElapsedDuration) + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountBeforeInitialDelay() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant()) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + // No offset to the clock to simulate update before initial delay + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() = + testScope.runTest { + // No update to OOBE launch time to simulate no OOBE is launched yet + setUpForDeviceConnection() + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + private suspend fun setUpForInitialDelayElapse() { + tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant()) + tutorialSchedulerRepository.updateLaunchTime(DeviceType.KEYBOARD, eduClock.instant()) + eduClock.offset(initialDelayElapsedDuration) + } + + @After + fun clear() { + testScope.launch { tutorialSchedulerRepository.clearDataStore() } + } + private suspend fun triggerMaxEducationSignals(gestureType: GestureType) { // Increment max number of signal to try triggering education for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) { @@ -288,9 +424,15 @@ class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : runCurrent() } - private suspend fun setUpForDeviceConnection() { - contextualEduInteractor.updateKeyboardFirstConnectionTime() - contextualEduInteractor.updateTouchpadFirstConnectionTime() + private fun setUpForDeviceConnection() { + touchpadRepository.setIsAnyTouchpadConnected(true) + keyboardRepository.setIsAnyKeyboardConnected(true) + } + + private fun getOverviewProxyListener(): OverviewProxyListener { + val listenerCaptor = argumentCaptor<OverviewProxyListener>() + verify(overviewProxyService).addCallback(listenerCaptor.capture()) + return listenerCaptor.firstValue } companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt deleted file mode 100644 index 98e09474d5f2..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.education.domain.interactor - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.contextualeducation.GestureType.ALL_APPS -import com.android.systemui.contextualeducation.GestureType.BACK -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.education.data.repository.contextualEducationRepository -import com.android.systemui.education.data.repository.fakeEduClock -import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType -import com.android.systemui.inputdevice.tutorial.tutorialSchedulerRepository -import com.android.systemui.keyboard.data.repository.keyboardRepository -import com.android.systemui.kosmos.testScope -import com.android.systemui.testKosmos -import com.android.systemui.touchpad.data.repository.touchpadRepository -import com.google.common.truth.Truth.assertThat -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class KeyboardTouchpadStatsInteractorTest : SysuiTestCase() { - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - private val underTest = kosmos.keyboardTouchpadEduStatsInteractor - private val keyboardRepository = kosmos.keyboardRepository - private val touchpadRepository = kosmos.touchpadRepository - private val repository = kosmos.contextualEducationRepository - private val fakeClock = kosmos.fakeEduClock - private val tutorialSchedulerRepository = kosmos.tutorialSchedulerRepository - private val initialDelayElapsedDuration = - KeyboardTouchpadEduStatsInteractorImpl.initialDelayDuration + 1.seconds - - @Test - fun dataUpdatedOnIncrementSignalCountWhenTouchpadConnected() = - testScope.runTest { - setUpForInitialDelayElapse() - touchpadRepository.setIsAnyTouchpadConnected(true) - - val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) - val originalValue = model!!.signalCount - underTest.incrementSignalCount(BACK) - - assertThat(model?.signalCount).isEqualTo(originalValue + 1) - } - - @Test - fun dataUnchangedOnIncrementSignalCountWhenTouchpadDisconnected() = - testScope.runTest { - setUpForInitialDelayElapse() - touchpadRepository.setIsAnyTouchpadConnected(false) - - val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) - val originalValue = model!!.signalCount - underTest.incrementSignalCount(BACK) - - assertThat(model?.signalCount).isEqualTo(originalValue) - } - - @Test - fun dataUpdatedOnIncrementSignalCountWhenKeyboardConnected() = - testScope.runTest { - setUpForInitialDelayElapse() - keyboardRepository.setIsAnyKeyboardConnected(true) - - val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS)) - val originalValue = model!!.signalCount - underTest.incrementSignalCount(ALL_APPS) - - assertThat(model?.signalCount).isEqualTo(originalValue + 1) - } - - @Test - fun dataUnchangedOnIncrementSignalCountWhenKeyboardDisconnected() = - testScope.runTest { - setUpForInitialDelayElapse() - keyboardRepository.setIsAnyKeyboardConnected(false) - - val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS)) - val originalValue = model!!.signalCount - underTest.incrementSignalCount(ALL_APPS) - - assertThat(model?.signalCount).isEqualTo(originalValue) - } - - @Test - fun dataAddedOnUpdateShortcutTriggerTime() = - testScope.runTest { - val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) - assertThat(model?.lastShortcutTriggeredTime).isNull() - underTest.updateShortcutTriggerTime(BACK) - assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant()) - } - - @Test - fun dataUpdatedOnIncrementSignalCountAfterInitialDelay() = - testScope.runTest { - setUpForDeviceConnection() - tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, fakeClock.instant()) - - fakeClock.offset(initialDelayElapsedDuration) - val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) - val originalValue = model!!.signalCount - underTest.incrementSignalCount(BACK) - - assertThat(model?.signalCount).isEqualTo(originalValue + 1) - } - - @Test - fun dataUnchangedOnIncrementSignalCountBeforeInitialDelay() = - testScope.runTest { - setUpForDeviceConnection() - tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, fakeClock.instant()) - - // No offset to the clock to simulate update before initial delay - val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) - val originalValue = model!!.signalCount - underTest.incrementSignalCount(BACK) - - assertThat(model?.signalCount).isEqualTo(originalValue) - } - - @Test - fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() = - testScope.runTest { - // No update to OOBE launch time to simulate no OOBE is launched yet - setUpForDeviceConnection() - - val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) - val originalValue = model!!.signalCount - underTest.incrementSignalCount(BACK) - - assertThat(model?.signalCount).isEqualTo(originalValue) - } - - private suspend fun setUpForInitialDelayElapse() { - tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, fakeClock.instant()) - tutorialSchedulerRepository.updateLaunchTime(DeviceType.KEYBOARD, fakeClock.instant()) - fakeClock.offset(initialDelayElapsedDuration) - } - - private fun setUpForDeviceConnection() { - touchpadRepository.setIsAnyTouchpadConnected(true) - keyboardRepository.setIsAnyKeyboardConnected(true) - } - - @After - fun clear() { - testScope.launch { tutorialSchedulerRepository.clearDataStore() } - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt index 1d80826d0b45..de3dc5730421 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt @@ -28,7 +28,6 @@ import com.android.internal.R import com.android.settingslib.notification.modes.TestModeBuilder import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestableContext -import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.asIcon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -145,13 +144,13 @@ class ModesTileDataInteractorTest : SysuiTestCase() { // Tile starts with the generic Modes icon. runCurrent() - assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON) + assertThat(tileData?.icon).isEqualTo(MODES_ICON) assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) // Add an inactive mode -> Still modes icon zenModeRepository.addMode(id = "Mode", active = false) runCurrent() - assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON) + assertThat(tileData?.icon).isEqualTo(MODES_ICON) assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) // Add an active mode with a default icon: icon should be the mode icon, and the @@ -159,7 +158,7 @@ class ModesTileDataInteractorTest : SysuiTestCase() { zenModeRepository.addMode( id = "Bedtime with default icon", type = AutomaticZenRule.TYPE_BEDTIME, - active = true, + active = true ) runCurrent() assertThat(tileData?.icon).isEqualTo(BEDTIME_ICON) @@ -190,7 +189,7 @@ class ModesTileDataInteractorTest : SysuiTestCase() { // Deactivate remaining mode: back to the default modes icon zenModeRepository.deactivateMode("Driving with custom icon") runCurrent() - assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON) + assertThat(tileData?.icon).isEqualTo(MODES_ICON) assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) } @@ -205,18 +204,18 @@ class ModesTileDataInteractorTest : SysuiTestCase() { ) runCurrent() - assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON) + assertThat(tileData?.icon).isEqualTo(MODES_ICON) assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) // Activate a Mode -> Icon doesn't change. zenModeRepository.addMode(id = "Mode", active = true) runCurrent() - assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON) + assertThat(tileData?.icon).isEqualTo(MODES_ICON) assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) zenModeRepository.deactivateMode(id = "Mode") runCurrent() - assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON) + assertThat(tileData?.icon).isEqualTo(MODES_ICON) assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) } @@ -264,7 +263,7 @@ class ModesTileDataInteractorTest : SysuiTestCase() { val BEDTIME_DRAWABLE = TestStubDrawable("bedtime") val CUSTOM_DRAWABLE = TestStubDrawable("custom") - val MODES_RESOURCE_ICON = Icon.Resource(MODES_DRAWABLE_ID, null) + val MODES_ICON = MODES_DRAWABLE.asIcon() val BEDTIME_ICON = BEDTIME_DRAWABLE.asIcon() val CUSTOM_ICON = CUSTOM_DRAWABLE.asIcon() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt index a58cb9ce25b3..c3d45dbbd09a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt @@ -22,9 +22,7 @@ import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.asIcon -import com.android.systemui.qs.tiles.ModesTile import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder import com.android.systemui.qs.tiles.viewmodel.QSTileState @@ -53,11 +51,6 @@ class ModesTileMapperTest : SysuiTestCase() { .apply { addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable()) addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable()) - addOverride( - ModesTile.ICON_RES_ID, - TestStubDrawable(ModesTile.ICON_RES_ID.toString()), - ) - addOverride(123, TestStubDrawable("123")) } .resources, context.theme, @@ -66,7 +59,12 @@ class ModesTileMapperTest : SysuiTestCase() { @Test fun inactiveState() { val icon = TestStubDrawable("res123").asIcon() - val model = ModesTileModel(isActivated = false, activeModes = emptyList(), icon = icon) + val model = + ModesTileModel( + isActivated = false, + activeModes = emptyList(), + icon = icon, + ) val state = underTest.map(config, model) @@ -78,7 +76,12 @@ class ModesTileMapperTest : SysuiTestCase() { @Test fun activeState_oneMode() { val icon = TestStubDrawable("res123").asIcon() - val model = ModesTileModel(isActivated = true, activeModes = listOf("DND"), icon = icon) + val model = + ModesTileModel( + isActivated = true, + activeModes = listOf("DND"), + icon = icon, + ) val state = underTest.map(config, model) @@ -105,36 +108,19 @@ class ModesTileMapperTest : SysuiTestCase() { } @Test - fun resourceIconModel_whenResIdsIdentical_mapsToLoadedIconWithInputResId() { - val icon = Icon.Resource(123, null) - val model = - ModesTileModel( - isActivated = false, - activeModes = emptyList(), - icon = icon, - iconResId = 123, - ) - - val state = underTest.map(config, model) - - assertThat(state.icon()).isEqualTo(TestStubDrawable("123").asIcon()) - assertThat(state.iconRes).isEqualTo(123) - } - - @Test - fun resourceIconModel_whenResIdsNonIdentical_mapsToLoadedIconWithIconResourceId() { - val icon = Icon.Resource(123, null) + fun state_modelHasIconResId_includesIconResId() { + val icon = TestStubDrawable("res123").asIcon() val model = ModesTileModel( isActivated = false, activeModes = emptyList(), icon = icon, - iconResId = 321, // Note: NOT 123. This will be ignored. + iconResId = 123 ) val state = underTest.map(config, model) - assertThat(state.icon()).isEqualTo(TestStubDrawable("123").asIcon()) + assertThat(state.icon()).isEqualTo(icon) assertThat(state.iconRes).isEqualTo(123) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/data/model/DisplayContentScenarios.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/data/model/DisplayContentScenarios.kt index 254f1e1efe13..4d71dc45001d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/data/model/DisplayContentScenarios.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/data/model/DisplayContentScenarios.kt @@ -21,8 +21,8 @@ import android.graphics.Rect import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FREE_FORM import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FULL_SCREEN import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.PIP -import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.SPLIT_BOTTOM -import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.SPLIT_TOP +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Orientation.HORIZONTAL +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Orientation.VERTICAL import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.emptyRootSplit import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.freeForm import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.fullScreen @@ -39,16 +39,14 @@ object DisplayContentScenarios { data class TaskSpec(val taskId: Int, val userId: Int, val name: String) + val emptyDisplayContent = DisplayContentModel(0, SystemUiState(shadeExpanded = false), listOf()) + /** Home screen, with only the launcher visible */ fun launcherOnly(shadeExpanded: Boolean = false) = DisplayContentModel( displayId = 0, systemUiState = SystemUiState(shadeExpanded = shadeExpanded), - rootTasks = - listOf( - launcher(visible = true), - emptyRootSplit, - ) + rootTasks = listOf(launcher(visible = true), emptyRootSplit), ) /** A Full screen activity for the personal (primary) user, with launcher behind it */ @@ -57,48 +55,72 @@ object DisplayContentScenarios { displayId = 0, systemUiState = SystemUiState(shadeExpanded = shadeExpanded), rootTasks = - listOf( - fullScreen(spec, visible = true), - launcher(visible = false), - emptyRootSplit, - ) + listOf(fullScreen(spec, visible = true), launcher(visible = false), emptyRootSplit), ) + enum class Orientation { + HORIZONTAL, + VERTICAL, + } + + internal fun Rect.splitLeft(margin: Int = 0) = Rect(left, top, centerX() - margin, bottom) + + internal fun Rect.splitRight(margin: Int = 0) = Rect(centerX() + margin, top, right, bottom) + + internal fun Rect.splitTop(margin: Int = 0) = Rect(left, top, right, centerY() - margin) + + internal fun Rect.splitBottom(margin: Int = 0) = Rect(left, centerY() + margin, right, bottom) + fun splitScreenApps( - top: TaskSpec, - bottom: TaskSpec, + displayId: Int = 0, + parentBounds: Rect = FULL_SCREEN, + taskMargin: Int = 0, + orientation: Orientation = VERTICAL, + first: TaskSpec, + second: TaskSpec, focusedTaskId: Int, + parentTaskId: Int = 2, shadeExpanded: Boolean = false, ): DisplayContentModel { - val topBounds = SPLIT_TOP - val bottomBounds = SPLIT_BOTTOM + + val firstBounds = + when (orientation) { + VERTICAL -> parentBounds.splitTop(taskMargin) + HORIZONTAL -> parentBounds.splitLeft(taskMargin) + } + val secondBounds = + when (orientation) { + VERTICAL -> parentBounds.splitBottom(taskMargin) + HORIZONTAL -> parentBounds.splitRight(taskMargin) + } + return DisplayContentModel( - displayId = 0, + displayId = displayId, systemUiState = SystemUiState(shadeExpanded = shadeExpanded), rootTasks = listOf( newRootTaskInfo( - taskId = 2, + taskId = parentTaskId, userId = TestUserIds.PERSONAL, - bounds = FULL_SCREEN, + bounds = parentBounds, topActivity = ComponentName.unflattenFromString( - if (top.taskId == focusedTaskId) top.name else bottom.name + if (first.taskId == focusedTaskId) first.name else second.name ), ) { listOf( newChildTask( - taskId = top.taskId, - bounds = topBounds, - userId = top.userId, - name = top.name + taskId = first.taskId, + bounds = firstBounds, + userId = first.userId, + name = first.name, ), newChildTask( - taskId = bottom.taskId, - bounds = bottomBounds, - userId = bottom.userId, - name = bottom.name - ) + taskId = second.taskId, + bounds = secondBounds, + userId = second.userId, + name = second.name, + ), ) // Child tasks are ordered bottom-up in RootTaskInfo. // Sort 'focusedTaskId' last. @@ -106,7 +128,7 @@ object DisplayContentScenarios { .sortedBy { it.id == focusedTaskId } }, launcher(visible = false), - ) + ), ) } @@ -124,7 +146,7 @@ object DisplayContentScenarios { fullScreen?.also { add(fullScreen(it, visible = true)) } add(launcher(visible = (fullScreen == null))) add(emptyRootSplit) - } + }, ) } @@ -142,7 +164,7 @@ object DisplayContentScenarios { return DisplayContentModel( displayId = 0, systemUiState = SystemUiState(shadeExpanded = shadeExpanded), - rootTasks = freeFormTasks + launcher(visible = true) + emptyRootSplit + rootTasks = freeFormTasks + launcher(visible = true) + emptyRootSplit, ) } @@ -153,11 +175,18 @@ object DisplayContentScenarios { * somewhat sensible in terms of logical position (Re: PIP, SPLIT, etc). */ object Bounds { + // "Phone" size val FULL_SCREEN = Rect(0, 0, 1080, 2400) val PIP = Rect(440, 1458, 1038, 1794) val SPLIT_TOP = Rect(0, 0, 1080, 1187) val SPLIT_BOTTOM = Rect(0, 1213, 1080, 2400) val FREE_FORM = Rect(119, 332, 1000, 1367) + + // "Tablet" size + val FREEFORM_FULL_SCREEN = Rect(0, 0, 2560, 1600) + val FREEFORM_MAXIMIZED = Rect(0, 48, 2560, 1480) + val FREEFORM_SPLIT_LEFT = Rect(0, 0, 1270, 1600) + val FREEFORM_SPLIT_RIGHT = Rect(1290, 0, 2560, 1600) } /** A collection of task names used in test scenarios */ @@ -177,6 +206,8 @@ object DisplayContentScenarios { "com.google.android.youtube/" + "com.google.android.apps.youtube.app.watchwhile.WatchWhileActivity" + const val MESSAGES = "com.google.android.apps.messaging/.ui.ConversationListActivity" + /** The NexusLauncher activity */ const val LAUNCHER = "com.google.android.apps.nexuslauncher/" + @@ -220,7 +251,7 @@ object DisplayContentScenarios { } /** NexusLauncher on the default display. Usually below all other visible tasks */ - fun launcher(visible: Boolean) = + fun launcher(visible: Boolean, bounds: Rect = FULL_SCREEN) = newRootTaskInfo( taskId = 1, activityType = ActivityType.Home, @@ -229,43 +260,63 @@ object DisplayContentScenarios { topActivity = ComponentName.unflattenFromString(ActivityNames.LAUNCHER), topActivityType = ActivityType.Home, ) { - listOf(newChildTask(taskId = 1002, name = ActivityNames.LAUNCHER)) + listOf(newChildTask(taskId = 1002, name = ActivityNames.LAUNCHER, bounds = bounds)) } /** A full screen Activity */ - fun fullScreen(task: TaskSpec, visible: Boolean) = + fun fullScreen(task: TaskSpec, visible: Boolean, bounds: Rect = FULL_SCREEN) = newRootTaskInfo( taskId = task.taskId, userId = task.userId, visible = visible, - bounds = FULL_SCREEN, + bounds = bounds, topActivity = ComponentName.unflattenFromString(task.name), ) { - listOf(newChildTask(taskId = task.taskId, userId = task.userId, name = task.name)) + listOf( + newChildTask( + taskId = task.taskId, + userId = task.userId, + name = task.name, + bounds = bounds, + ) + ) } /** An activity in Picture-in-Picture mode */ - fun pictureInPicture(task: TaskSpec) = + fun pictureInPicture(task: TaskSpec, bounds: Rect = PIP) = newRootTaskInfo( taskId = task.taskId, userId = task.userId, - bounds = PIP, windowingMode = WindowingMode.PictureInPicture, topActivity = ComponentName.unflattenFromString(task.name), ) { - listOf(newChildTask(taskId = task.taskId, userId = userId, name = task.name)) + listOf( + newChildTask( + taskId = task.taskId, + userId = userId, + name = task.name, + bounds = bounds, + ) + ) } /** An activity in FreeForm mode */ - fun freeForm(task: TaskSpec) = + fun freeForm(task: TaskSpec, bounds: Rect = FREE_FORM) = newRootTaskInfo( taskId = task.taskId, userId = task.userId, - bounds = FREE_FORM, + bounds = bounds, windowingMode = WindowingMode.Freeform, topActivity = ComponentName.unflattenFromString(task.name), ) { - listOf(newChildTask(taskId = task.taskId, userId = userId, name = task.name)) + listOf( + newChildTask( + taskId = task.taskId, + userId = userId, + name = task.name, + bounds = bounds, + ) + ) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/NewRootTaskInfo.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/NewRootTaskInfo.kt index 6c35b233ffec..cedf0c8a2c06 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/NewRootTaskInfo.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/NewRootTaskInfo.kt @@ -69,7 +69,7 @@ fun RootTaskInfo.newChildTask( taskId: Int, name: String, bounds: Rect? = null, - userId: Int? = null + userId: Int? = null, ): ChildTaskModel { return ChildTaskModel(taskId, name, bounds ?: this.bounds, userId ?: this.userId) } @@ -83,7 +83,7 @@ fun newRootTaskInfo( running: Boolean = true, activityType: ActivityType = Standard, windowingMode: WindowingMode = FullScreen, - bounds: Rect? = null, + bounds: Rect = Rect(), topActivity: ComponentName? = null, topActivityType: ActivityType = Standard, numActivities: Int? = null, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/PrivateProfilePolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/PrivateProfilePolicyTest.kt index 6e57761aafa6..b7f565df4a3c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/PrivateProfilePolicyTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/PrivateProfilePolicyTest.kt @@ -17,8 +17,8 @@ package com.android.systemui.screenshot.policy import android.content.ComponentName -import androidx.test.ext.junit.runners.AndroidJUnit4 import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.systemui.kosmos.Kosmos import com.android.systemui.screenshot.data.model.DisplayContentModel import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.FILES @@ -59,7 +59,7 @@ class PrivateProfilePolicyTest { policy.check( singleFullScreen( spec = TaskSpec(taskId = 1002, name = YOUTUBE, userId = PRIVATE), - shadeExpanded = true + shadeExpanded = true, ) ) @@ -93,8 +93,8 @@ class PrivateProfilePolicyTest { CaptureParameters( type = FullScreen(displayId = 0), component = ComponentName.unflattenFromString(YOUTUBE), - owner = UserHandle.of(PRIVATE) - ) + owner = UserHandle.of(PRIVATE), + ), ) ) } @@ -110,25 +110,20 @@ class PrivateProfilePolicyTest { listOf( fullScreen( TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL), - visible = true + visible = true, ), fullScreen( TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE), - visible = false + visible = false, ), launcher(visible = false), emptyRootSplit, - ) + ), ) ) assertThat(result) - .isEqualTo( - NotMatched( - PrivateProfilePolicy.NAME, - PrivateProfilePolicy.NO_VISIBLE_TASKS, - ) - ) + .isEqualTo(NotMatched(PrivateProfilePolicy.NAME, PrivateProfilePolicy.NO_VISIBLE_TASKS)) } @Test @@ -136,9 +131,9 @@ class PrivateProfilePolicyTest { val result = policy.check( splitScreenApps( - top = TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL), - bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE), - focusedTaskId = 1003 + first = TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL), + second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE), + focusedTaskId = 1003, ) ) @@ -150,8 +145,8 @@ class PrivateProfilePolicyTest { CaptureParameters( type = FullScreen(displayId = 0), component = ComponentName.unflattenFromString(YOUTUBE), - owner = UserHandle.of(PRIVATE) - ) + owner = UserHandle.of(PRIVATE), + ), ) ) } @@ -161,9 +156,9 @@ class PrivateProfilePolicyTest { val result = policy.check( splitScreenApps( - top = TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL), - bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE), - focusedTaskId = 1002 + first = TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL), + second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE), + focusedTaskId = 1002, ) ) @@ -175,8 +170,8 @@ class PrivateProfilePolicyTest { CaptureParameters( type = FullScreen(displayId = 0), component = ComponentName.unflattenFromString(FILES), - owner = UserHandle.of(PRIVATE) - ) + owner = UserHandle.of(PRIVATE), + ), ) ) } @@ -196,8 +191,8 @@ class PrivateProfilePolicyTest { CaptureParameters( type = FullScreen(displayId = 0), component = ComponentName.unflattenFromString(YOUTUBE_PIP), - owner = UserHandle.of(PRIVATE) - ) + owner = UserHandle.of(PRIVATE), + ), ) ) } @@ -220,8 +215,8 @@ class PrivateProfilePolicyTest { CaptureParameters( type = FullScreen(displayId = 0), component = ComponentName.unflattenFromString(YOUTUBE_PIP), - owner = UserHandle.of(PRIVATE) - ) + owner = UserHandle.of(PRIVATE), + ), ) ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/ScreenshotPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/ScreenshotPolicyTest.kt new file mode 100644 index 000000000000..28eb9fc1364b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/ScreenshotPolicyTest.kt @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.policy + +import android.content.ComponentName +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.FILES +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.LAUNCHER +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.MESSAGES +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.YOUTUBE +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FREEFORM_FULL_SCREEN +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FULL_SCREEN +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Orientation.VERTICAL +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.TaskSpec +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.freeFormApps +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.pictureInPictureApp +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.singleFullScreen +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.splitScreenApps +import com.android.systemui.screenshot.data.repository.profileTypeRepository +import com.android.systemui.screenshot.policy.CaptureType.FullScreen +import com.android.systemui.screenshot.policy.CaptureType.IsolatedTask +import com.android.systemui.screenshot.policy.CaptureType.RootTask +import com.android.systemui.screenshot.policy.TestUserIds.PERSONAL +import com.android.systemui.screenshot.policy.TestUserIds.PRIVATE +import com.android.systemui.screenshot.policy.TestUserIds.WORK +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ScreenshotPolicyTest { + private val kosmos = Kosmos() + + private val defaultComponent = ComponentName("default", "default") + private val defaultOwner = UserHandle.SYSTEM + + @Test + fun fullScreen_work() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + singleFullScreen(TaskSpec(taskId = 1002, name = FILES, userId = WORK)), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = IsolatedTask(taskId = 1002, taskBounds = FULL_SCREEN), + component = ComponentName.unflattenFromString(FILES), + owner = UserHandle.of(WORK), + ) + ) + } + + @Test + fun fullScreen_private() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + singleFullScreen(TaskSpec(taskId = 1002, name = YOUTUBE, userId = PRIVATE)), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = FullScreen(displayId = 0), + component = ComponentName.unflattenFromString(YOUTUBE), + owner = UserHandle.of(PRIVATE), + ) + ) + } + + @Test + fun splitScreen_workAndPersonal() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + splitScreenApps( + first = TaskSpec(taskId = 1002, name = FILES, userId = WORK), + second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL), + focusedTaskId = 1002, + ), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = FullScreen(displayId = 0), + component = ComponentName.unflattenFromString(YOUTUBE), + owner = UserHandle.of(PERSONAL), + ) + ) + } + + @Test + fun splitScreen_personalAndPrivate() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + splitScreenApps( + first = TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL), + second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE), + focusedTaskId = 1002, + ), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = FullScreen(displayId = 0), + component = ComponentName.unflattenFromString(YOUTUBE), + owner = UserHandle.of(PRIVATE), + ) + ) + } + + @Test + fun splitScreen_workAndPrivate() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + splitScreenApps( + first = TaskSpec(taskId = 1002, name = FILES, userId = WORK), + second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE), + focusedTaskId = 1002, + ), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = FullScreen(displayId = 0), + component = ComponentName.unflattenFromString(YOUTUBE), + owner = UserHandle.of(PRIVATE), + ) + ) + } + + @Test + fun splitScreen_twoWorkTasks() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + splitScreenApps( + parentTaskId = 1, + parentBounds = FREEFORM_FULL_SCREEN, + orientation = VERTICAL, + first = TaskSpec(taskId = 1002, name = FILES, userId = WORK), + second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = WORK), + focusedTaskId = 1002, + ), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = + RootTask( + parentTaskId = 1, + taskBounds = FREEFORM_FULL_SCREEN, + childTaskIds = listOf(1002, 1003), + ), + component = ComponentName.unflattenFromString(FILES), + owner = UserHandle.of(WORK), + ) + ) + } + + @Test + fun freeform_floatingWindows() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + freeFormApps( + TaskSpec(taskId = 1002, name = FILES, userId = WORK), + TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL), + focusedTaskId = 1003, + ), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = FullScreen(displayId = 0), + component = ComponentName.unflattenFromString(YOUTUBE), + owner = UserHandle.of(PERSONAL), + ) + ) + } + + @Test + fun freeform_floatingWindows_maximized() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + freeFormApps( + TaskSpec(taskId = 1002, name = FILES, userId = WORK), + TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL), + focusedTaskId = 1003, + ), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = FullScreen(displayId = 0), + component = ComponentName.unflattenFromString(YOUTUBE), + owner = UserHandle.of(PERSONAL), + ) + ) + } + + @Test + fun freeform_floatingWindows_withPrivate() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + freeFormApps( + TaskSpec(taskId = 1002, name = FILES, userId = WORK), + TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE), + TaskSpec(taskId = 1004, name = MESSAGES, userId = PERSONAL), + focusedTaskId = 1004, + ), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = FullScreen(displayId = 0), + component = ComponentName.unflattenFromString(YOUTUBE), + owner = UserHandle.of(PRIVATE), + ) + ) + } + + @Test + fun freeform_floating_workOnly() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + freeFormApps( + TaskSpec(taskId = 1002, name = FILES, userId = WORK), + focusedTaskId = 1002, + ), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = FullScreen(displayId = 0), + component = ComponentName.unflattenFromString(LAUNCHER), + owner = defaultOwner, + ) + ) + } + + @Test + fun fullScreen_shadeExpanded() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + singleFullScreen( + TaskSpec(taskId = 1002, name = FILES, userId = WORK), + shadeExpanded = true, + ), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = FullScreen(displayId = 0), + component = defaultComponent, + owner = defaultOwner, + ) + ) + } + + @Test + fun fullScreen_with_PictureInPicture() = runTest { + val policy = ScreenshotPolicy(kosmos.profileTypeRepository) + + val result = + policy.apply( + pictureInPictureApp( + pip = TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL), + fullScreen = TaskSpec(taskId = 1003, name = FILES, userId = WORK), + ), + defaultComponent, + defaultOwner, + ) + + assertThat(result) + .isEqualTo( + CaptureParameters( + type = IsolatedTask(taskId = 1003, taskBounds = FULL_SCREEN), + component = ComponentName.unflattenFromString(FILES), + owner = UserHandle.of(WORK), + ) + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt index be9fcc2be3a3..30a786c291cb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt @@ -31,13 +31,13 @@ import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Activi import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.YOUTUBE import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FREE_FORM import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FULL_SCREEN -import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.SPLIT_TOP import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks import com.android.systemui.screenshot.data.model.DisplayContentScenarios.TaskSpec import com.android.systemui.screenshot.data.model.DisplayContentScenarios.freeFormApps import com.android.systemui.screenshot.data.model.DisplayContentScenarios.pictureInPictureApp import com.android.systemui.screenshot.data.model.DisplayContentScenarios.singleFullScreen import com.android.systemui.screenshot.data.model.DisplayContentScenarios.splitScreenApps +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.splitTop import com.android.systemui.screenshot.data.model.SystemUiState import com.android.systemui.screenshot.data.repository.profileTypeRepository import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult @@ -69,6 +69,7 @@ class WorkProfilePolicyTest { @JvmField @Rule(order = 2) val mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock lateinit var mContext: Context + @Mock lateinit var mResources: Resources private val kosmos = Kosmos() @@ -94,17 +95,11 @@ class WorkProfilePolicyTest { DisplayContentModel( displayId = 0, systemUiState = SystemUiState(shadeExpanded = false), - rootTasks = listOf(RootTasks.emptyWithNoChildTasks) + rootTasks = listOf(RootTasks.emptyWithNoChildTasks), ) ) - assertThat(result) - .isEqualTo( - NotMatched( - WorkProfilePolicy.NAME, - WORK_TASK_NOT_TOP, - ) - ) + assertThat(result).isEqualTo(NotMatched(WorkProfilePolicy.NAME, WORK_TASK_NOT_TOP)) } @Test @@ -114,13 +109,7 @@ class WorkProfilePolicyTest { singleFullScreen(TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL)) ) - assertThat(result) - .isEqualTo( - NotMatched( - WorkProfilePolicy.NAME, - WORK_TASK_NOT_TOP, - ) - ) + assertThat(result).isEqualTo(NotMatched(WorkProfilePolicy.NAME, WORK_TASK_NOT_TOP)) } @Test @@ -129,17 +118,11 @@ class WorkProfilePolicyTest { policy.check( singleFullScreen( TaskSpec(taskId = 1002, name = FILES, userId = WORK), - shadeExpanded = true + shadeExpanded = true, ) ) - assertThat(result) - .isEqualTo( - NotMatched( - WorkProfilePolicy.NAME, - SHADE_EXPANDED, - ) - ) + assertThat(result).isEqualTo(NotMatched(WorkProfilePolicy.NAME, SHADE_EXPANDED)) } @Test @@ -156,7 +139,7 @@ class WorkProfilePolicyTest { type = IsolatedTask(taskId = 1002, taskBounds = FULL_SCREEN), component = ComponentName.unflattenFromString(FILES), owner = UserHandle.of(WORK), - ) + ), ) ) } @@ -166,9 +149,11 @@ class WorkProfilePolicyTest { val result = policy.check( splitScreenApps( - top = TaskSpec(taskId = 1002, name = FILES, userId = WORK), - bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL), - focusedTaskId = 1002 + parentBounds = FULL_SCREEN, + taskMargin = 20, + first = TaskSpec(taskId = 1002, name = FILES, userId = WORK), + second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL), + focusedTaskId = 1002, ) ) @@ -178,10 +163,10 @@ class WorkProfilePolicyTest { policy = WorkProfilePolicy.NAME, reason = WORK_TASK_IS_TOP, CaptureParameters( - type = IsolatedTask(taskId = 1002, taskBounds = SPLIT_TOP), + type = IsolatedTask(taskId = 1002, taskBounds = FULL_SCREEN.splitTop(20)), component = ComponentName.unflattenFromString(FILES), owner = UserHandle.of(WORK), - ) + ), ) ) } @@ -191,19 +176,13 @@ class WorkProfilePolicyTest { val result = policy.check( splitScreenApps( - top = TaskSpec(taskId = 1002, name = FILES, userId = WORK), - bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL), - focusedTaskId = 1003 + first = TaskSpec(taskId = 1002, name = FILES, userId = WORK), + second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL), + focusedTaskId = 1003, ) ) - assertThat(result) - .isEqualTo( - NotMatched( - WorkProfilePolicy.NAME, - WORK_TASK_NOT_TOP, - ) - ) + assertThat(result).isEqualTo(NotMatched(WorkProfilePolicy.NAME, WORK_TASK_NOT_TOP)) } @Test @@ -225,7 +204,7 @@ class WorkProfilePolicyTest { type = IsolatedTask(taskId = 1003, taskBounds = FULL_SCREEN), component = ComponentName.unflattenFromString(FILES), owner = UserHandle.of(WORK), - ) + ), ) ) } @@ -238,7 +217,7 @@ class WorkProfilePolicyTest { freeFormApps( TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL), TaskSpec(taskId = 1003, name = FILES, userId = WORK), - focusedTaskId = 1003 + focusedTaskId = 1003, ) ) @@ -251,7 +230,7 @@ class WorkProfilePolicyTest { type = IsolatedTask(taskId = 1003, taskBounds = FREE_FORM), component = ComponentName.unflattenFromString(FILES), owner = UserHandle.of(WORK), - ) + ), ) ) } @@ -264,16 +243,10 @@ class WorkProfilePolicyTest { freeFormApps( TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL), TaskSpec(taskId = 1003, name = FILES, userId = WORK), - focusedTaskId = 1003 + focusedTaskId = 1003, ) ) - assertThat(result) - .isEqualTo( - NotMatched( - WorkProfilePolicy.NAME, - DESKTOP_MODE_ENABLED, - ) - ) + assertThat(result).isEqualTo(NotMatched(WorkProfilePolicy.NAME, DESKTOP_MODE_ENABLED)) } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt index 0bcb58dee934..5f973917440c 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt @@ -175,8 +175,10 @@ constructor( actions.map { action -> UserSwitcherDropdownItemViewModel( icon = - Icon.Resource( - action.iconResourceId, + Icon.Loaded( + applicationContext.resources.getDrawable( + action.iconResourceId + ), contentDescription = null, ), text = Text.Resource(action.textResourceId), diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index a5b22775f3d5..c6be0dd76a06 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -34,6 +34,7 @@ import com.android.systemui.display.ui.viewmodel.ConnectingDisplayViewModel; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; +import com.android.systemui.education.dagger.ContextualEducationModule; import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialModule; import com.android.systemui.keyboard.shortcut.ShortcutHelperModule; import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule; @@ -153,6 +154,7 @@ import javax.inject.Named; VolumeModule.class, WallpaperModule.class, ShortcutHelperModule.class, + ContextualEducationModule.class, }) public abstract class ReferenceSystemUIModule { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index b55108d6ab1d..450863fb53c9 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -63,7 +63,6 @@ import com.android.systemui.display.DisplayModule; import com.android.systemui.doze.dagger.DozeComponent; import com.android.systemui.dreams.dagger.DreamModule; import com.android.systemui.dump.DumpManager; -import com.android.systemui.education.dagger.ContextualEducationModule; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.FlagDependenciesModule; import com.android.systemui.flags.FlagsModule; @@ -272,8 +271,7 @@ import javax.inject.Named; UserModule.class, UtilModule.class, NoteTaskModule.class, - WalletModule.class, - ContextualEducationModule.class + WalletModule.class }, subcomponents = { ComplicationComponent.class, diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt index 7fa7da192ad0..abe0289baec8 100644 --- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt +++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt @@ -18,15 +18,12 @@ package com.android.systemui.education.dagger import com.android.systemui.CoreStartable import com.android.systemui.Flags -import com.android.systemui.contextualeducation.GestureType import com.android.systemui.coroutines.newTracingContext import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.data.repository.ContextualEducationRepository import com.android.systemui.education.data.repository.UserContextualEducationRepository import com.android.systemui.education.domain.interactor.ContextualEducationInteractor import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor -import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor -import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl import com.android.systemui.education.ui.view.ContextualEduUiCoordinator import dagger.Binds import dagger.Lazy @@ -83,18 +80,6 @@ interface ContextualEducationModule { } @Provides - fun provideKeyboardTouchpadEduStatsInteractor( - implLazy: Lazy<KeyboardTouchpadEduStatsInteractorImpl> - ): KeyboardTouchpadEduStatsInteractor { - return if (Flags.keyboardTouchpadContextualEducation()) { - implLazy.get() - } else { - // No-op implementation when the flag is disabled. - return NoOpKeyboardTouchpadEduStatsInteractor - } - } - - @Provides @IntoMap @ClassKey(KeyboardTouchpadEduInteractor::class) fun provideKeyboardTouchpadEduInteractor( @@ -124,12 +109,6 @@ interface ContextualEducationModule { } } -private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor { - override fun incrementSignalCount(gestureType: GestureType) {} - - override fun updateShortcutTriggerTime(gestureType: GestureType) {} -} - private object NoOpCoreStartable : CoreStartable { override fun start() {} } diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt index faee32694964..c17f3fb6dfe4 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt @@ -18,6 +18,9 @@ package com.android.systemui.education.domain.interactor import android.os.SystemProperties import com.android.systemui.CoreStartable +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.dagger.ContextualEducationModule.EduClock @@ -25,6 +28,13 @@ import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.shared.model.EducationInfo import com.android.systemui.education.shared.model.EducationUiType import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD +import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository +import com.android.systemui.recents.OverviewProxyService +import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.time.Clock import javax.inject.Inject import kotlin.time.Duration @@ -33,9 +43,11 @@ import kotlin.time.DurationUnit import kotlin.time.toDuration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch @@ -48,6 +60,8 @@ constructor( @Background private val backgroundScope: CoroutineScope, private val contextualEducationInteractor: ContextualEducationInteractor, private val userInputDeviceRepository: UserInputDeviceRepository, + private val tutorialRepository: TutorialSchedulerRepository, + private val overviewProxyService: OverviewProxyService, @EduClock private val clock: Clock, ) : CoreStartable { @@ -59,14 +73,16 @@ constructor( getDurationForConfig("persist.contextual_edu.usage_session_sec", 3.days) val minIntervalBetweenEdu = getDurationForConfig("persist.contextual_edu.edu_interval_sec", 7.days) + val initialDelayDuration = + getDurationForConfig("persist.contextual_edu.initial_delay_sec", 7.days) private fun getDurationForConfig( systemPropertyKey: String, - defaultDuration: Duration + defaultDuration: Duration, ): Duration = SystemProperties.getLong( systemPropertyKey, - /* defaultValue= */ defaultDuration.inWholeSeconds + /* defaultValue= */ defaultDuration.inWholeSeconds, ) .toDuration(DurationUnit.SECONDS) } @@ -74,6 +90,24 @@ constructor( private val _educationTriggered = MutableStateFlow<EducationInfo?>(null) val educationTriggered = _educationTriggered.asStateFlow() + private val statsUpdateRequests: Flow<StatsUpdateRequest> = conflatedCallbackFlow { + val listener: OverviewProxyListener = + object : OverviewProxyListener { + override fun updateContextualEduStats( + isTrackpadGesture: Boolean, + gestureType: GestureType, + ) { + trySendWithFailureLogging( + StatsUpdateRequest(isTrackpadGesture, gestureType), + TAG, + ) + } + } + + overviewProxyService.addCallback(listener) + awaitClose { overviewProxyService.removeCallback(listener) } + } + @OptIn(ExperimentalCoroutinesApi::class) override fun start() { backgroundScope.launch { @@ -133,6 +167,16 @@ constructor( contextualEducationInteractor.updateShortcutTriggerTime(it) } } + + backgroundScope.launch { + statsUpdateRequests.collect { + if (it.isTrackpadGesture) { + contextualEducationInteractor.updateShortcutTriggerTime(it.gestureType) + } else { + incrementSignalCount(it.gestureType) + } + } + } } private fun isEducationNeeded(model: GestureEduModel): Boolean { @@ -160,4 +204,41 @@ constructor( private fun getEduType(model: GestureEduModel) = if (model.educationShownCount > 0) EducationUiType.Notification else EducationUiType.Toast + + private suspend fun incrementSignalCount(gestureType: GestureType) { + val targetDevice = getTargetDevice(gestureType) + if (isTargetDeviceConnected(targetDevice) && hasInitialDelayElapsed(targetDevice)) { + contextualEducationInteractor.incrementSignalCount(gestureType) + } + } + + private suspend fun isTargetDeviceConnected(deviceType: DeviceType): Boolean { + return when (deviceType) { + KEYBOARD -> userInputDeviceRepository.isAnyKeyboardConnectedForUser.first().isConnected + TOUCHPAD -> userInputDeviceRepository.isAnyTouchpadConnectedForUser.first().isConnected + } + } + + /** + * Keyboard shortcut education would be provided for All Apps. Touchpad gesture education would + * be provided for the rest of the gesture types (i.e. Home, Overview, Back). This method maps + * gesture to its target education device. + */ + private fun getTargetDevice(gestureType: GestureType) = + when (gestureType) { + ALL_APPS -> KEYBOARD + else -> TOUCHPAD + } + + private suspend fun hasInitialDelayElapsed(deviceType: DeviceType): Boolean { + val oobeLaunchTime = tutorialRepository.launchTime(deviceType) ?: return false + return clock + .instant() + .isAfter(oobeLaunchTime.plusSeconds(initialDelayDuration.inWholeSeconds)) + } + + private data class StatsUpdateRequest( + val isTrackpadGesture: Boolean, + val gestureType: GestureType, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt deleted file mode 100644 index 43e39cf08e01..000000000000 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.education.domain.interactor - -import android.os.SystemProperties -import com.android.systemui.contextualeducation.GestureType -import com.android.systemui.contextualeducation.GestureType.ALL_APPS -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.education.dagger.ContextualEducationModule.EduClock -import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository -import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType -import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD -import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD -import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository -import java.time.Clock -import javax.inject.Inject -import kotlin.time.Duration -import kotlin.time.Duration.Companion.days -import kotlin.time.DurationUnit -import kotlin.time.toDuration -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch - -/** - * Encapsulates the update functions of KeyboardTouchpadEduStatsInteractor. This encapsulation is - * for having a different implementation of interactor when the feature flag is off. - */ -interface KeyboardTouchpadEduStatsInteractor { - fun incrementSignalCount(gestureType: GestureType) - - fun updateShortcutTriggerTime(gestureType: GestureType) -} - -/** Allow update to education data related to keyboard/touchpad. */ -@SysUISingleton -class KeyboardTouchpadEduStatsInteractorImpl -@Inject -constructor( - @Background private val backgroundScope: CoroutineScope, - private val contextualEducationInteractor: ContextualEducationInteractor, - private val inputDeviceRepository: UserInputDeviceRepository, - private val tutorialRepository: TutorialSchedulerRepository, - @EduClock private val clock: Clock, -) : KeyboardTouchpadEduStatsInteractor { - - companion object { - val initialDelayDuration: Duration - get() = - SystemProperties.getLong( - "persist.contextual_edu.initial_delay_sec", - /* defaultValue= */ 7.days.inWholeSeconds, - ) - .toDuration(DurationUnit.SECONDS) - } - - override fun incrementSignalCount(gestureType: GestureType) { - backgroundScope.launch { - val targetDevice = getTargetDevice(gestureType) - if (isTargetDeviceConnected(targetDevice) && hasInitialDelayElapsed(targetDevice)) { - contextualEducationInteractor.incrementSignalCount(gestureType) - } - } - } - - override fun updateShortcutTriggerTime(gestureType: GestureType) { - backgroundScope.launch { - contextualEducationInteractor.updateShortcutTriggerTime(gestureType) - } - } - - private suspend fun isTargetDeviceConnected(deviceType: DeviceType): Boolean { - return when (deviceType) { - KEYBOARD -> inputDeviceRepository.isAnyKeyboardConnectedForUser.first().isConnected - TOUCHPAD -> inputDeviceRepository.isAnyTouchpadConnectedForUser.first().isConnected - } - } - - /** - * Keyboard shortcut education would be provided for All Apps. Touchpad gesture education would - * be provided for the rest of the gesture types (i.e. Home, Overview, Back). This method maps - * gesture to its target education device. - */ - private fun getTargetDevice(gestureType: GestureType) = - when (gestureType) { - ALL_APPS -> KEYBOARD - else -> TOUCHPAD - } - - private suspend fun hasInitialDelayElapsed(deviceType: DeviceType): Boolean { - val oobeLaunchTime = tutorialRepository.launchTime(deviceType) ?: return false - return clock - .instant() - .isAfter(oobeLaunchTime.plusSeconds(initialDelayDuration.inWholeSeconds)) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt b/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt index 62694ceffda8..ef7e7eb59898 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt @@ -22,18 +22,13 @@ import com.android.systemui.qs.tileimpl.QSTileImpl /** * Creates a [QSTile.Icon] from an [Icon]. - * * [Icon.Loaded] && [resId] null -> [QSTileImpl.DrawableIcon] - * * [Icon.Loaded] && [resId] available -> [QSTileImpl.DrawableIconWithRes] + * * [Icon.Loaded] -> [QSTileImpl.DrawableIcon] * * [Icon.Resource] -> [QSTileImpl.ResourceIcon] */ -fun Icon.asQSTileIcon(resId: Int?): QSTile.Icon { +fun Icon.asQSTileIcon(): QSTile.Icon { return when (this) { is Icon.Loaded -> { - if (resId != null) { - QSTileImpl.DrawableIconWithRes(this.drawable, resId) - } else { - QSTileImpl.DrawableIcon(this.drawable) - } + QSTileImpl.DrawableIcon(this.drawable) } is Icon.Resource -> { QSTileImpl.ResourceIcon.get(this.res) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt index 3bbe624595d9..cf2db6c66ce7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt @@ -121,7 +121,7 @@ constructor( state?.apply { this.state = tileState.activationState.legacyState val tileStateIcon = tileState.icon() - icon = tileStateIcon?.asQSTileIcon(tileState.iconRes) ?: ResourceIcon.get(ICON_RES_ID) + icon = tileStateIcon?.asQSTileIcon() ?: ResourceIcon.get(ICON_RES_ID) label = tileLabel secondaryLabel = tileState.secondaryLabel contentDescription = tileState.contentDescription diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt index cc14e71986f5..3e44258229f9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt @@ -78,14 +78,14 @@ constructor( } else { return ModesTileModel( isActivated = activeModes.isAnyActive(), - icon = Icon.Resource(ModesTile.ICON_RES_ID, null), + icon = context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(), iconResId = ModesTile.ICON_RES_ID, activeModes = activeModes.modeNames, ) } } - private data class TileIcon(val icon: Icon, val resId: Int?) + private data class TileIcon(val icon: Icon.Loaded, val resId: Int?) private fun getTileIcon(activeMode: ZenModeInfo?): TileIcon { return if (activeMode != null) { @@ -96,7 +96,7 @@ constructor( TileIcon(activeMode.icon.drawable.asIcon(), null) } } else { - TileIcon(Icon.Resource(ModesTile.ICON_RES_ID, null), ModesTile.ICON_RES_ID) + TileIcon(context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(), ModesTile.ICON_RES_ID) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt index 9c31e322dfd2..db4812342050 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt @@ -21,12 +21,12 @@ import com.android.systemui.common.shared.model.Icon data class ModesTileModel( val isActivated: Boolean, val activeModes: List<String>, - val icon: Icon, + val icon: Icon.Loaded, /** * Resource id corresponding to [icon]. Will only be present if it's know to correspond to a * resource with a known id in SystemUI (such as resources from `android.R`, * `com.android.internal.R`, or `com.android.systemui.res` itself). */ - val iconResId: Int? = null, + val iconResId: Int? = null ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt index 537b56bccae8..69da3134314b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt @@ -18,9 +18,7 @@ package com.android.systemui.qs.tiles.impl.modes.ui import android.content.res.Resources import android.icu.text.MessageFormat -import android.util.Log import android.widget.Button -import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel @@ -32,30 +30,14 @@ import javax.inject.Inject class ModesTileMapper @Inject -constructor(@Main private val resources: Resources, val theme: Resources.Theme) : - QSTileDataToStateMapper<ModesTileModel> { +constructor( + @Main private val resources: Resources, + val theme: Resources.Theme, +) : QSTileDataToStateMapper<ModesTileModel> { override fun map(config: QSTileConfig, data: ModesTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - val loadedIcon: Icon.Loaded = - when (val dataIcon = data.icon) { - is Icon.Resource -> { - if (data.iconResId != dataIcon.res) { - Log.wtf( - "ModesTileMapper", - "Icon.Resource.res & iconResId are not identical", - ) - } - iconRes = dataIcon.res - Icon.Loaded(resources.getDrawable(dataIcon.res, theme), null) - } - is Icon.Loaded -> { - iconRes = data.iconResId - dataIcon - } - } - - icon = { loadedIcon } - + iconRes = data.iconResId + icon = { data.icon } activationState = if (data.isActivated) { QSTileState.ActivationState.ACTIVE @@ -65,7 +47,10 @@ constructor(@Main private val resources: Resources, val theme: Resources.Theme) secondaryLabel = getModesStatus(data, resources) contentDescription = "$label. $secondaryLabel" supportedActions = - setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + setOf( + QSTileState.UserAction.CLICK, + QSTileState.UserAction.LONG_CLICK, + ) sideViewIcon = QSTileState.SideViewIcon.Chevron expandedAccessibilityClass = Button::class } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 559c2637ed4f..ce9c441654bf 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -87,7 +87,6 @@ import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; -import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.WakefulnessLifecycle; @@ -160,8 +159,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private final Provider<SceneInteractor> mSceneInteractor; private final Provider<ShadeInteractor> mShadeInteractor; - private final KeyboardTouchpadEduStatsInteractor mKeyboardTouchpadEduStatsInteractor; - private final Runnable mConnectionRunnable = () -> internalConnectToCurrentUser("runnable: startConnectionToCurrentUser"); private final ComponentName mRecentsComponentName; @@ -660,8 +657,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis AssistUtils assistUtils, DumpManager dumpManager, Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder, - BroadcastDispatcher broadcastDispatcher, - KeyboardTouchpadEduStatsInteractor keyboardTouchpadEduStatsInteractor + BroadcastDispatcher broadcastDispatcher ) { // b/241601880: This component should only be running for primary users or // secondaryUsers when visibleBackgroundUsers are supported. @@ -699,7 +695,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis mDisplayTracker = displayTracker; mUnfoldTransitionProgressForwarder = unfoldTransitionProgressForwarder; mBroadcastDispatcher = broadcastDispatcher; - mKeyboardTouchpadEduStatsInteractor = keyboardTouchpadEduStatsInteractor; if (!KeyguardWmStateRefactor.isEnabled()) { mSysuiUnlockAnimationController = sysuiUnlockAnimationController; @@ -940,19 +935,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis return isEnabled() && !QuickStepContract.isLegacyMode(mNavBarMode); } - /** - * Updates contextual education stats when a gesture is triggered - * @param isTrackpadGesture indicates if the gesture is triggered by trackpad - * @param gestureType type of gesture triggered - */ - public void updateContextualEduStats(boolean isTrackpadGesture, GestureType gestureType) { - if (isTrackpadGesture) { - mKeyboardTouchpadEduStatsInteractor.updateShortcutTriggerTime(gestureType); - } else { - mKeyboardTouchpadEduStatsInteractor.incrementSignalCount(gestureType); - } - } - public boolean isEnabled() { return mIsEnabled; } @@ -978,6 +960,17 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } } + /** + * Updates contextual education stats when a gesture is triggered + * @param isTrackpadGesture indicates if the gesture is triggered by trackpad + * @param gestureType type of gesture triggered + */ + public void updateContextualEduStats(boolean isTrackpadGesture, GestureType gestureType) { + for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { + mConnectionCallbacks.get(i).updateContextualEduStats(isTrackpadGesture, gestureType); + } + } + private void notifyHomeRotationEnabled(boolean enabled) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onHomeRotationEnabled(enabled); @@ -1207,6 +1200,9 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis /** Set override of home button long press duration, touch slop multiplier, and haptic. */ default void setOverrideHomeButtonLongPress( long override, float slopMultiplier, boolean haptic) {} + /** Updates contextual education stats when target gesture type is triggered. */ + default void updateContextualEduStats( + boolean isTrackpadGesture, GestureType gestureType) {} } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureType.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureType.kt index 0ef5207cad37..945520126474 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureType.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureType.kt @@ -24,8 +24,8 @@ sealed interface CaptureType { data class FullScreen(val displayId: Int) : CaptureType /** Capture the contents of the task only. */ - data class IsolatedTask( - val taskId: Int, - val taskBounds: Rect?, - ) : CaptureType + data class IsolatedTask(val taskId: Int, val taskBounds: Rect?) : CaptureType + + data class RootTask(val parentTaskId: Int, val taskBounds: Rect?, val childTaskIds: List<Int>) : + CaptureType } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt index 039143a1907d..e840668688a0 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt @@ -26,6 +26,7 @@ import android.os.UserHandle import android.util.Log import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE +import com.android.systemui.Flags.screenshotPolicySplitAndDesktopMode import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.screenshot.ImageCapture import com.android.systemui.screenshot.ScreenshotData @@ -47,14 +48,17 @@ class PolicyRequestProcessor( private val capture: ImageCapture, /** Provides information about the tasks on a given display */ private val displayTasks: DisplayContentRepository, - /** The list of policies to apply, in order of priority */ + /** The legacy list of policy implementations to apply, in order of priority */ private val policies: List<CapturePolicy>, + /** Implements the combined policy rules for all profile types. */ + private val policy: ScreenshotPolicy, /** The owner to assign for screenshot when a focused task isn't visible */ private val defaultOwner: UserHandle = myUserHandle(), /** The assigned component when no application has focus, or not visible */ private val defaultComponent: ComponentName, ) : ScreenshotRequestProcessor { override suspend fun process(original: ScreenshotData): ScreenshotData { + if (original.type == TAKE_SCREENSHOT_PROVIDED_IMAGE) { // The request contains an already captured screenshot, accept it as is. Log.i(TAG, "Screenshot bitmap provided. No modifications applied.") @@ -62,6 +66,12 @@ class PolicyRequestProcessor( } val displayContent = displayTasks.getDisplayContent(original.displayId) + if (screenshotPolicySplitAndDesktopMode()) { + Log.i(TAG, "Applying screenshot policy....") + val type = policy.apply(displayContent, defaultComponent, defaultOwner) + return modify(original, type) + } + // If policies yield explicit modifications, apply them and return the result Log.i(TAG, "Applying policy checks....") policies.map { policy -> @@ -79,10 +89,8 @@ class PolicyRequestProcessor( } /** Produce a new [ScreenshotData] using [CaptureParameters] */ - private suspend fun modify( - original: ScreenshotData, - updates: CaptureParameters, - ): ScreenshotData { + suspend fun modify(original: ScreenshotData, updates: CaptureParameters): ScreenshotData { + Log.d(TAG, "[modify] CaptureParameters = $updates") // Update and apply bitmap capture depending on the parameters. val updated = when (val type = updates.type) { @@ -94,6 +102,14 @@ class PolicyRequestProcessor( type.taskId, type.taskBounds, ) + is CaptureType.RootTask -> + replaceWithTaskSnapshot( + original, + updates.component, + updates.owner, + type.parentTaskId, + type.taskBounds, + ) is FullScreen -> replaceWithScreenshot( original, diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt index f768cfb2ceb5..dd39f92643ce 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt @@ -26,9 +26,11 @@ internal fun RootTaskInfo.childTasksTopDown(): Sequence<ChildTaskModel> { childTaskIds[index], childTaskNames[index], childTaskBounds[index], - childTaskUserIds[index] + childTaskUserIds[index], ) } } internal fun RootTaskInfo.hasChildTasks() = childTaskUserIds.isNotEmpty() + +internal fun RootTaskInfo.childTaskCount() = childTaskIds.size diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicy.kt new file mode 100644 index 000000000000..9967afffb6a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicy.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.policy + +import android.app.ActivityTaskManager.RootTaskInfo +import android.app.WindowConfiguration +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED +import android.content.ComponentName +import android.os.UserHandle +import android.util.Log +import com.android.systemui.screenshot.data.model.DisplayContentModel +import com.android.systemui.screenshot.data.model.ProfileType +import com.android.systemui.screenshot.data.model.ProfileType.PRIVATE +import com.android.systemui.screenshot.data.model.ProfileType.WORK +import com.android.systemui.screenshot.data.repository.ProfileTypeRepository +import com.android.systemui.screenshot.policy.CaptureType.FullScreen +import com.android.systemui.screenshot.policy.CaptureType.IsolatedTask +import com.android.systemui.screenshot.policy.CaptureType.RootTask +import javax.inject.Inject + +private const val TAG = "ScreenshotPolicy" + +/** Determines what to capture and which user owns the output. */ +class ScreenshotPolicy @Inject constructor(private val profileTypes: ProfileTypeRepository) { + /** + * Apply the policy to the content, resulting in [CaptureParameters]. + * + * @param content the content of the display + * @param defaultComponent the component associated with the screenshot by default + * @param defaultOwner the user to own the screenshot by default + */ + suspend fun apply( + content: DisplayContentModel, + defaultComponent: ComponentName, + defaultOwner: UserHandle, + ): CaptureParameters { + val defaultFullScreen by lazy { + CaptureParameters( + type = FullScreen(displayId = content.displayId), + component = defaultComponent, + owner = defaultOwner, + ) + } + + // When the systemUI notification shade is open, disregard tasks. + if (content.systemUiState.shadeExpanded) { + return defaultFullScreen + } + + // find the first (top) RootTask which is visible and not Picture-in-Picture + val topRootTask = + content.rootTasks.firstOrNull { + it.isVisible && it.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED + } ?: return defaultFullScreen + + Log.d(TAG, "topRootTask: $topRootTask") + val rootTaskOwners = topRootTask.childTaskUserIds.distinct() + + // Special case: Only WORK in top root task which is full-screen or maximized freeform + if ( + rootTaskOwners.size == 1 && + profileTypes.getProfileType(rootTaskOwners.single()) == WORK && + (topRootTask.isFullScreen() || topRootTask.isMaximizedFreeform()) + ) { + val type = + if (topRootTask.childTaskCount() > 1) { + RootTask( + parentTaskId = topRootTask.taskId, + taskBounds = topRootTask.bounds, + childTaskIds = topRootTask.childTasksTopDown().map { it.id }.toList(), + ) + } else { + IsolatedTask( + taskId = topRootTask.childTasksTopDown().first().id, + taskBounds = topRootTask.bounds, + ) + } + // Capture the RootTask (and all children) + return CaptureParameters( + type = type, + component = topRootTask.topActivity, + owner = UserHandle.of(rootTaskOwners.single()), + ) + } + + // In every other case the output will be a full screen capture regardless of content. + // For this reason, consider all owners of all visible content on the display (in all + // root tasks). This includes all root tasks in free-form mode. + val visibleChildTasks = + content.rootTasks.filter { it.isVisible }.flatMap { it.childTasksTopDown() } + + val allVisibleProfileTypes = + visibleChildTasks + .map { it.userId } + .distinct() + .associate { profileTypes.getProfileType(it) to UserHandle.of(it) } + + // If any visible content belongs to the private profile user -> private profile + // otherwise the personal user (including partial screen work content). + val ownerHandle = + allVisibleProfileTypes[PRIVATE] + ?: allVisibleProfileTypes[ProfileType.NONE] + ?: defaultOwner + + // Attribute to the component of top-most task owned by this user (or fallback to default) + val topComponent = + visibleChildTasks.firstOrNull { it.userId == ownerHandle.identifier }?.componentName + + return CaptureParameters( + type = FullScreen(content.displayId), + component = topComponent ?: topRootTask.topActivity ?: defaultComponent, + owner = ownerHandle, + ) + } + + private fun RootTaskInfo.isFullScreen(): Boolean = + configuration.windowConfiguration.windowingMode == WINDOWING_MODE_FULLSCREEN + + private fun RootTaskInfo.isMaximizedFreeform(): Boolean { + val bounds = configuration.windowConfiguration.bounds + val maxBounds = configuration.windowConfiguration.maxBounds + + if ( + windowingMode != WINDOWING_MODE_FREEFORM || + childTaskCount() != 1 || + childTaskBounds[0] != bounds + ) { + return false + } + + // Maximized floating windows fill maxBounds width + if (bounds.width() != maxBounds.width()) { + return false + } + + // Maximized floating windows fill nearly all the height + return (bounds.height().toFloat() / maxBounds.height()) >= 0.89f + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt index 2cb9fe7f1a9d..a9c6370bb776 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt @@ -37,7 +37,6 @@ import kotlinx.coroutines.CoroutineDispatcher @Module interface ScreenshotPolicyModule { - @Binds @SysUISingleton fun bindProfileTypeRepository(impl: ProfileTypeRepositoryImpl): ProfileTypeRepository @@ -67,6 +66,7 @@ interface ScreenshotPolicyModule { imageCapture: ImageCapture, displayContentRepo: DisplayContentRepository, policyListProvider: Provider<List<CapturePolicy>>, + standardPolicy: ScreenshotPolicy, ): ScreenshotRequestProcessor { return PolicyRequestProcessor( background = background, @@ -75,7 +75,8 @@ interface ScreenshotPolicyModule { policies = policyListProvider.get(), defaultOwner = Process.myUserHandle(), defaultComponent = - ComponentName(context.packageName, SystemUIService::class.java.toString()) + ComponentName(context.packageName, SystemUIService::class.java.toString()), + policy = standardPolicy, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt index 29450a20b1d3..cf90c0a58e94 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt @@ -28,7 +28,6 @@ import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult.NotMatc import com.android.systemui.screenshot.policy.CaptureType.IsolatedTask import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import javax.inject.Inject -import kotlinx.coroutines.flow.first /** * Condition: When the top visible task (excluding PIP mode) belongs to a work user. @@ -37,10 +36,8 @@ import kotlinx.coroutines.flow.first */ class WorkProfilePolicy @Inject -constructor( - private val profileTypes: ProfileTypeRepository, - private val context: Context, -) : CapturePolicy { +constructor(private val profileTypes: ProfileTypeRepository, private val context: Context) : + CapturePolicy { override suspend fun check(content: DisplayContentModel): PolicyResult { // The systemUI notification shade isn't a work app, skip. @@ -65,11 +62,7 @@ constructor( .map { it to it.childTasksTopDown().first() } .firstOrNull { (_, child) -> profileTypes.getProfileType(child.userId) == ProfileType.WORK - } - ?: return NotMatched( - policy = NAME, - reason = WORK_TASK_NOT_TOP, - ) + } ?: return NotMatched(policy = NAME, reason = WORK_TASK_NOT_TOP) // If matched, return parameters needed to modify the request. return PolicyResult.Matched( @@ -79,7 +72,7 @@ constructor( type = IsolatedTask(taskId = childTask.id, taskBounds = childTask.bounds), component = childTask.componentName ?: rootTask.topActivity, owner = UserHandle.of(childTask.userId), - ) + ), ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt index 4959224ead2d..3bfde68def50 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt @@ -35,7 +35,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager -import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager @@ -122,9 +121,6 @@ class OverviewProxyServiceTest : SysuiTestCase() { Optional<UnfoldTransitionProgressForwarder> @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock - private lateinit var keyboardTouchpadEduStatsInteractor: KeyboardTouchpadEduStatsInteractor - @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -293,7 +289,6 @@ class OverviewProxyServiceTest : SysuiTestCase() { dumpManager, unfoldTransitionProgressForwarder, broadcastDispatcher, - keyboardTouchpadEduStatsInteractor, ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt index 0d4cb4c6751c..7709a65712a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt @@ -20,30 +20,141 @@ import android.graphics.Bitmap import android.graphics.Insets import android.graphics.Rect import android.os.UserHandle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.systemui.Flags +import com.android.systemui.kosmos.Kosmos import com.android.systemui.screenshot.ImageCapture import com.android.systemui.screenshot.ScreenshotData import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.FILES +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FULL_SCREEN import com.android.systemui.screenshot.data.model.DisplayContentScenarios.TaskSpec +import com.android.systemui.screenshot.data.model.DisplayContentScenarios.emptyDisplayContent import com.android.systemui.screenshot.data.model.DisplayContentScenarios.launcherOnly import com.android.systemui.screenshot.data.model.DisplayContentScenarios.singleFullScreen import com.android.systemui.screenshot.data.repository.DisplayContentRepository +import com.android.systemui.screenshot.data.repository.profileTypeRepository +import com.android.systemui.screenshot.policy.CaptureType.FullScreen +import com.android.systemui.screenshot.policy.CaptureType.IsolatedTask import com.android.systemui.screenshot.policy.TestUserIds.PERSONAL import com.android.systemui.screenshot.policy.TestUserIds.WORK import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PolicyRequestProcessorTest { + private val kosmos = Kosmos() + + private val screenshotRequest = + ScreenshotData( + TAKE_SCREENSHOT_FULLSCREEN, + SCREENSHOT_KEY_CHORD, + UserHandle.CURRENT, + topComponent = null, + originalScreenBounds = FULL_SCREEN, + taskId = -1, + originalInsets = Insets.NONE, + bitmap = null, + displayId = DEFAULT_DISPLAY, + ) + + val defaultComponent = ComponentName("default", "Component") + val defaultOwner = UserHandle.of(PERSONAL) + + @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule() + + /** Tests applying CaptureParameters with 'IsolatedTask' CaptureType */ + @Test + @EnableFlags(Flags.FLAG_SCREENSHOT_POLICY_SPLIT_AND_DESKTOP_MODE) + fun testProcess_newPolicy_isolatedTask() = runTest { + val taskImage = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + + /* Create a policy request processor with no capture policies */ + val requestProcessor = + PolicyRequestProcessor( + Dispatchers.Unconfined, + createImageCapture(task = taskImage), + policy = ScreenshotPolicy(kosmos.profileTypeRepository), + policies = emptyList(), + defaultOwner = defaultOwner, + defaultComponent = defaultComponent, + displayTasks = { emptyDisplayContent }, + ) + + val result = + requestProcessor.modify( + screenshotRequest, + CaptureParameters( + IsolatedTask(taskId = TASK_ID, taskBounds = null), + ComponentName.unflattenFromString(FILES), + UserHandle.of(WORK), + ), + ) + + assertWithMessage("The screenshot bitmap").that(result.bitmap).isSameInstanceAs(taskImage) + + assertWithMessage("The assigned owner of the screenshot") + .that(result.userHandle) + .isEqualTo(UserHandle.of(WORK)) + + assertWithMessage("The topComponent of the screenshot") + .that(result.topComponent) + .isEqualTo(ComponentName.unflattenFromString(FILES)) + + assertWithMessage("Task ID").that(result.taskId).isEqualTo(TASK_ID) + } + + /** Tests applying CaptureParameters with 'FullScreen' CaptureType */ + @Test + @EnableFlags(Flags.FLAG_SCREENSHOT_POLICY_SPLIT_AND_DESKTOP_MODE) + fun testProcess_newPolicy_fullScreen() = runTest { + val screenImage = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + + /* Create a policy request processor with no capture policies */ + val requestProcessor = + PolicyRequestProcessor( + Dispatchers.Unconfined, + createImageCapture(display = screenImage), + policy = ScreenshotPolicy(kosmos.profileTypeRepository), + policies = emptyList(), + defaultOwner = defaultOwner, + defaultComponent = defaultComponent, + displayTasks = { emptyDisplayContent }, + ) + + val result = + requestProcessor.modify( + screenshotRequest, + CaptureParameters(FullScreen(displayId = 0), defaultComponent, defaultOwner), + ) + + assertWithMessage("The result bitmap").that(result.bitmap).isSameInstanceAs(screenImage) + + assertWithMessage("The assigned owner of the screenshot") + .that(result.userHandle) + .isEqualTo(defaultOwner) + + assertWithMessage("The topComponent of the screenshot") + .that(result.topComponent) + .isEqualTo(defaultComponent) + + assertWithMessage("Task ID").that(result.taskId).isEqualTo(-1) + } + /** Tests behavior when no policies are applied */ @Test + @DisableFlags(Flags.FLAG_SCREENSHOT_POLICY_SPLIT_AND_DESKTOP_MODE) fun testProcess_defaultOwner_whenNoPolicyApplied() { val fullScreenWork = DisplayContentRepository { singleFullScreen(TaskSpec(taskId = TASK_ID, name = FILES, userId = WORK)) @@ -67,6 +178,7 @@ class PolicyRequestProcessorTest { PolicyRequestProcessor( Dispatchers.Unconfined, createImageCapture(), + policy = ScreenshotPolicy(kosmos.profileTypeRepository), policies = emptyList(), defaultOwner = UserHandle.of(PERSONAL), defaultComponent = ComponentName("default", "Component"), @@ -95,6 +207,7 @@ class PolicyRequestProcessorTest { PolicyRequestProcessor( Dispatchers.Unconfined, createImageCapture(display = null), + policy = ScreenshotPolicy(kosmos.profileTypeRepository), policies = emptyList(), defaultComponent = ComponentName("default", "Component"), displayTasks = DisplayContentRepository { launcherOnly() }, @@ -118,7 +231,7 @@ class PolicyRequestProcessorTest { reason = "", parameters = CaptureParameters( - CaptureType.IsolatedTask(taskId = 0, taskBounds = null), + IsolatedTask(taskId = 0, taskBounds = null), null, UserHandle.CURRENT, ), @@ -130,6 +243,7 @@ class PolicyRequestProcessorTest { PolicyRequestProcessor( Dispatchers.Unconfined, createImageCapture(task = null), + policy = ScreenshotPolicy(kosmos.profileTypeRepository), policies = listOf(captureTaskPolicy), defaultComponent = ComponentName("default", "Component"), displayTasks = fullScreenWork, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt index 2d275f9e9691..3fd2503096b5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt @@ -24,6 +24,7 @@ import com.android.systemui.keyboard.data.repository.keyboardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.recents.OverviewProxyService import com.android.systemui.touchpad.data.repository.touchpadRepository import com.android.systemui.user.data.repository.userRepository import org.mockito.kotlin.mock @@ -38,27 +39,13 @@ var Kosmos.keyboardTouchpadEduInteractor by testDispatcher, keyboardRepository, touchpadRepository, - userRepository + userRepository, ), - clock = fakeEduClock + tutorialSchedulerRepository, + mockOverviewProxyService, + clock = fakeEduClock, ) } +var Kosmos.mockOverviewProxyService by Kosmos.Fixture { mock<OverviewProxyService>() } var Kosmos.mockEduInputManager by Kosmos.Fixture { mock<InputManager>() } - -var Kosmos.keyboardTouchpadEduStatsInteractor by - Kosmos.Fixture { - KeyboardTouchpadEduStatsInteractorImpl( - backgroundScope = testScope.backgroundScope, - contextualEducationInteractor = contextualEducationInteractor, - inputDeviceRepository = - UserInputDeviceRepository( - testDispatcher, - keyboardRepository, - touchpadRepository, - userRepository - ), - tutorialSchedulerRepository, - fakeEduClock - ) - } diff --git a/services/core/java/com/android/server/net/OWNERS b/services/core/java/com/android/server/net/OWNERS index bbc7c013f170..4596a445ed30 100644 --- a/services/core/java/com/android/server/net/OWNERS +++ b/services/core/java/com/android/server/net/OWNERS @@ -2,7 +2,5 @@ set noparent file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking per-file NetworkPolicyManagerService.java=jackyu@google.com, sarahchin@google.com -jsharkey@android.com sudheersai@google.com -yamasani@google.com suprabh@google.com diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index 93482e769a2b..122836e19d58 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -21,6 +21,9 @@ import static android.content.Context.BIND_ALLOW_WHITELIST_MANAGEMENT; import static android.content.Context.BIND_AUTO_CREATE; import static android.content.Context.BIND_FOREGROUND_SERVICE; import static android.content.Context.DEVICE_POLICY_SERVICE; +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; +import static android.content.pm.PackageManager.MATCH_INSTANT; import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND; @@ -106,7 +109,8 @@ abstract public class ManagedServices { protected final String TAG = getClass().getSimpleName().replace('$', '.'); protected final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private static final int ON_BINDING_DIED_REBIND_DELAY_MS = 10000; + protected static final int ON_BINDING_DIED_REBIND_DELAY_MS = 10000; + protected static final int ON_BINDING_DIED_REBIND_MSG = 1234; protected static final String ENABLED_SERVICES_SEPARATOR = ":"; private static final String DB_VERSION_1 = "1"; private static final String DB_VERSION_2 = "2"; @@ -875,7 +879,21 @@ abstract public class ManagedServices { String approvedItem = getApprovedValue(pkgOrComponent); if (approvedItem != null) { + final ComponentName component = ComponentName.unflattenFromString(approvedItem); if (enabled) { + if (Flags.notificationNlsRebind()) { + if (component != null && !isValidService(component, userId)) { + // Only fail if package is available + // If not, it will be validated again in onPackagesChanged + final PackageManager pm = mContext.getPackageManager(); + if (pm.isPackageAvailable(component.getPackageName())) { + Slog.w(TAG, "Skip allowing " + mConfig.caption + + " " + pkgOrComponent + " (userSet: " + userSet + + ") for invalid service"); + return; + } + } + } approved.add(approvedItem); } else { approved.remove(approvedItem); @@ -973,7 +991,7 @@ abstract public class ManagedServices { || isPackageOrComponentAllowed(component.getPackageName(), userId))) { return false; } - return componentHasBindPermission(component, userId); + return isValidService(component, userId); } private boolean componentHasBindPermission(ComponentName component, int userId) { @@ -1220,12 +1238,21 @@ abstract public class ManagedServices { if (!TextUtils.isEmpty(packageName)) { queryIntent.setPackage(packageName); } + + if (Flags.notificationNlsRebind()) { + // Expand the package query + extraFlags |= MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE; + extraFlags |= MATCH_INSTANT; + } + List<ResolveInfo> installedServices = pm.queryIntentServicesAsUser( queryIntent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA | extraFlags, userId); - if (DEBUG) - Slog.v(TAG, mConfig.serviceInterface + " services: " + installedServices); + if (DEBUG) { + Slog.v(TAG, mConfig.serviceInterface + " pkg: " + packageName + " services: " + + installedServices); + } if (installedServices != null) { for (int i = 0, count = installedServices.size(); i < count; i++) { ResolveInfo resolveInfo = installedServices.get(i); @@ -1325,11 +1352,12 @@ abstract public class ManagedServices { if (TextUtils.equals(getPackageName(approvedPackageOrComponent), packageName)) { final ComponentName component = ComponentName.unflattenFromString( approvedPackageOrComponent); - if (component != null && !componentHasBindPermission(component, userId)) { + if (component != null && !isValidService(component, userId)) { approved.removeAt(j); if (DEBUG) { Slog.v(TAG, "Removing " + approvedPackageOrComponent - + " from approved list; no bind permission found " + + " from approved list; no bind permission or " + + "service interface filter found " + mConfig.bindPermission); } } @@ -1348,6 +1376,15 @@ abstract public class ManagedServices { } } + protected boolean isValidService(ComponentName component, int userId) { + if (Flags.notificationNlsRebind()) { + return componentHasBindPermission(component, userId) && queryPackageForServices( + component.getPackageName(), userId).contains(component); + } else { + return componentHasBindPermission(component, userId); + } + } + protected boolean isValidEntry(String packageOrComponent, int userId) { return hasMatchingServices(packageOrComponent, userId); } @@ -1505,23 +1542,27 @@ abstract public class ManagedServices { * Called when user switched to unbind all services from other users. */ @VisibleForTesting - void unbindOtherUserServices(int currentUser) { + void unbindOtherUserServices(int switchedToUser) { TimingsTraceAndSlog t = new TimingsTraceAndSlog(); - t.traceBegin("ManagedServices.unbindOtherUserServices_current" + currentUser); - unbindServicesImpl(currentUser, true /* allExceptUser */); + t.traceBegin("ManagedServices.unbindOtherUserServices_current" + switchedToUser); + unbindServicesImpl(switchedToUser, true /* allExceptUser */); t.traceEnd(); } - void unbindUserServices(int user) { + void unbindUserServices(int removedUser) { TimingsTraceAndSlog t = new TimingsTraceAndSlog(); - t.traceBegin("ManagedServices.unbindUserServices" + user); - unbindServicesImpl(user, false /* allExceptUser */); + t.traceBegin("ManagedServices.unbindUserServices" + removedUser); + unbindServicesImpl(removedUser, false /* allExceptUser */); t.traceEnd(); } void unbindServicesImpl(int user, boolean allExceptUser) { final SparseArray<Set<ComponentName>> componentsToUnbind = new SparseArray<>(); synchronized (mMutex) { + if (Flags.notificationNlsRebind()) { + // Remove enqueued rebinds to avoid rebinding services for a switched user + mHandler.removeMessages(ON_BINDING_DIED_REBIND_MSG); + } final Set<ManagedServiceInfo> removableBoundServices = getRemovableConnectedServices(); for (ManagedServiceInfo info : removableBoundServices) { if ((allExceptUser && (info.userid != user)) @@ -1716,6 +1757,7 @@ abstract public class ManagedServices { mServicesRebinding.add(servicesBindingTag); mHandler.postDelayed(() -> reregisterService(name, userid), + ON_BINDING_DIED_REBIND_MSG, ON_BINDING_DIED_REBIND_DELAY_MS); } else { Slog.v(TAG, getCaption() + " not rebinding in user " + userid diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index f79d9ef174ea..c479acfd6228 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -194,3 +194,13 @@ flag { description: "Enables sound uri with vibration source in notification channel" bug: "351975435" } + +flag { + name: "notification_nls_rebind" + namespace: "systemui" + description: "Check for NLS service intent filter when rebinding services" + bug: "347674739" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java b/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java index dcb47a7b60b6..4c9cbc449656 100644 --- a/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java +++ b/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java @@ -42,27 +42,28 @@ public class StatsBootstrapAtomService extends IStatsBootstrapAtomService.Stub { return; } StatsEvent.Builder builder = StatsEvent.newBuilder().setAtomId(atom.atomId); - for (StatsBootstrapAtomValue value : atom.values) { + for (StatsBootstrapAtomValue atomValue : atom.values) { + StatsBootstrapAtomValue.Primitive value = atomValue.value; switch (value.getTag()) { - case StatsBootstrapAtomValue.boolValue: + case StatsBootstrapAtomValue.Primitive.boolValue: builder.writeBoolean(value.getBoolValue()); break; - case StatsBootstrapAtomValue.intValue: + case StatsBootstrapAtomValue.Primitive.intValue: builder.writeInt(value.getIntValue()); break; - case StatsBootstrapAtomValue.longValue: + case StatsBootstrapAtomValue.Primitive.longValue: builder.writeLong(value.getLongValue()); break; - case StatsBootstrapAtomValue.floatValue: + case StatsBootstrapAtomValue.Primitive.floatValue: builder.writeFloat(value.getFloatValue()); break; - case StatsBootstrapAtomValue.stringValue: + case StatsBootstrapAtomValue.Primitive.stringValue: builder.writeString(value.getStringValue()); break; - case StatsBootstrapAtomValue.bytesValue: + case StatsBootstrapAtomValue.Primitive.bytesValue: builder.writeByteArray(value.getBytesValue()); break; - case StatsBootstrapAtomValue.stringArrayValue: + case StatsBootstrapAtomValue.Primitive.stringArrayValue: builder.writeStringArray(value.getStringArrayValue()); break; default: @@ -71,6 +72,25 @@ public class StatsBootstrapAtomService extends IStatsBootstrapAtomService.Stub { return; } + StatsBootstrapAtomValue.Annotation[] annotations = atomValue.annotations; + for (StatsBootstrapAtomValue.Annotation annotation : atomValue.annotations) { + if (annotation.id != StatsBootstrapAtomValue.Annotation.Id.IS_UID) { + Slog.e(TAG, "Unexpected annotation ID: " + annotation.id + + ", for atom " + atom.atomId + ": only UIDs are supported!"); + return; + } + + switch (annotation.value.getTag()) { + case StatsBootstrapAtomValue.Annotation.Primitive.boolValue: + builder.addBooleanAnnotation( + annotation.id, annotation.value.getBoolValue()); + break; + default: + Slog.e(TAG, "Unexpected value type " + annotation.value.getTag() + + " when logging UID for atom " + atom.atomId); + return; + } + } } StatsLog.write(builder.usePooledBuffer().build()); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index 48bc9d7c51a1..b5724b5c0cc8 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -21,8 +21,10 @@ import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; +import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND; +import static com.android.server.notification.Flags.FLAG_NOTIFICATION_NLS_REBIND; import static com.android.server.notification.ManagedServices.APPROVAL_BY_COMPONENT; import static com.android.server.notification.ManagedServices.APPROVAL_BY_PACKAGE; import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled; @@ -63,11 +65,14 @@ import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; +import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; +import android.testing.TestableLooper; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -82,7 +87,9 @@ import com.android.server.UiServiceTestCase; import com.google.android.collect.Lists; +import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -103,7 +110,10 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; + public class ManagedServicesTest extends UiServiceTestCase { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @Mock private IPackageManager mIpm; @@ -115,6 +125,7 @@ public class ManagedServicesTest extends UiServiceTestCase { private ManagedServices.UserProfiles mUserProfiles; @Mock private DevicePolicyManager mDpm; Object mLock = new Object(); + private TestableLooper mTestableLooper; UserInfo mZero = new UserInfo(0, "zero", 0); UserInfo mTen = new UserInfo(10, "ten", 0); @@ -142,6 +153,7 @@ public class ManagedServicesTest extends UiServiceTestCase { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + mTestableLooper = new TestableLooper(Looper.getMainLooper()); mContext.setMockPackageManager(mPm); mContext.addMockSystemService(Context.USER_SERVICE, mUm); @@ -199,6 +211,11 @@ public class ManagedServicesTest extends UiServiceTestCase { mIpm, APPROVAL_BY_COMPONENT); } + @After + public void tearDown() throws Exception { + mTestableLooper.destroy(); + } + @Test public void testBackupAndRestore_migration() throws Exception { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { @@ -888,7 +905,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); - mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); + mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>()); service.addApprovedList("a", 0, true); service.reregisterService(cn, 0); @@ -919,7 +936,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); - mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); + mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>()); service.addApprovedList("a", 0, false); service.reregisterService(cn, 0); @@ -950,7 +967,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); - mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); + mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>()); service.addApprovedList("a/a", 0, true); service.reregisterService(cn, 0); @@ -981,7 +998,7 @@ public class ManagedServicesTest extends UiServiceTestCase { return true; }); - mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>()); + mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>()); service.addApprovedList("a/a", 0, false); service.reregisterService(cn, 0); @@ -1053,6 +1070,78 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_NOTIFICATION_NLS_REBIND) + public void registerService_bindingDied_rebindIsClearedOnUserSwitch() throws Exception { + Context context = mock(Context.class); + PackageManager pm = mock(PackageManager.class); + ApplicationInfo ai = new ApplicationInfo(); + ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; + + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); + when(context.getPackageManager()).thenReturn(pm); + when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); + + ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, mIpm, + APPROVAL_BY_PACKAGE); + service = spy(service); + ComponentName cn = ComponentName.unflattenFromString("a/a"); + + // Trigger onBindingDied for component when registering + // => will schedule a rebind in 10 seconds + when(context.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + ServiceConnection sc = (ServiceConnection) args[1]; + sc.onBindingDied(cn); + return true; + }); + service.registerService(cn, 0); + assertThat(service.isBound(cn, 0)).isFalse(); + + // Switch to user 10 + service.onUserSwitched(10); + + // Check that the scheduled rebind for user 0 was cleared + mTestableLooper.moveTimeForward(ManagedServices.ON_BINDING_DIED_REBIND_DELAY_MS); + mTestableLooper.processAllMessages(); + verify(service, never()).reregisterService(any(), anyInt()); + } + + @Test + public void registerService_bindingDied_rebindIsExecutedAfterTimeout() throws Exception { + Context context = mock(Context.class); + PackageManager pm = mock(PackageManager.class); + ApplicationInfo ai = new ApplicationInfo(); + ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; + + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); + when(context.getPackageManager()).thenReturn(pm); + when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); + + ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, mIpm, + APPROVAL_BY_PACKAGE); + service = spy(service); + ComponentName cn = ComponentName.unflattenFromString("a/a"); + + // Trigger onBindingDied for component when registering + // => will schedule a rebind in 10 seconds + when(context.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + ServiceConnection sc = (ServiceConnection) args[1]; + sc.onBindingDied(cn); + return true; + }); + service.registerService(cn, 0); + assertThat(service.isBound(cn, 0)).isFalse(); + + // Check that the scheduled rebind is run + mTestableLooper.moveTimeForward(ManagedServices.ON_BINDING_DIED_REBIND_DELAY_MS); + mTestableLooper.processAllMessages(); + verify(service, times(1)).reregisterService(eq(cn), eq(0)); + } + + @Test public void testPackageUninstall_packageNoLongerInApprovedList() throws Exception { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, @@ -1211,6 +1300,65 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_NOTIFICATION_NLS_REBIND) + public void testUpgradeAppNoIntentFilterNoRebind() throws Exception { + Context context = spy(getContext()); + doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any()); + + ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, + mIpm, APPROVAL_BY_COMPONENT); + + List<String> packages = new ArrayList<>(); + packages.add("package"); + addExpectedServices(service, packages, 0); + + final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1"); + final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2"); + + // Both components are approved initially + mExpectedPrimaryComponentNames.clear(); + mExpectedPrimaryPackages.clear(); + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2"); + mExpectedSecondaryComponentNames.clear(); + mExpectedSecondaryPackages.clear(); + + loadXml(service); + + //Component package/C1 loses serviceInterface intent filter + ManagedServices.Config config = service.getConfig(); + when(mPm.queryIntentServicesAsUser(any(), anyInt(), anyInt())) + .thenAnswer(new Answer<List<ResolveInfo>>() { + @Override + public List<ResolveInfo> answer(InvocationOnMock invocationOnMock) + throws Throwable { + Object[] args = invocationOnMock.getArguments(); + Intent invocationIntent = (Intent) args[0]; + if (invocationIntent != null) { + if (invocationIntent.getAction().equals(config.serviceInterface) + && packages.contains(invocationIntent.getPackage())) { + List<ResolveInfo> dummyServices = new ArrayList<>(); + ResolveInfo resolveInfo = new ResolveInfo(); + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = invocationIntent.getPackage(); + serviceInfo.name = approvedComponent.getClassName(); + serviceInfo.permission = service.getConfig().bindPermission; + resolveInfo.serviceInfo = serviceInfo; + dummyServices.add(resolveInfo); + return dummyServices; + } + } + return new ArrayList<>(); + } + }); + + // Trigger package update + service.onPackagesChanged(false, new String[]{"package"}, new int[]{0}); + + assertFalse(service.isComponentEnabledForCurrentProfiles(unapprovedComponent)); + assertTrue(service.isComponentEnabledForCurrentProfiles(approvedComponent)); + } + + @Test public void testSetPackageOrComponentEnabled() throws Exception { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, @@ -1223,6 +1371,21 @@ public class ManagedServicesTest extends UiServiceTestCase { "user10package1/K", "user10.3/Component", "user10package2/L", "user10.4/Component"})); + // mock permissions for services + PackageManager pm = mock(PackageManager.class); + when(getContext().getPackageManager()).thenReturn(pm); + List<ComponentName> enabledComponents = List.of( + ComponentName.unflattenFromString("package/Comp"), + ComponentName.unflattenFromString("package/C2"), + ComponentName.unflattenFromString("again/M4"), + ComponentName.unflattenFromString("user10package/B"), + ComponentName.unflattenFromString("user10/Component"), + ComponentName.unflattenFromString("user10package1/K"), + ComponentName.unflattenFromString("user10.3/Component"), + ComponentName.unflattenFromString("user10package2/L"), + ComponentName.unflattenFromString("user10.4/Component")); + mockServiceInfoWithMetaData(enabledComponents, service, pm, new ArrayMap<>()); + for (int userId : expectedEnabled.keySet()) { ArrayList<String> expectedForUser = expectedEnabled.get(userId); for (int i = 0; i < expectedForUser.size(); i++) { @@ -1284,6 +1447,90 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_NOTIFICATION_NLS_REBIND) + public void testSetPackageOrComponentEnabled_pkgInstalledAfterEnabling() throws Exception { + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, + mIpm, APPROVAL_BY_COMPONENT); + + final int userId = 0; + final String validComponent = "again/M4"; + ArrayList<String> expectedEnabled = Lists.newArrayList("package/Comp", "package/C2", + validComponent); + + PackageManager pm = mock(PackageManager.class); + when(getContext().getPackageManager()).thenReturn(pm); + service = spy(service); + + // Component again/M4 is a valid service and the package is available + doReturn(true).when(service) + .isValidService(ComponentName.unflattenFromString(validComponent), userId); + when(pm.isPackageAvailable("again")).thenReturn(true); + + // "package" is not available and its services are not valid + doReturn(false).when(service) + .isValidService(ComponentName.unflattenFromString("package/Comp"), userId); + doReturn(false).when(service) + .isValidService(ComponentName.unflattenFromString("package/C2"), userId); + when(pm.isPackageAvailable("package")).thenReturn(false); + + // Enable all components + for (String component: expectedEnabled) { + service.setPackageOrComponentEnabled(component, userId, true, true); + } + + // Verify everything added is approved + for (String component: expectedEnabled) { + assertTrue("Not allowed: user: " + userId + " entry: " + component + + " for approval level " + APPROVAL_BY_COMPONENT, + service.isPackageOrComponentAllowed(component, userId)); + } + + // Add missing package "package" + service.onPackagesChanged(false, new String[]{"package"}, new int[]{0}); + + // Check that component of "package" are not enabled + assertFalse(service.isComponentEnabledForCurrentProfiles( + ComponentName.unflattenFromString("package/Comp"))); + assertFalse(service.isPackageOrComponentAllowed("package/Comp", userId)); + + assertFalse(service.isComponentEnabledForCurrentProfiles( + ComponentName.unflattenFromString("package/C2"))); + assertFalse(service.isPackageOrComponentAllowed("package/C2", userId)); + + // Check that the valid components are still enabled + assertTrue(service.isComponentEnabledForCurrentProfiles( + ComponentName.unflattenFromString(validComponent))); + assertTrue(service.isPackageOrComponentAllowed(validComponent, userId)); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_NLS_REBIND) + public void testSetPackageOrComponentEnabled_invalidComponent() throws Exception { + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, + mIpm, APPROVAL_BY_COMPONENT); + + final int userId = 0; + final String invalidComponent = "package/Comp"; + + PackageManager pm = mock(PackageManager.class); + when(getContext().getPackageManager()).thenReturn(pm); + service = spy(service); + + // Component is an invalid service and the package is available + doReturn(false).when(service) + .isValidService(ComponentName.unflattenFromString(invalidComponent), userId); + when(pm.isPackageAvailable("package")).thenReturn(true); + service.setPackageOrComponentEnabled(invalidComponent, userId, true, true); + + // Verify that the component was not enabled + assertFalse("Not allowed: user: " + userId + " entry: " + invalidComponent + + " for approval level " + APPROVAL_BY_COMPONENT, + service.isPackageOrComponentAllowed(invalidComponent, userId)); + assertFalse(service.isComponentEnabledForCurrentProfiles( + ComponentName.unflattenFromString(invalidComponent))); + } + + @Test public void testGetAllowedPackages_byUser() throws Exception { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, @@ -1944,7 +2191,7 @@ public class ManagedServicesTest extends UiServiceTestCase { metaDataAutobindAllow.putBoolean(META_DATA_DEFAULT_AUTOBIND, true); metaDatas.put(cn_allowed, metaDataAutobindAllow); - mockServiceInfoWithMetaData(componentNames, service, metaDatas); + mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas); service.addApprovedList(cn_allowed.flattenToString(), 0, true); service.addApprovedList(cn_disallowed.flattenToString(), 0, true); @@ -1989,7 +2236,7 @@ public class ManagedServicesTest extends UiServiceTestCase { metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false); metaDatas.put(cn_disallowed, metaDataAutobindDisallow); - mockServiceInfoWithMetaData(componentNames, service, metaDatas); + mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas); service.addApprovedList(cn_disallowed.flattenToString(), 0, true); @@ -2028,7 +2275,7 @@ public class ManagedServicesTest extends UiServiceTestCase { metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false); metaDatas.put(cn_disallowed, metaDataAutobindDisallow); - mockServiceInfoWithMetaData(componentNames, service, metaDatas); + mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas); service.addApprovedList(cn_disallowed.flattenToString(), 0, true); @@ -2099,8 +2346,8 @@ public class ManagedServicesTest extends UiServiceTestCase { } private void mockServiceInfoWithMetaData(List<ComponentName> componentNames, - ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas) - throws RemoteException { + ManagedServices service, PackageManager packageManager, + ArrayMap<ComponentName, Bundle> metaDatas) throws RemoteException { when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer( (Answer<ServiceInfo>) invocation -> { ComponentName invocationCn = invocation.getArgument(0); @@ -2115,6 +2362,39 @@ public class ManagedServicesTest extends UiServiceTestCase { return null; } ); + + // add components to queryIntentServicesAsUser response + final List<String> packages = new ArrayList<>(); + for (ComponentName cn: componentNames) { + packages.add(cn.getPackageName()); + } + ManagedServices.Config config = service.getConfig(); + when(packageManager.queryIntentServicesAsUser(any(), anyInt(), anyInt())). + thenAnswer(new Answer<List<ResolveInfo>>() { + @Override + public List<ResolveInfo> answer(InvocationOnMock invocationOnMock) + throws Throwable { + Object[] args = invocationOnMock.getArguments(); + Intent invocationIntent = (Intent) args[0]; + if (invocationIntent != null) { + if (invocationIntent.getAction().equals(config.serviceInterface) + && packages.contains(invocationIntent.getPackage())) { + List<ResolveInfo> dummyServices = new ArrayList<>(); + for (ComponentName cn: componentNames) { + ResolveInfo resolveInfo = new ResolveInfo(); + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = invocationIntent.getPackage(); + serviceInfo.name = cn.getClassName(); + serviceInfo.permission = service.getConfig().bindPermission; + resolveInfo.serviceInfo = serviceInfo; + dummyServices.add(resolveInfo); + } + return dummyServices; + } + } + return new ArrayList<>(); + } + }); } private void resetComponentsAndPackages() { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java index 0f7de7d78ccf..2c645e0ca353 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java @@ -28,6 +28,7 @@ import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertNull; + import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Matchers.any; @@ -197,6 +198,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase { public void testWriteXml_userTurnedOffNAS() throws Exception { int userId = ActivityManager.getCurrentUser(); + doReturn(true).when(mAssistants).isValidService(eq(mCn), eq(userId)); + mAssistants.loadDefaultsFromConfig(true); mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true, @@ -432,6 +435,10 @@ public class NotificationAssistantsTest extends UiServiceTestCase { public void testSetPackageOrComponentEnabled_onlyOnePackage() throws Exception { ComponentName component1 = ComponentName.unflattenFromString("package/Component1"); ComponentName component2 = ComponentName.unflattenFromString("package/Component2"); + + doReturn(true).when(mAssistants).isValidService(eq(component1), eq(mZero.id)); + doReturn(true).when(mAssistants).isValidService(eq(component2), eq(mZero.id)); + mAssistants.setPackageOrComponentEnabled(component1.flattenToString(), mZero.id, true, true, true); verify(mNm, never()).setNotificationAssistantAccessGrantedForUserInternal( @@ -577,6 +584,7 @@ public class NotificationAssistantsTest extends UiServiceTestCase { public void testSetAdjustmentTypeSupportedState() throws Exception { int userId = ActivityManager.getCurrentUser(); + doReturn(true).when(mAssistants).isValidService(eq(mCn), eq(userId)); mAssistants.loadDefaultsFromConfig(true); mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true, true, true); @@ -600,6 +608,7 @@ public class NotificationAssistantsTest extends UiServiceTestCase { public void testSetAdjustmentTypeSupportedState_readWriteXml_entries() throws Exception { int userId = ActivityManager.getCurrentUser(); + doReturn(true).when(mAssistants).isValidService(eq(mCn), eq(userId)); mAssistants.loadDefaultsFromConfig(true); mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true, true, true); @@ -623,6 +632,7 @@ public class NotificationAssistantsTest extends UiServiceTestCase { public void testSetAdjustmentTypeSupportedState_readWriteXml_empty() throws Exception { int userId = ActivityManager.getCurrentUser(); + doReturn(true).when(mAssistants).isValidService(eq(mCn), eq(userId)); mAssistants.loadDefaultsFromConfig(true); mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true, true, true); |