Implement minimized clipboard layout

When the keyboard is visible, show a minimized version of the clipboard
UI to avoid conflicting with the paste suggestion from the keyboard. On
tap, show the expanded (regular) layout.

Bug: 260707686
Test: ClipboardOverlayControllerTest, ClipboardModelTest

Change-Id: Ifbd66096d94fb4553110a2eded34a6eb387c476f
This commit is contained in:
Miranda Kephart
2023-01-25 12:15:24 -05:00
parent 89c8a28400
commit 1a70395d52
11 changed files with 764 additions and 44 deletions

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
<solid android:color="?androidprv:attr/colorAccentSecondary"/>
<corners android:radius="10dp"/>
</shape>

View File

@@ -0,0 +1,25 @@
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9,42Q7.7,42 6.85,41.15Q6,40.3 6,39V9Q6,7.7 6.85,6.85Q7.7,6 9,6H19.1Q19.45,4.25 20.825,3.125Q22.2,2 24,2Q25.8,2 27.175,3.125Q28.55,4.25 28.9,6H39Q40.3,6 41.15,6.85Q42,7.7 42,9V39Q42,40.3 41.15,41.15Q40.3,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V9Q39,9 39,9Q39,9 39,9H36V13.5H12V9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM24,9Q24.85,9 25.425,8.425Q26,7.85 26,7Q26,6.15 25.425,5.575Q24.85,5 24,5Q23.15,5 22.575,5.575Q22,6.15 22,7Q22,7.85 22.575,8.425Q23.15,9 24,9Z"/>
</vector>

View File

@@ -125,6 +125,45 @@
android:layout_width="@dimen/clipboard_preview_size"
android:layout_height="@dimen/clipboard_preview_size"/>
</FrameLayout>
<LinearLayout
android:id="@+id/minimized_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:elevation="7dp"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
android:background="@drawable/clipboard_minimized_background">
<ImageView
android:src="@drawable/ic_content_paste"
android:tint="?attr/overlayButtonTextColor"
android:layout_width="24dp"
android:layout_height="24dp"/>
<ImageView
android:src="@*android:drawable/ic_chevron_end"
android:tint="?attr/overlayButtonTextColor"
android:layout_width="24dp"
android:layout_height="24dp"
android:paddingEnd="-8dp"
android:paddingStart="-4dp"/>
</LinearLayout>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/clipboard_content_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:barrierDirection="top"
app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/clipboard_content_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:barrierDirection="end"
app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
<FrameLayout
android:id="@+id/dismiss_button"
android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
@@ -132,10 +171,10 @@
android:elevation="10dp"
android:visibility="gone"
android:alpha="0"
app:layout_constraintStart_toEndOf="@id/clipboard_preview"
app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
app:layout_constraintTop_toTopOf="@id/clipboard_preview"
app:layout_constraintBottom_toTopOf="@id/clipboard_preview"
app:layout_constraintStart_toEndOf="@id/clipboard_content_end"
app:layout_constraintEnd_toEndOf="@id/clipboard_content_end"
app:layout_constraintTop_toTopOf="@id/clipboard_content_top"
app:layout_constraintBottom_toTopOf="@id/clipboard_content_top"
android:contentDescription="@string/clipboard_dismiss_description">
<ImageView
android:id="@+id/dismiss_image"

View File

@@ -21,6 +21,7 @@ import static android.content.ClipDescription.CLASSIFICATION_COMPLETE;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN;
import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
@@ -35,6 +36,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.flags.FeatureFlags;
import javax.inject.Inject;
import javax.inject.Provider;
@@ -57,6 +59,7 @@ public class ClipboardListener implements
private final Provider<ClipboardOverlayController> mOverlayProvider;
private final ClipboardToast mClipboardToast;
private final ClipboardManager mClipboardManager;
private final FeatureFlags mFeatureFlags;
private final UiEventLogger mUiEventLogger;
private ClipboardOverlay mClipboardOverlay;
@@ -65,11 +68,13 @@ public class ClipboardListener implements
Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
ClipboardToast clipboardToast,
ClipboardManager clipboardManager,
FeatureFlags featureFlags,
UiEventLogger uiEventLogger) {
mContext = context;
mOverlayProvider = clipboardOverlayControllerProvider;
mClipboardToast = clipboardToast;
mClipboardManager = clipboardManager;
mFeatureFlags = featureFlags;
mUiEventLogger = uiEventLogger;
}
@@ -107,7 +112,11 @@ public class ClipboardListener implements
} else {
mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
}
mClipboardOverlay.setClipData(clipData, clipSource);
if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
mClipboardOverlay.setClipData(clipData, clipSource);
} else {
mClipboardOverlay.setClipDataLegacy(clipData, clipSource);
}
mClipboardOverlay.setOnSessionCompleteListener(() -> {
// Session is complete, free memory until it's needed again.
mClipboardOverlay = null;
@@ -150,6 +159,8 @@ public class ClipboardListener implements
}
interface ClipboardOverlay {
void setClipDataLegacy(ClipData clipData, String clipSource);
void setClipData(ClipData clipData, String clipSource);
void setOnSessionCompleteListener(Runnable runnable);

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.clipboardoverlay
import android.content.ClipData
import android.content.ClipDescription.EXTRA_IS_SENSITIVE
import android.content.Context
import android.graphics.Bitmap
import android.text.TextUtils
import android.util.Log
import android.util.Size
import com.android.systemui.R
import java.io.IOException
data class ClipboardModel(
val clipData: ClipData?,
val source: String,
val type: Type = Type.OTHER,
val item: ClipData.Item? = null,
val isSensitive: Boolean = false,
val isRemote: Boolean = false,
) {
private var _bitmap: Bitmap? = null
fun dataMatches(other: ClipboardModel?): Boolean {
if (other == null) {
return false
}
return source == other.source &&
type == other.type &&
item?.text == other.item?.text &&
item?.uri == other.item?.uri &&
isSensitive == other.isSensitive
}
fun loadThumbnail(context: Context): Bitmap? {
if (_bitmap == null && type == Type.IMAGE && item?.uri != null) {
try {
val size = context.resources.getDimensionPixelSize(R.dimen.overlay_x_scale)
_bitmap =
context.contentResolver.loadThumbnail(item.uri, Size(size, size * 4), null)
} catch (e: IOException) {
Log.e(TAG, "Thumbnail loading failed!", e)
}
}
return _bitmap
}
internal companion object {
private val TAG: String = "ClipboardModel"
@JvmStatic
fun fromClipData(
context: Context,
utils: ClipboardOverlayUtils,
clipData: ClipData?,
source: String
): ClipboardModel {
if (clipData == null || clipData.itemCount == 0) {
return ClipboardModel(clipData, source)
}
val sensitive = clipData.description?.extras?.getBoolean(EXTRA_IS_SENSITIVE) ?: false
val item = clipData.getItemAt(0)!!
val type = getType(context, item)
val remote = utils.isRemoteCopy(context, clipData, source)
return ClipboardModel(clipData, source, type, item, sensitive, remote)
}
private fun getType(context: Context, item: ClipData.Item): Type {
return if (!TextUtils.isEmpty(item.text)) {
Type.TEXT
} else if (
item.uri != null &&
context.contentResolver.getType(item.uri)?.startsWith("image") == true
) {
Type.IMAGE
} else {
Type.OTHER
}
}
}
enum class Type {
TEXT,
IMAGE,
OTHER
}
}

View File

@@ -17,7 +17,6 @@
package com.android.systemui.clipboardoverlay;
import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON;
@@ -31,10 +30,9 @@ import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBO
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
import static com.android.systemui.flags.Flags.CLIPBOARD_REMOTE_BEHAVIOR;
import static java.util.Objects.requireNonNull;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.RemoteAction;
@@ -47,7 +45,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.hardware.display.DisplayManager;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.Looper;
@@ -55,14 +52,15 @@ import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.Log;
import android.util.Size;
import android.view.Display;
import android.view.InputEvent;
import android.view.InputEventReceiver;
import android.view.InputMonitor;
import android.view.MotionEvent;
import android.view.WindowInsets;
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -71,7 +69,6 @@ import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.Overl
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.screenshot.TimeoutHandler;
import com.android.systemui.settings.DisplayTracker;
import java.io.IOException;
import java.util.Optional;
@@ -95,8 +92,6 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
private final Context mContext;
private final ClipboardLogger mClipboardLogger;
private final BroadcastDispatcher mBroadcastDispatcher;
private final DisplayManager mDisplayManager;
private final DisplayTracker mDisplayTracker;
private final ClipboardOverlayWindow mWindow;
private final TimeoutHandler mTimeoutHandler;
private final ClipboardOverlayUtils mClipboardUtils;
@@ -122,6 +117,9 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
private Runnable mOnUiUpdate;
private boolean mIsMinimized;
private ClipboardModel mClipboardModel;
private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks =
new ClipboardOverlayView.ClipboardOverlayCallbacks() {
@Override
@@ -175,6 +173,13 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
animateOut();
}
@Override
public void onMinimizedViewTapped() {
if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
animateFromMinimized();
}
}
};
@Inject
@@ -187,33 +192,28 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
FeatureFlags featureFlags,
ClipboardOverlayUtils clipboardUtils,
@Background Executor bgExecutor,
UiEventLogger uiEventLogger,
DisplayTracker displayTracker) {
UiEventLogger uiEventLogger) {
mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
mDisplayTracker = displayTracker;
final Context displayContext = context.createDisplayContext(getDefaultDisplay());
mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null);
mClipboardLogger = new ClipboardLogger(uiEventLogger);
mView = clipboardOverlayView;
mWindow = clipboardOverlayWindow;
mWindow.init(mView::setInsets, () -> {
mWindow.init(this::onInsetsChanged, () -> {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
hideImmediate();
});
mFeatureFlags = featureFlags;
mTimeoutHandler = timeoutHandler;
mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
mFeatureFlags = featureFlags;
mClipboardUtils = clipboardUtils;
mBgExecutor = bgExecutor;
mView.setCallbacks(mClipboardCallbacks);
mWindow.withWindowAttached(() -> {
mWindow.setContentView(mView);
mView.setInsets(mWindow.getWindowInsets(),
@@ -258,8 +258,135 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
}
@VisibleForTesting
void onInsetsChanged(WindowInsets insets, int orientation) {
mView.setInsets(insets, orientation);
if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
if (shouldShowMinimized(insets) && !mIsMinimized) {
mIsMinimized = true;
mView.setMinimized(true);
}
}
}
@Override // ClipboardListener.ClipboardOverlay
public void setClipData(ClipData clipData, String clipSource) {
public void setClipData(ClipData data, String source) {
ClipboardModel model = ClipboardModel.fromClipData(mContext, mClipboardUtils, data, source);
if (mExitAnimator != null && mExitAnimator.isRunning()) {
mExitAnimator.cancel();
}
boolean shouldAnimate = !model.dataMatches(mClipboardModel);
mClipboardModel = model;
mClipboardLogger.setClipSource(mClipboardModel.getSource());
if (shouldAnimate) {
reset();
mClipboardLogger.setClipSource(mClipboardModel.getSource());
if (shouldShowMinimized(mWindow.getWindowInsets())) {
mIsMinimized = true;
mView.setMinimized(true);
} else {
setExpandedView();
}
animateIn();
mView.announceForAccessibility(getAccessibilityAnnouncement(mClipboardModel.getType()));
} else if (!mIsMinimized) {
setExpandedView();
}
if (mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR) && mClipboardModel.isRemote()) {
mTimeoutHandler.cancelTimeout();
mOnUiUpdate = null;
} else {
mOnUiUpdate = mTimeoutHandler::resetTimeout;
mOnUiUpdate.run();
}
}
private void setExpandedView() {
final ClipboardModel model = mClipboardModel;
mView.setMinimized(false);
switch (model.getType()) {
case TEXT:
if ((mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR) && model.isRemote())
|| DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
if (model.getItem().getTextLinks() != null) {
classifyText(model);
}
}
if (model.isSensitive()) {
mView.showTextPreview(mContext.getString(R.string.clipboard_asterisks), true);
} else {
mView.showTextPreview(model.getItem().getText(), false);
}
mView.setEditAccessibilityAction(true);
mOnPreviewTapped = this::editText;
break;
case IMAGE:
if (model.isSensitive() || model.loadThumbnail(mContext) != null) {
mView.showImagePreview(
model.isSensitive() ? null : model.loadThumbnail(mContext));
mView.setEditAccessibilityAction(true);
mOnPreviewTapped = () -> editImage(model.getItem().getUri());
} else {
// image loading failed
mView.showDefaultTextPreview();
}
break;
case OTHER:
mView.showDefaultTextPreview();
break;
}
if (mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR)) {
if (!model.isRemote()) {
maybeShowRemoteCopy(model.getClipData());
}
} else {
maybeShowRemoteCopy(model.getClipData());
}
if (model.getType() != ClipboardModel.Type.OTHER) {
mOnShareTapped = () -> shareContent(model.getClipData());
mView.showShareChip();
}
}
private boolean shouldShowMinimized(WindowInsets insets) {
return insets.getInsets(WindowInsets.Type.ime()).bottom > 0;
}
private void animateFromMinimized() {
mIsMinimized = false;
setExpandedView();
animateIn();
}
private String getAccessibilityAnnouncement(ClipboardModel.Type type) {
if (type == ClipboardModel.Type.TEXT) {
return mContext.getString(R.string.clipboard_text_copied);
} else if (type == ClipboardModel.Type.IMAGE) {
return mContext.getString(R.string.clipboard_image_copied);
} else {
return mContext.getString(R.string.clipboard_content_copied);
}
}
private void classifyText(ClipboardModel model) {
mBgExecutor.execute(() -> {
Optional<RemoteAction> remoteAction =
mClipboardUtils.getAction(model.getItem(), model.getSource());
if (model.equals(mClipboardModel)) {
remoteAction.ifPresent(action -> {
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN);
mView.setActionChip(action, () -> {
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
animateOut();
});
});
}
});
}
@Override // ClipboardListener.ClipboardOverlay
public void setClipDataLegacy(ClipData clipData, String clipSource) {
if (mExitAnimator != null && mExitAnimator.isRunning()) {
mExitAnimator.cancel();
}
@@ -516,10 +643,6 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
mClipboardLogger.reset();
}
private Display getDefaultDisplay() {
return mDisplayManager.getDisplay(mDisplayTracker.getDefaultDisplayId());
}
static class ClipboardLogger {
private final UiEventLogger mUiEventLogger;
private String mClipSource;

View File

@@ -18,8 +18,6 @@ package com.android.systemui.clipboardoverlay;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static java.util.Objects.requireNonNull;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -77,6 +75,8 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
void onShareButtonTapped();
void onPreviewTapped();
void onMinimizedViewTapped();
}
private static final String TAG = "ClipboardView";
@@ -92,6 +92,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
private ImageView mImagePreview;
private TextView mTextPreview;
private TextView mHiddenPreview;
private LinearLayout mMinimizedPreview;
private View mPreviewBorder;
private OverlayActionChip mEditChip;
private OverlayActionChip mShareChip;
@@ -117,18 +118,18 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
@Override
protected void onFinishInflate() {
mActionContainerBackground =
requireNonNull(findViewById(R.id.actions_container_background));
mActionContainer = requireNonNull(findViewById(R.id.actions));
mClipboardPreview = requireNonNull(findViewById(R.id.clipboard_preview));
mImagePreview = requireNonNull(findViewById(R.id.image_preview));
mTextPreview = requireNonNull(findViewById(R.id.text_preview));
mHiddenPreview = requireNonNull(findViewById(R.id.hidden_preview));
mPreviewBorder = requireNonNull(findViewById(R.id.preview_border));
mEditChip = requireNonNull(findViewById(R.id.edit_chip));
mShareChip = requireNonNull(findViewById(R.id.share_chip));
mRemoteCopyChip = requireNonNull(findViewById(R.id.remote_copy_chip));
mDismissButton = requireNonNull(findViewById(R.id.dismiss_button));
mActionContainerBackground = requireViewById(R.id.actions_container_background);
mActionContainer = requireViewById(R.id.actions);
mClipboardPreview = requireViewById(R.id.clipboard_preview);
mPreviewBorder = requireViewById(R.id.preview_border);
mImagePreview = requireViewById(R.id.image_preview);
mTextPreview = requireViewById(R.id.text_preview);
mHiddenPreview = requireViewById(R.id.hidden_preview);
mMinimizedPreview = requireViewById(R.id.minimized_preview);
mEditChip = requireViewById(R.id.edit_chip);
mShareChip = requireViewById(R.id.share_chip);
mRemoteCopyChip = requireViewById(R.id.remote_copy_chip);
mDismissButton = requireViewById(R.id.dismiss_button);
mEditChip.setAlpha(1);
mShareChip.setAlpha(1);
@@ -163,6 +164,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped());
mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped());
mMinimizedPreview.setOnClickListener(v -> clipboardCallbacks.onMinimizedViewTapped());
}
void setEditAccessibilityAction(boolean editable) {
@@ -177,12 +179,28 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
}
}
void setMinimized(boolean minimized) {
if (minimized) {
mMinimizedPreview.setVisibility(View.VISIBLE);
mClipboardPreview.setVisibility(View.GONE);
mPreviewBorder.setVisibility(View.GONE);
mActionContainer.setVisibility(View.GONE);
mActionContainerBackground.setVisibility(View.GONE);
} else {
mMinimizedPreview.setVisibility(View.GONE);
mClipboardPreview.setVisibility(View.VISIBLE);
mPreviewBorder.setVisibility(View.VISIBLE);
mActionContainer.setVisibility(View.VISIBLE);
}
}
void setInsets(WindowInsets insets, int orientation) {
FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) getLayoutParams();
if (p == null) {
return;
}
Rect margins = computeMargins(insets, orientation);
p.setMargins(margins.left, margins.top, margins.right, margins.bottom);
setLayoutParams(p);
requestLayout();
@@ -204,6 +222,12 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
(int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
touchRegion.op(tmpRect, Region.Op.UNION);
mMinimizedPreview.getBoundsOnScreen(tmpRect);
tmpRect.inset(
(int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
(int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
touchRegion.op(tmpRect, Region.Op.UNION);
mDismissButton.getBoundsOnScreen(tmpRect);
touchRegion.op(tmpRect, Region.Op.UNION);
@@ -298,6 +322,8 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
scaleAnim.setDuration(333);
scaleAnim.addUpdateListener(animation -> {
float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
mMinimizedPreview.setScaleX(previewScale);
mMinimizedPreview.setScaleY(previewScale);
mClipboardPreview.setScaleX(previewScale);
mClipboardPreview.setScaleY(previewScale);
mPreviewBorder.setScaleX(previewScale);
@@ -319,12 +345,14 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
alphaAnim.setDuration(283);
alphaAnim.addUpdateListener(animation -> {
float alpha = animation.getAnimatedFraction();
mMinimizedPreview.setAlpha(alpha);
mClipboardPreview.setAlpha(alpha);
mPreviewBorder.setAlpha(alpha);
mDismissButton.setAlpha(alpha);
mActionContainer.setAlpha(alpha);
});
mMinimizedPreview.setAlpha(0);
mActionContainer.setAlpha(0);
mPreviewBorder.setAlpha(0);
mClipboardPreview.setAlpha(0);
@@ -356,6 +384,8 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
scaleAnim.setDuration(250);
scaleAnim.addUpdateListener(animation -> {
float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
mMinimizedPreview.setScaleX(previewScale);
mMinimizedPreview.setScaleY(previewScale);
mClipboardPreview.setScaleX(previewScale);
mClipboardPreview.setScaleY(previewScale);
mPreviewBorder.setScaleX(previewScale);
@@ -377,6 +407,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
alphaAnim.setDuration(166);
alphaAnim.addUpdateListener(animation -> {
float alpha = 1 - animation.getAnimatedFraction();
mMinimizedPreview.setAlpha(alpha);
mClipboardPreview.setAlpha(alpha);
mPreviewBorder.setAlpha(alpha);
mDismissButton.setAlpha(alpha);
@@ -399,6 +430,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
mTextPreview.setVisibility(View.GONE);
mImagePreview.setVisibility(View.GONE);
mHiddenPreview.setVisibility(View.GONE);
mMinimizedPreview.setVisibility(View.GONE);
v.setVisibility(View.VISIBLE);
}

View File

@@ -535,6 +535,8 @@ object Flags {
// 1700 - clipboard
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
// TODO(b/267162944): Tracking bug
@JvmField val CLIPBOARD_MINIMIZED_LAYOUT = unreleasedFlag(1702, "clipboard_data_model")
// 1800 - shade container
@JvmField

View File

@@ -16,6 +16,8 @@
package com.android.systemui.clipboardoverlay;
import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
import static org.junit.Assert.assertEquals;
@@ -38,6 +40,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.flags.FakeFeatureFlags;
import org.junit.Before;
import org.junit.Test;
@@ -60,6 +63,7 @@ public class ClipboardListenerTest extends SysuiTestCase {
private ClipboardOverlayController mOverlayController;
@Mock
private ClipboardToast mClipboardToast;
private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock
private UiEventLogger mUiEventLogger;
@@ -93,8 +97,10 @@ public class ClipboardListenerTest extends SysuiTestCase {
when(mClipboardManager.getPrimaryClip()).thenReturn(mSampleClipData);
when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource);
mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, true);
mClipboardListener = new ClipboardListener(getContext(), mOverlayControllerProvider,
mClipboardToast, mClipboardManager, mUiEventLogger);
mClipboardToast, mClipboardManager, mFeatureFlags, mUiEventLogger);
}
@@ -187,4 +193,34 @@ public class ClipboardListenerTest extends SysuiTestCase {
verify(mClipboardToast, times(1)).showCopiedToast();
verifyZeroInteractions(mOverlayControllerProvider);
}
@Test
public void test_minimizedLayoutFlagOff_usesLegacy() {
mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
mClipboardListener.start();
mClipboardListener.onPrimaryClipChanged();
verify(mOverlayControllerProvider).get();
verify(mOverlayController).setClipDataLegacy(
mClipDataCaptor.capture(), mStringCaptor.capture());
assertEquals(mSampleClipData, mClipDataCaptor.getValue());
assertEquals(mSampleSource, mStringCaptor.getValue());
}
@Test
public void test_minimizedLayoutFlagOn_usesNew() {
mClipboardListener.start();
mClipboardListener.onPrimaryClipChanged();
verify(mOverlayControllerProvider).get();
verify(mOverlayController).setClipData(
mClipDataCaptor.capture(), mStringCaptor.capture());
assertEquals(mSampleClipData, mClipDataCaptor.getValue());
assertEquals(mSampleSource, mStringCaptor.getValue());
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2022 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.clipboardoverlay
import android.content.ClipData
import android.content.ClipDescription
import android.content.ContentResolver
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.PersistableBundle
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.whenever
import java.io.IOException
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
class ClipboardModelTest : SysuiTestCase() {
@Mock private lateinit var mClipboardUtils: ClipboardOverlayUtils
@Mock private lateinit var mMockContext: Context
@Mock private lateinit var mMockContentResolver: ContentResolver
private lateinit var mSampleClipData: ClipData
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
mSampleClipData = ClipData("Test", arrayOf("text/plain"), ClipData.Item("Test Item"))
}
@Test
fun test_nullClipData() {
val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, null, "test source")
assertNull(model.clipData)
assertEquals("test source", model.source)
assertEquals(ClipboardModel.Type.OTHER, model.type)
assertNull(model.item)
assertFalse(model.isSensitive)
assertFalse(model.isRemote)
assertNull(model.loadThumbnail(mContext))
}
@Test
fun test_textClipData() {
val source = "test source"
val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, mSampleClipData, source)
assertEquals(mSampleClipData, model.clipData)
assertEquals(source, model.source)
assertEquals(ClipboardModel.Type.TEXT, model.type)
assertEquals(mSampleClipData.getItemAt(0), model.item)
assertFalse(model.isSensitive)
assertFalse(model.isRemote)
assertNull(model.loadThumbnail(mContext))
}
@Test
fun test_sensitiveExtra() {
val description = mSampleClipData.description
val b = PersistableBundle()
b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
description.extras = b
val data = ClipData(description, mSampleClipData.getItemAt(0))
val (_, _, _, _, sensitive) =
ClipboardModel.fromClipData(mContext, mClipboardUtils, data, "")
assertTrue(sensitive)
}
@Test
fun test_remoteExtra() {
whenever(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true)
val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, mSampleClipData, "")
assertTrue(model.isRemote)
}
@Test
@Throws(IOException::class)
fun test_imageClipData() {
val testBitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888)
whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver)
whenever(mMockContext.resources).thenReturn(mContext.resources)
whenever(mMockContentResolver.loadThumbnail(any(), any(), any())).thenReturn(testBitmap)
whenever(mMockContentResolver.getType(any())).thenReturn("image")
val imageClipData =
ClipData("Test", arrayOf("text/plain"), ClipData.Item(Uri.parse("test")))
val model = ClipboardModel.fromClipData(mMockContext, mClipboardUtils, imageClipData, "")
assertEquals(ClipboardModel.Type.IMAGE, model.type)
assertEquals(testBitmap, model.loadThumbnail(mMockContext))
}
@Test
@Throws(IOException::class)
fun test_imageClipData_loadFailure() {
whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver)
whenever(mMockContext.resources).thenReturn(mContext.resources)
whenever(mMockContentResolver.loadThumbnail(any(), any(), any())).thenThrow(IOException())
whenever(mMockContentResolver.getType(any())).thenReturn("image")
val imageClipData =
ClipData("Test", arrayOf("text/plain"), ClipData.Item(Uri.parse("test")))
val model = ClipboardModel.fromClipData(mMockContext, mClipboardUtils, imageClipData, "")
assertEquals(ClipboardModel.Type.IMAGE, model.type)
assertNull(model.loadThumbnail(mMockContext))
}
}

View File

@@ -16,13 +16,17 @@
package com.android.systemui.clipboardoverlay;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
import static com.android.systemui.flags.Flags.CLIPBOARD_REMOTE_BEHAVIOR;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -35,8 +39,11 @@ import android.app.RemoteAction;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Context;
import android.graphics.Insets;
import android.graphics.Rect;
import android.net.Uri;
import android.os.PersistableBundle;
import android.view.WindowInsets;
import android.view.textclassifier.TextLinks;
import androidx.test.filters.SmallTest;
@@ -102,11 +109,14 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator);
when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator);
when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
getImeInsets(new Rect(0, 0, 0, 0)));
mSampleClipData = new ClipData("Test", new String[]{"text/plain"},
new ClipData.Item("Test Item"));
mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, false);
mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, true); // turned off for legacy tests
mOverlayController = new ClipboardOverlayController(
mContext,
@@ -118,8 +128,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
mFeatureFlags,
mClipboardUtils,
mExecutor,
mUiEventLogger,
mDisplayTracker);
mUiEventLogger);
verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture());
mCallbacks = mOverlayCallbacksCaptor.getValue();
}
@@ -129,6 +138,159 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
mOverlayController.hideImmediate();
}
@Test
public void test_setClipData_nullData_legacy() {
ClipData clipData = null;
mOverlayController.setClipDataLegacy(clipData, "");
verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
verify(mClipboardOverlayView, times(0)).showShareChip();
verify(mClipboardOverlayView, times(1)).getEnterAnimation();
}
@Test
public void test_setClipData_invalidImageData_legacy() {
ClipData clipData = new ClipData("", new String[]{"image/png"},
new ClipData.Item(Uri.parse("")));
mOverlayController.setClipDataLegacy(clipData, "");
verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
verify(mClipboardOverlayView, times(0)).showShareChip();
verify(mClipboardOverlayView, times(1)).getEnterAnimation();
}
@Test
public void test_setClipData_textData_legacy() {
mOverlayController.setClipDataLegacy(mSampleClipData, "");
verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false);
verify(mClipboardOverlayView, times(1)).showShareChip();
verify(mClipboardOverlayView, times(1)).getEnterAnimation();
}
@Test
public void test_setClipData_sensitiveTextData_legacy() {
ClipDescription description = mSampleClipData.getDescription();
PersistableBundle b = new PersistableBundle();
b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
description.setExtras(b);
ClipData data = new ClipData(description, mSampleClipData.getItemAt(0));
mOverlayController.setClipDataLegacy(data, "");
verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true);
verify(mClipboardOverlayView, times(1)).showShareChip();
verify(mClipboardOverlayView, times(1)).getEnterAnimation();
}
@Test
public void test_setClipData_repeatedCalls_legacy() {
when(mAnimator.isRunning()).thenReturn(true);
mOverlayController.setClipDataLegacy(mSampleClipData, "");
mOverlayController.setClipDataLegacy(mSampleClipData, "");
verify(mClipboardOverlayView, times(1)).getEnterAnimation();
}
@Test
public void test_viewCallbacks_onShareTapped_legacy() {
mOverlayController.setClipDataLegacy(mSampleClipData, "");
mCallbacks.onShareButtonTapped();
verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "");
verify(mClipboardOverlayView, times(1)).getExitAnimation();
}
@Test
public void test_viewCallbacks_onDismissTapped_legacy() {
mOverlayController.setClipDataLegacy(mSampleClipData, "");
mCallbacks.onDismissButtonTapped();
verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "");
verify(mClipboardOverlayView, times(1)).getExitAnimation();
}
@Test
public void test_multipleDismissals_dismissesOnce_legacy() {
mCallbacks.onSwipeDismissInitiated(mAnimator);
mCallbacks.onDismissButtonTapped();
mCallbacks.onSwipeDismissInitiated(mAnimator);
mCallbacks.onDismissButtonTapped();
verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED, 0, null);
verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
}
@Test
public void test_remoteCopy_withFlagOn_legacy() {
mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true);
mOverlayController.setClipDataLegacy(mSampleClipData, "");
verify(mTimeoutHandler, never()).resetTimeout();
}
@Test
public void test_remoteCopy_withFlagOff_legacy() {
when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true);
mOverlayController.setClipDataLegacy(mSampleClipData, "");
verify(mTimeoutHandler).resetTimeout();
}
@Test
public void test_nonRemoteCopy_legacy() {
mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(false);
mOverlayController.setClipDataLegacy(mSampleClipData, "");
verify(mTimeoutHandler).resetTimeout();
}
@Test
public void test_logsUseLastClipSource_legacy() {
mOverlayController.setClipDataLegacy(mSampleClipData, "first.package");
mCallbacks.onDismissButtonTapped();
mOverlayController.setClipDataLegacy(mSampleClipData, "second.package");
mCallbacks.onDismissButtonTapped();
verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "first.package");
verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, "second.package");
verifyNoMoreInteractions(mUiEventLogger);
}
@Test
public void test_logOnClipboardActionsShown_legacy() {
ClipData.Item item = mSampleClipData.getItemAt(0);
item.setTextLinks(Mockito.mock(TextLinks.class));
mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
when(mClipboardUtils.isRemoteCopy(any(Context.class), any(ClipData.class), anyString()))
.thenReturn(true);
when(mClipboardUtils.getAction(any(ClipData.Item.class), anyString()))
.thenReturn(Optional.of(Mockito.mock(RemoteAction.class)));
when(mClipboardOverlayView.post(any(Runnable.class))).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
((Runnable) invocation.getArgument(0)).run();
return null;
}
});
mOverlayController.setClipDataLegacy(
new ClipData(mSampleClipData.getDescription(), item), "actionShownSource");
mExecutor.runAllReady();
verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_ACTION_SHOWN, 0, "actionShownSource");
verifyNoMoreInteractions(mUiEventLogger);
}
// start of refactored setClipData tests
@Test
public void test_setClipData_nullData() {
ClipData clipData = null;
@@ -280,4 +442,43 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_ACTION_SHOWN, 0, "actionShownSource");
verifyNoMoreInteractions(mUiEventLogger);
}
@Test
public void test_noInsets_showsExpanded() {
mOverlayController.setClipData(mSampleClipData, "");
verify(mClipboardOverlayView, never()).setMinimized(true);
verify(mClipboardOverlayView).setMinimized(false);
verify(mClipboardOverlayView).showTextPreview("Test Item", false);
}
@Test
public void test_insets_showsMinimized() {
when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
getImeInsets(new Rect(0, 0, 0, 1)));
mOverlayController.setClipData(mSampleClipData, "");
verify(mClipboardOverlayView).setMinimized(true);
verify(mClipboardOverlayView, never()).setMinimized(false);
verify(mClipboardOverlayView, never()).showTextPreview(any(), anyBoolean());
mCallbacks.onMinimizedViewTapped();
verify(mClipboardOverlayView).setMinimized(false);
verify(mClipboardOverlayView).showTextPreview("Test Item", false);
}
@Test
public void test_insetsChanged_minimizes() {
mOverlayController.setClipData(mSampleClipData, "");
verify(mClipboardOverlayView, never()).setMinimized(true);
WindowInsets insetsWithKeyboard = getImeInsets(new Rect(0, 0, 0, 1));
mOverlayController.onInsetsChanged(insetsWithKeyboard, ORIENTATION_PORTRAIT);
verify(mClipboardOverlayView).setMinimized(true);
}
private static WindowInsets getImeInsets(Rect r) {
return new WindowInsets.Builder().setInsets(WindowInsets.Type.ime(), Insets.of(r)).build();
}
}