Loading quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java +65 −62 Original line number Diff line number Diff line Loading @@ -74,7 +74,7 @@ public final class DigitalWellBeingToast { SPLIT_GRID_BANNER_SMALL, }) @Retention(RetentionPolicy.SOURCE) @interface SPLIT_BANNER_CONFIG{} @interface SplitBannerConfig{} static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS); static final int MINUTE_MS = 60000; Loading @@ -88,7 +88,6 @@ public final class DigitalWellBeingToast { private Task mTask; private boolean mHasLimit; private long mAppUsageLimitTimeMs; private long mAppRemainingTimeMs; @Nullable private View mBanner; Loading @@ -96,10 +95,11 @@ public final class DigitalWellBeingToast { private float mBannerOffsetPercentage; @Nullable private SplitBounds mSplitBounds; private int mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN; private float mSplitOffsetTranslationY; private float mSplitOffsetTranslationX; private boolean mIsDestroyed = false; public DigitalWellBeingToast(RecentsViewContainer container, TaskView taskView) { mContainer = container; mTaskView = taskView; Loading @@ -110,12 +110,10 @@ public final class DigitalWellBeingToast { mHasLimit = false; mTaskView.setContentDescription(mTask.titleDescription); replaceBanner(null); mAppUsageLimitTimeMs = -1; mAppRemainingTimeMs = -1; } private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) { mAppUsageLimitTimeMs = appUsageLimitTimeMs; mAppRemainingTimeMs = appRemainingTimeMs; mHasLimit = true; TextView toast = mContainer.getViewCache().getView(R.layout.digital_wellbeing_toast, Loading @@ -138,7 +136,9 @@ public final class DigitalWellBeingToast { } public void initialize(Task task) { mAppUsageLimitTimeMs = mAppRemainingTimeMs = -1; if (mIsDestroyed) { throw new IllegalStateException("Cannot re-initialize a destroyed toast"); } mTask = task; ORDERED_BG_EXECUTOR.execute(() -> { AppUsageLimit usageLimit = null; Loading @@ -155,72 +155,76 @@ public final class DigitalWellBeingToast { usageLimit != null ? usageLimit.getUsageRemaining() : -1; mTaskView.post(() -> { if (mIsDestroyed) { return; } if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) { setNoLimit(); } else { setLimit(appUsageLimitTimeMs, appRemainingTimeMs); } }); }); } ); /** * Mark the DWB toast as destroyed and remove banner from TaskView. */ public void destroy() { mIsDestroyed = true; mTaskView.post(() -> replaceBanner(null)); } public void setSplitConfiguration(SplitBounds splitBounds) { public void setSplitBounds(@Nullable SplitBounds splitBounds) { mSplitBounds = splitBounds; } private @SplitBannerConfig int getSplitBannerConfig() { if (mSplitBounds == null || !mContainer.getDeviceProfile().isTablet || mTaskView.isFocusedTask()) { mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN; return; return SPLIT_BANNER_FULLSCREEN; } // For portrait grid only height of task changes, not width. So we keep the text the same if (!mContainer.getDeviceProfile().isLeftRightSplit) { mSplitBannerConfig = SPLIT_GRID_BANNER_LARGE; return; return SPLIT_GRID_BANNER_LARGE; } // For landscape grid, for 30% width we only show icon, otherwise show icon and time if (mTask.key.id == mSplitBounds.leftTopTaskId) { mSplitBannerConfig = mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; return mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; } else { mSplitBannerConfig = mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; return mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; } } private String getReadableDuration( Duration duration, FormatWidth formatWidthHourAndMinute, @StringRes int durationLessThanOneMinuteStringId, boolean forceFormatWidth) { @StringRes int durationLessThanOneMinuteStringId) { int hours = Math.toIntExact(duration.toHours()); int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes()); // Apply formatWidthHourAndMinute if both the hour part and the minute part are non-zero. // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero. if (hours > 0 && minutes > 0) { return MeasureFormat.getInstance(Locale.getDefault(), formatWidthHourAndMinute) return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.NARROW) .formatMeasures( new Measure(hours, MeasureUnit.HOUR), new Measure(minutes, MeasureUnit.MINUTE)); } // Apply formatWidthHourOrMinute if only the hour part is non-zero (unless forced). // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced). if (hours > 0) { return MeasureFormat.getInstance( Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE) .formatMeasures(new Measure(hours, MeasureUnit.HOUR)); return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( new Measure(hours, MeasureUnit.HOUR)); } // Apply formatWidthHourOrMinute if only the minute part is non-zero (unless forced). // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced). if (minutes > 0) { return MeasureFormat.getInstance( Locale.getDefault() , forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE) .formatMeasures(new Measure(minutes, MeasureUnit.MINUTE)); return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( new Measure(minutes, MeasureUnit.MINUTE)); } // Use a specific string for usage less than one minute but non-zero. Loading @@ -229,13 +233,12 @@ public final class DigitalWellBeingToast { } // Otherwise, return 0-minute string. return MeasureFormat.getInstance( Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE) .formatMeasures(new Measure(0, MeasureUnit.MINUTE)); return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( new Measure(0, MeasureUnit.MINUTE)); } /** * Returns text to show for the banner depending on {@link #mSplitBannerConfig} * Returns text to show for the banner depending on {@link #getSplitBannerConfig()} * If {@param forContentDesc} is {@code true}, this will always return the full * string corresponding to {@link #SPLIT_BANNER_FULLSCREEN} */ Loading @@ -245,16 +248,16 @@ public final class DigitalWellBeingToast { (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS : remainingTime); String readableDuration = getReadableDuration(duration, FormatWidth.NARROW, R.string.shorter_duration_less_than_one_minute, false /* forceFormatWidth */); if (forContentDesc || mSplitBannerConfig == SPLIT_BANNER_FULLSCREEN) { R.string.shorter_duration_less_than_one_minute /* forceFormatWidth */); @SplitBannerConfig int splitBannerConfig = getSplitBannerConfig(); if (forContentDesc || splitBannerConfig == SPLIT_BANNER_FULLSCREEN) { return mContainer.asContext().getString( R.string.time_left_for_app, readableDuration); } if (mSplitBannerConfig == SPLIT_GRID_BANNER_SMALL) { if (splitBannerConfig == SPLIT_GRID_BANNER_SMALL) { // show no text return ""; } else { // SPLIT_GRID_BANNER_LARGE Loading Loading @@ -309,7 +312,7 @@ public final class DigitalWellBeingToast { private void setBanner(@Nullable View view) { mBanner = view; if (view != null && mTaskView.getRecentsView() != null) { if (mBanner != null && mTaskView.getRecentsView() != null) { setupAndAddBanner(); setBannerOutline(); } Loading quickstep/src/com/android/quickstep/views/GroupedTaskView.kt +5 −0 Original line number Diff line number Diff line Loading @@ -162,6 +162,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT ) } taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) } setOrientationState(orientedState) } Loading Loading @@ -240,6 +241,10 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) { splitBoundsConfig = splitBounds taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) it.digitalWellBeingToast?.initialize(it.task) } invalidate() } Loading quickstep/src/com/android/quickstep/views/TaskView.kt +10 −4 Original line number Diff line number Diff line Loading @@ -512,6 +512,7 @@ constructor( onTaskListVisibilityChanged(false) borderEnabled = false taskViewId = UNBOUND_TASK_VIEW_ID taskContainers.forEach { it.destroy() } } // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. Loading Loading @@ -801,12 +802,12 @@ constructor( taskContainers.forEach { if (visible) { recentsModel.iconCache .updateIconInBackground(it.task) { thumbnailData -> setIcon(it.iconView, thumbnailData.icon) .updateIconInBackground(it.task) { task -> setIcon(it.iconView, task.icon) if (enableOverviewIconMenu()) { setText(it.iconView, thumbnailData.title) setText(it.iconView, task.title) } it.digitalWellBeingToast?.initialize(thumbnailData) it.digitalWellBeingToast?.initialize(task) } ?.also { request -> pendingIconLoadRequests.add(request) } } else { Loading Loading @@ -1586,6 +1587,11 @@ constructor( val taskView: TaskView get() = this@TaskView fun destroy() { digitalWellBeingToast?.destroy() thumbnailView?.let { taskView.removeView(it) } } // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView fun bindThumbnailView() { Loading Loading
quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java +65 −62 Original line number Diff line number Diff line Loading @@ -74,7 +74,7 @@ public final class DigitalWellBeingToast { SPLIT_GRID_BANNER_SMALL, }) @Retention(RetentionPolicy.SOURCE) @interface SPLIT_BANNER_CONFIG{} @interface SplitBannerConfig{} static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS); static final int MINUTE_MS = 60000; Loading @@ -88,7 +88,6 @@ public final class DigitalWellBeingToast { private Task mTask; private boolean mHasLimit; private long mAppUsageLimitTimeMs; private long mAppRemainingTimeMs; @Nullable private View mBanner; Loading @@ -96,10 +95,11 @@ public final class DigitalWellBeingToast { private float mBannerOffsetPercentage; @Nullable private SplitBounds mSplitBounds; private int mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN; private float mSplitOffsetTranslationY; private float mSplitOffsetTranslationX; private boolean mIsDestroyed = false; public DigitalWellBeingToast(RecentsViewContainer container, TaskView taskView) { mContainer = container; mTaskView = taskView; Loading @@ -110,12 +110,10 @@ public final class DigitalWellBeingToast { mHasLimit = false; mTaskView.setContentDescription(mTask.titleDescription); replaceBanner(null); mAppUsageLimitTimeMs = -1; mAppRemainingTimeMs = -1; } private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) { mAppUsageLimitTimeMs = appUsageLimitTimeMs; mAppRemainingTimeMs = appRemainingTimeMs; mHasLimit = true; TextView toast = mContainer.getViewCache().getView(R.layout.digital_wellbeing_toast, Loading @@ -138,7 +136,9 @@ public final class DigitalWellBeingToast { } public void initialize(Task task) { mAppUsageLimitTimeMs = mAppRemainingTimeMs = -1; if (mIsDestroyed) { throw new IllegalStateException("Cannot re-initialize a destroyed toast"); } mTask = task; ORDERED_BG_EXECUTOR.execute(() -> { AppUsageLimit usageLimit = null; Loading @@ -155,72 +155,76 @@ public final class DigitalWellBeingToast { usageLimit != null ? usageLimit.getUsageRemaining() : -1; mTaskView.post(() -> { if (mIsDestroyed) { return; } if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) { setNoLimit(); } else { setLimit(appUsageLimitTimeMs, appRemainingTimeMs); } }); }); } ); /** * Mark the DWB toast as destroyed and remove banner from TaskView. */ public void destroy() { mIsDestroyed = true; mTaskView.post(() -> replaceBanner(null)); } public void setSplitConfiguration(SplitBounds splitBounds) { public void setSplitBounds(@Nullable SplitBounds splitBounds) { mSplitBounds = splitBounds; } private @SplitBannerConfig int getSplitBannerConfig() { if (mSplitBounds == null || !mContainer.getDeviceProfile().isTablet || mTaskView.isFocusedTask()) { mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN; return; return SPLIT_BANNER_FULLSCREEN; } // For portrait grid only height of task changes, not width. So we keep the text the same if (!mContainer.getDeviceProfile().isLeftRightSplit) { mSplitBannerConfig = SPLIT_GRID_BANNER_LARGE; return; return SPLIT_GRID_BANNER_LARGE; } // For landscape grid, for 30% width we only show icon, otherwise show icon and time if (mTask.key.id == mSplitBounds.leftTopTaskId) { mSplitBannerConfig = mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; return mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; } else { mSplitBannerConfig = mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; return mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; } } private String getReadableDuration( Duration duration, FormatWidth formatWidthHourAndMinute, @StringRes int durationLessThanOneMinuteStringId, boolean forceFormatWidth) { @StringRes int durationLessThanOneMinuteStringId) { int hours = Math.toIntExact(duration.toHours()); int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes()); // Apply formatWidthHourAndMinute if both the hour part and the minute part are non-zero. // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero. if (hours > 0 && minutes > 0) { return MeasureFormat.getInstance(Locale.getDefault(), formatWidthHourAndMinute) return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.NARROW) .formatMeasures( new Measure(hours, MeasureUnit.HOUR), new Measure(minutes, MeasureUnit.MINUTE)); } // Apply formatWidthHourOrMinute if only the hour part is non-zero (unless forced). // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced). if (hours > 0) { return MeasureFormat.getInstance( Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE) .formatMeasures(new Measure(hours, MeasureUnit.HOUR)); return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( new Measure(hours, MeasureUnit.HOUR)); } // Apply formatWidthHourOrMinute if only the minute part is non-zero (unless forced). // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced). if (minutes > 0) { return MeasureFormat.getInstance( Locale.getDefault() , forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE) .formatMeasures(new Measure(minutes, MeasureUnit.MINUTE)); return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( new Measure(minutes, MeasureUnit.MINUTE)); } // Use a specific string for usage less than one minute but non-zero. Loading @@ -229,13 +233,12 @@ public final class DigitalWellBeingToast { } // Otherwise, return 0-minute string. return MeasureFormat.getInstance( Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE) .formatMeasures(new Measure(0, MeasureUnit.MINUTE)); return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( new Measure(0, MeasureUnit.MINUTE)); } /** * Returns text to show for the banner depending on {@link #mSplitBannerConfig} * Returns text to show for the banner depending on {@link #getSplitBannerConfig()} * If {@param forContentDesc} is {@code true}, this will always return the full * string corresponding to {@link #SPLIT_BANNER_FULLSCREEN} */ Loading @@ -245,16 +248,16 @@ public final class DigitalWellBeingToast { (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS : remainingTime); String readableDuration = getReadableDuration(duration, FormatWidth.NARROW, R.string.shorter_duration_less_than_one_minute, false /* forceFormatWidth */); if (forContentDesc || mSplitBannerConfig == SPLIT_BANNER_FULLSCREEN) { R.string.shorter_duration_less_than_one_minute /* forceFormatWidth */); @SplitBannerConfig int splitBannerConfig = getSplitBannerConfig(); if (forContentDesc || splitBannerConfig == SPLIT_BANNER_FULLSCREEN) { return mContainer.asContext().getString( R.string.time_left_for_app, readableDuration); } if (mSplitBannerConfig == SPLIT_GRID_BANNER_SMALL) { if (splitBannerConfig == SPLIT_GRID_BANNER_SMALL) { // show no text return ""; } else { // SPLIT_GRID_BANNER_LARGE Loading Loading @@ -309,7 +312,7 @@ public final class DigitalWellBeingToast { private void setBanner(@Nullable View view) { mBanner = view; if (view != null && mTaskView.getRecentsView() != null) { if (mBanner != null && mTaskView.getRecentsView() != null) { setupAndAddBanner(); setBannerOutline(); } Loading
quickstep/src/com/android/quickstep/views/GroupedTaskView.kt +5 −0 Original line number Diff line number Diff line Loading @@ -162,6 +162,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT ) } taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) } setOrientationState(orientedState) } Loading Loading @@ -240,6 +241,10 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) { splitBoundsConfig = splitBounds taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) it.digitalWellBeingToast?.initialize(it.task) } invalidate() } Loading
quickstep/src/com/android/quickstep/views/TaskView.kt +10 −4 Original line number Diff line number Diff line Loading @@ -512,6 +512,7 @@ constructor( onTaskListVisibilityChanged(false) borderEnabled = false taskViewId = UNBOUND_TASK_VIEW_ID taskContainers.forEach { it.destroy() } } // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. Loading Loading @@ -801,12 +802,12 @@ constructor( taskContainers.forEach { if (visible) { recentsModel.iconCache .updateIconInBackground(it.task) { thumbnailData -> setIcon(it.iconView, thumbnailData.icon) .updateIconInBackground(it.task) { task -> setIcon(it.iconView, task.icon) if (enableOverviewIconMenu()) { setText(it.iconView, thumbnailData.title) setText(it.iconView, task.title) } it.digitalWellBeingToast?.initialize(thumbnailData) it.digitalWellBeingToast?.initialize(task) } ?.also { request -> pendingIconLoadRequests.add(request) } } else { Loading Loading @@ -1586,6 +1587,11 @@ constructor( val taskView: TaskView get() = this@TaskView fun destroy() { digitalWellBeingToast?.destroy() thumbnailView?.let { taskView.removeView(it) } } // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView fun bindThumbnailView() { Loading