diff options
14 files changed, 2334 insertions, 704 deletions
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 1a1fc75a41a1..0e9abee2f050 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.systemui.screenshot.DraggableConstraintLayout +<com.android.systemui.clipboardoverlay.ClipboardOverlayView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" @@ -157,4 +157,4 @@ android:layout_margin="@dimen/overlay_dismiss_button_margin" android:src="@drawable/overlay_cancel"/> </FrameLayout> -</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file +</com.android.systemui.clipboardoverlay.ClipboardOverlayView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml new file mode 100644 index 000000000000..1a1fc75a41a1 --- /dev/null +++ b/packages/SystemUI/res/layout/clipboard_overlay_legacy.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<com.android.systemui.screenshot.DraggableConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/clipboard_ui" + android:theme="@style/FloatingOverlay" + android:alpha="0" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/clipboard_overlay_window_name"> + <ImageView + android:id="@+id/actions_container_background" + android:visibility="gone" + android:layout_height="0dp" + android:layout_width="0dp" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" + app:layout_constraintBottom_toBottomOf="@+id/actions_container" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/actions_container" + app:layout_constraintEnd_toEndOf="@+id/actions_container"/> + <HorizontalScrollView + android:id="@+id/actions_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" + android:paddingEnd="@dimen/overlay_action_container_padding_right" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:scrollbars="none" + android:layout_marginBottom="4dp" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_percent="1.0" + app:layout_constraintWidth_max="wrap" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@+id/preview_border" + app:layout_constraintEnd_toEndOf="parent"> + <LinearLayout + android:id="@+id/actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:animateLayoutChanges="true"> + <include layout="@layout/overlay_action_chip" + android:id="@+id/share_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/remote_copy_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/edit_chip"/> + </LinearLayout> + </HorizontalScrollView> + <View + android:id="@+id/preview_border" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="@dimen/overlay_offset_x" + android:layout_marginBottom="12dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + android:elevation="7dp" + app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end" + app:layout_constraintTop_toTopOf="@id/clipboard_preview_top" + android:background="@drawable/overlay_border"/> + <androidx.constraintlayout.widget.Barrier + android:id="@+id/clipboard_preview_end" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierMargin="@dimen/overlay_border_width" + app:barrierDirection="end" + app:constraint_referenced_ids="clipboard_preview"/> + <androidx.constraintlayout.widget.Barrier + android:id="@+id/clipboard_preview_top" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="top" + app:barrierMargin="@dimen/overlay_border_width_neg" + app:constraint_referenced_ids="clipboard_preview"/> + <FrameLayout + android:id="@+id/clipboard_preview" + android:elevation="7dp" + android:background="@drawable/overlay_preview_background" + android:clipChildren="true" + android:clipToOutline="true" + android:clipToPadding="true" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_margin="@dimen/overlay_border_width" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:layout_constraintBottom_toBottomOf="@id/preview_border" + app:layout_constraintStart_toStartOf="@id/preview_border" + app:layout_constraintEnd_toEndOf="@id/preview_border" + app:layout_constraintTop_toTopOf="@id/preview_border"> + <TextView android:id="@+id/text_preview" + android:textFontWeight="500" + android:padding="8dp" + android:gravity="center|start" + android:ellipsize="end" + android:autoSizeTextType="uniform" + android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font" + android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font" + android:textColor="?attr/overlayButtonTextColor" + android:textColorLink="?attr/overlayButtonTextColor" + android:background="?androidprv:attr/colorAccentSecondary" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="@dimen/clipboard_preview_size"/> + <ImageView + android:id="@+id/image_preview" + android:scaleType="fitCenter" + android:adjustViewBounds="true" + android:contentDescription="@string/clipboard_image_preview" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + <TextView + android:id="@+id/hidden_preview" + android:visibility="gone" + android:textFontWeight="500" + android:padding="8dp" + android:gravity="center" + android:textSize="14sp" + android:textColor="?attr/overlayButtonTextColor" + android:background="?androidprv:attr/colorAccentSecondary" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="@dimen/clipboard_preview_size"/> + </FrameLayout> + <FrameLayout + android:id="@+id/dismiss_button" + android:layout_width="@dimen/overlay_dismiss_button_tappable_size" + android:layout_height="@dimen/overlay_dismiss_button_tappable_size" + 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" + android:contentDescription="@string/clipboard_dismiss_description"> + <ImageView + android:id="@+id/dismiss_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="@dimen/overlay_dismiss_button_margin" + android:src="@drawable/overlay_cancel"/> + </FrameLayout> +</com.android.systemui.screenshot.DraggableConstraintLayout>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java index 05e3f1ce87a6..82e570438dab 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java @@ -31,9 +31,12 @@ 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 com.android.systemui.flags.Flags; import com.android.systemui.util.DeviceConfigProxy; import javax.inject.Inject; +import javax.inject.Provider; /** * ClipboardListener brings up a clipboard overlay when something is copied to the clipboard. @@ -51,20 +54,30 @@ public class ClipboardListener implements private final Context mContext; private final DeviceConfigProxy mDeviceConfig; - private final ClipboardOverlayControllerFactory mOverlayFactory; + private final Provider<ClipboardOverlayController> mOverlayProvider; + private final ClipboardOverlayControllerLegacyFactory mOverlayFactory; private final ClipboardManager mClipboardManager; private final UiEventLogger mUiEventLogger; - private ClipboardOverlayController mClipboardOverlayController; + private final FeatureFlags mFeatureFlags; + private boolean mUsingNewOverlay; + private ClipboardOverlay mClipboardOverlay; @Inject public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy, - ClipboardOverlayControllerFactory overlayFactory, ClipboardManager clipboardManager, - UiEventLogger uiEventLogger) { + Provider<ClipboardOverlayController> clipboardOverlayControllerProvider, + ClipboardOverlayControllerLegacyFactory overlayFactory, + ClipboardManager clipboardManager, + UiEventLogger uiEventLogger, + FeatureFlags featureFlags) { mContext = context; mDeviceConfig = deviceConfigProxy; + mOverlayProvider = clipboardOverlayControllerProvider; mOverlayFactory = overlayFactory; mClipboardManager = clipboardManager; mUiEventLogger = uiEventLogger; + mFeatureFlags = featureFlags; + + mUsingNewOverlay = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR); } @Override @@ -89,16 +102,22 @@ public class ClipboardListener implements return; } - if (mClipboardOverlayController == null) { - mClipboardOverlayController = mOverlayFactory.create(mContext); + boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR); + if (mClipboardOverlay == null || enabled != mUsingNewOverlay) { + mUsingNewOverlay = enabled; + if (enabled) { + mClipboardOverlay = mOverlayProvider.get(); + } else { + mClipboardOverlay = mOverlayFactory.create(mContext); + } mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource); } else { mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource); } - mClipboardOverlayController.setClipData(clipData, clipSource); - mClipboardOverlayController.setOnSessionCompleteListener(() -> { + mClipboardOverlay.setClipData(clipData, clipSource); + mClipboardOverlay.setOnSessionCompleteListener(() -> { // Session is complete, free memory until it's needed again. - mClipboardOverlayController = null; + mClipboardOverlay = null; }); } @@ -120,4 +139,10 @@ public class ClipboardListener implements private static boolean isEmulator() { return SystemProperties.getBoolean("ro.boot.qemu", false); } + + interface ClipboardOverlay { + void setClipData(ClipData clipData, String clipSource); + + void setOnSessionCompleteListener(Runnable runnable); + } } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index 7e499ebdf691..bfb27a4c87b8 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -17,7 +17,6 @@ package com.android.systemui.clipboardoverlay; import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS; -import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; @@ -37,11 +36,6 @@ import static java.util.Objects.requireNonNull; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.annotation.MainThread; -import android.app.ICompatCameraControlCallback; import android.app.RemoteAction; import android.content.BroadcastReceiver; import android.content.ClipData; @@ -52,14 +46,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Insets; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.Region; -import android.graphics.drawable.Icon; import android.hardware.display.DisplayManager; import android.hardware.input.InputManager; import android.net.Uri; @@ -67,57 +54,37 @@ import android.os.AsyncTask; import android.os.Looper; import android.provider.DeviceConfig; import android.text.TextUtils; -import android.util.DisplayMetrics; import android.util.Log; -import android.util.MathUtils; import android.util.Size; -import android.util.TypedValue; import android.view.Display; -import android.view.DisplayCutout; -import android.view.Gravity; import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.InputMonitor; -import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.View; -import android.view.ViewRootImpl; -import android.view.ViewTreeObserver; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityManager; -import android.view.animation.LinearInterpolator; -import android.view.animation.PathInterpolator; import android.view.textclassifier.TextClassification; import android.view.textclassifier.TextClassificationManager; import android.view.textclassifier.TextClassifier; import android.view.textclassifier.TextLinks; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.core.view.ViewCompat; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.android.internal.logging.UiEventLogger; -import com.android.internal.policy.PhoneWindow; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastSender; -import com.android.systemui.screenshot.DraggableConstraintLayout; -import com.android.systemui.screenshot.FloatingWindowUtil; -import com.android.systemui.screenshot.OverlayActionChip; +import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext; import com.android.systemui.screenshot.TimeoutHandler; import java.io.IOException; import java.util.ArrayList; +import java.util.Optional; + +import javax.inject.Inject; /** * Controls state and UI for the overlay that appears when something is added to the clipboard */ -public class ClipboardOverlayController { +public class ClipboardOverlayController implements ClipboardListener.ClipboardOverlay { private static final String TAG = "ClipboardOverlayCtrlr"; /** Constants for screenshot/copy deconflicting */ @@ -126,36 +93,22 @@ public class ClipboardOverlayController { public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY"; private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000; - private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe - private static final int FONT_SEARCH_STEP_PX = 4; private final Context mContext; private final ClipboardLogger mClipboardLogger; private final BroadcastDispatcher mBroadcastDispatcher; private final DisplayManager mDisplayManager; - private final DisplayMetrics mDisplayMetrics; - private final WindowManager mWindowManager; - private final WindowManager.LayoutParams mWindowLayoutParams; - private final PhoneWindow mWindow; + private final ClipboardOverlayWindow mWindow; private final TimeoutHandler mTimeoutHandler; - private final AccessibilityManager mAccessibilityManager; private final TextClassifier mTextClassifier; - private final DraggableConstraintLayout mView; - private final View mClipboardPreview; - private final ImageView mImagePreview; - private final TextView mTextPreview; - private final TextView mHiddenPreview; - private final View mPreviewBorder; - private final OverlayActionChip mEditChip; - private final OverlayActionChip mShareChip; - private final OverlayActionChip mRemoteCopyChip; - private final View mActionContainerBackground; - private final View mDismissButton; - private final LinearLayout mActionContainer; - private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>(); + private final ClipboardOverlayView mView; private Runnable mOnSessionCompleteListener; + private Runnable mOnRemoteCopyTapped; + private Runnable mOnShareTapped; + private Runnable mOnEditTapped; + private Runnable mOnPreviewTapped; private InputMonitor mInputMonitor; private InputEventReceiver mInputEventReceiver; @@ -163,14 +116,66 @@ public class ClipboardOverlayController { private BroadcastReceiver mCloseDialogsReceiver; private BroadcastReceiver mScreenshotReceiver; - private boolean mBlockAttach = false; private Animator mExitAnimator; private Animator mEnterAnimator; - private final int mOrientation; - private boolean mKeyboardVisible; + private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks = + new ClipboardOverlayView.ClipboardOverlayCallbacks() { + @Override + public void onInteraction() { + mTimeoutHandler.resetTimeout(); + } + + @Override + public void onSwipeDismissInitiated(Animator animator) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); + mExitAnimator = animator; + } + + @Override + public void onDismissComplete() { + hideImmediate(); + } + + @Override + public void onPreviewTapped() { + if (mOnPreviewTapped != null) { + mOnPreviewTapped.run(); + } + } + + @Override + public void onShareButtonTapped() { + if (mOnShareTapped != null) { + mOnShareTapped.run(); + } + } + + @Override + public void onEditButtonTapped() { + if (mOnEditTapped != null) { + mOnEditTapped.run(); + } + } + + @Override + public void onRemoteCopyButtonTapped() { + if (mOnRemoteCopyTapped != null) { + mOnRemoteCopyTapped.run(); + } + } + + @Override + public void onDismissButtonTapped() { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + animateOut(); + } + }; - public ClipboardOverlayController(Context context, + @Inject + public ClipboardOverlayController(@OverlayWindowContext Context context, + ClipboardOverlayView clipboardOverlayView, + ClipboardOverlayWindow clipboardOverlayWindow, BroadcastDispatcher broadcastDispatcher, BroadcastSender broadcastSender, TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) { @@ -181,121 +186,26 @@ public class ClipboardOverlayController { mClipboardLogger = new ClipboardLogger(uiEventLogger); - mAccessibilityManager = AccessibilityManager.getInstance(mContext); + mView = clipboardOverlayView; + mWindow = clipboardOverlayWindow; + mWindow.init(mView::setInsets, () -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER); + hideImmediate(); + }); + mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class)) .getTextClassifier(); - mWindowManager = mContext.getSystemService(WindowManager.class); - - mDisplayMetrics = new DisplayMetrics(); - mContext.getDisplay().getRealMetrics(mDisplayMetrics); - mTimeoutHandler = timeoutHandler; mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS); - // Setup the window that we are going to use - mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); - mWindowLayoutParams.setTitle("ClipboardOverlay"); - - mWindow = FloatingWindowUtil.getFloatingWindow(mContext); - mWindow.setWindowManager(mWindowManager, null, null); - - setWindowFocusable(false); - - mView = (DraggableConstraintLayout) - LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay, null); - mActionContainerBackground = - requireNonNull(mView.findViewById(R.id.actions_container_background)); - mActionContainer = requireNonNull(mView.findViewById(R.id.actions)); - mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview)); - mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview)); - mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview)); - mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview)); - mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border)); - mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip)); - mShareChip = requireNonNull(mView.findViewById(R.id.share_chip)); - mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip)); - mEditChip.setAlpha(1); - mShareChip.setAlpha(1); - mRemoteCopyChip.setAlpha(1); - mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button)); - - mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share)); - mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() { - @Override - public void onInteraction() { - mTimeoutHandler.resetTimeout(); - } - - @Override - public void onSwipeDismissInitiated(Animator animator) { - mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); - mExitAnimator = animator; - } + mView.setCallbacks(mClipboardCallbacks); - @Override - public void onDismissComplete() { - hideImmediate(); - } - }); - mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> { - int availableHeight = mTextPreview.getHeight() - - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom()); - mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight()); - return true; - }); - - mDismissButton.setOnClickListener(view -> { - mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED); - animateOut(); - }); - - mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); - mRemoteCopyChip.setIcon( - Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true); - mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); - mOrientation = mContext.getResources().getConfiguration().orientation; - - attachWindow(); - withWindowAttached(() -> { + mWindow.withWindowAttached(() -> { mWindow.setContentView(mView); - WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); - mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime()); - updateInsets(insets); - mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - WindowInsets insets = - mWindowManager.getCurrentWindowMetrics().getWindowInsets(); - boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime()); - if (keyboardVisible != mKeyboardVisible) { - mKeyboardVisible = keyboardVisible; - updateInsets(insets); - } - } - }); - mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( - new ViewRootImpl.ActivityConfigCallback() { - @Override - public void onConfigurationChanged(Configuration overrideConfig, - int newDisplayId) { - if (mContext.getResources().getConfiguration().orientation - != mOrientation) { - mClipboardLogger.logSessionComplete( - CLIPBOARD_OVERLAY_DISMISSED_OTHER); - hideImmediate(); - } - } - - @Override - public void requestCompatCameraControl( - boolean showControl, boolean transformationApplied, - ICompatCameraControlCallback callback) { - Log.w(TAG, "unexpected requestCompatCameraControl call"); - } - }); + mView.setInsets(mWindow.getWindowInsets(), + mContext.getResources().getConfiguration().orientation); }); mTimeoutHandler.setOnTimeoutRunnable(() -> { @@ -336,21 +246,19 @@ public class ClipboardOverlayController { broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION); } - void setClipData(ClipData clipData, String clipSource) { + @Override // ClipboardListener.ClipboardOverlay + public void setClipData(ClipData clipData, String clipSource) { if (mExitAnimator != null && mExitAnimator.isRunning()) { mExitAnimator.cancel(); } reset(); - String accessibilityAnnouncement; + String accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null && clipData.getDescription().getExtras() .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE); if (clipData == null || clipData.getItemCount() == 0) { - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); - accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + mView.showDefaultTextPreview(); } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) { ClipData.Item item = clipData.getItemAt(0); if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, @@ -360,53 +268,47 @@ public class ClipboardOverlayController { } } if (isSensitive) { - showEditableText( - mContext.getResources().getString(R.string.clipboard_asterisks), true); + showEditableText(mContext.getString(R.string.clipboard_asterisks), true); } else { showEditableText(item.getText(), false); } - showShareChip(clipData); + mOnShareTapped = () -> shareContent(clipData); + mView.showShareChip(); accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied); } else if (clipData.getItemAt(0).getUri() != null) { if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) { - showShareChip(clipData); + mOnShareTapped = () -> shareContent(clipData); + mView.showShareChip(); accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied); - } else { - accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); } } else { - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); - accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + mView.showDefaultTextPreview(); } + maybeShowRemoteCopy(clipData); + animateIn(); + mView.announceForAccessibility(accessibilityAnnouncement); + mTimeoutHandler.resetTimeout(); + } + + private void maybeShowRemoteCopy(ClipData clipData) { Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext); // Only show remote copy if it's available. PackageManager packageManager = mContext.getPackageManager(); if (packageManager.resolveActivity( remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) { - mRemoteCopyChip.setContentDescription( - mContext.getString(R.string.clipboard_send_nearby_description)); - mRemoteCopyChip.setVisibility(View.VISIBLE); - mRemoteCopyChip.setOnClickListener((v) -> { + mView.setRemoteCopyVisibility(true); + mOnRemoteCopyTapped = () -> { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED); mContext.startActivity(remoteCopyIntent); animateOut(); - }); - mActionContainerBackground.setVisibility(View.VISIBLE); + }; } else { - mRemoteCopyChip.setVisibility(View.GONE); + mView.setRemoteCopyVisibility(false); } - withWindowAttached(() -> { - if (mEnterAnimator == null || !mEnterAnimator.isRunning()) { - mView.post(this::animateIn); - } - mView.announceForAccessibility(accessibilityAnnouncement); - }); - mTimeoutHandler.resetTimeout(); } - void setOnSessionCompleteListener(Runnable runnable) { + @Override // ClipboardListener.ClipboardOverlay + public void setOnSessionCompleteListener(Runnable runnable) { mOnSessionCompleteListener = runnable; } @@ -418,72 +320,29 @@ public class ClipboardOverlayController { actions.addAll(classification.getActions()); } mView.post(() -> { - resetActionChips(); - if (actions.size() > 0) { - mActionContainerBackground.setVisibility(View.VISIBLE); - for (RemoteAction action : actions) { - Intent targetIntent = action.getActionIntent().getIntent(); - ComponentName component = targetIntent.getComponent(); - if (component != null && !TextUtils.equals(source, - component.getPackageName())) { - OverlayActionChip chip = constructActionChip(action); - mActionContainer.addView(chip); - mActionChips.add(chip); - break; // only show at most one action chip - } - } - } - }); - } - - private void showShareChip(ClipData clip) { - mShareChip.setVisibility(View.VISIBLE); - mActionContainerBackground.setVisibility(View.VISIBLE); - mShareChip.setOnClickListener((v) -> shareContent(clip)); - } - - private OverlayActionChip constructActionChip(RemoteAction action) { - OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate( - R.layout.overlay_action_chip, mActionContainer, false); - chip.setText(action.getTitle()); - chip.setContentDescription(action.getTitle()); - chip.setIcon(action.getIcon(), false); - chip.setPendingIntent(action.getActionIntent(), () -> { - mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED); - animateOut(); + Optional<RemoteAction> action = actions.stream().filter(remoteAction -> { + ComponentName component = remoteAction.getActionIntent().getIntent().getComponent(); + return component != null && !TextUtils.equals(source, component.getPackageName()); + }).findFirst(); + mView.resetActionChips(); + action.ifPresent(remoteAction -> mView.setActionChip(remoteAction, () -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED); + animateOut(); + })); }); - chip.setAlpha(1); - return chip; } private void monitorOutsideTouches() { InputManager inputManager = mContext.getSystemService(InputManager.class); mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0); - mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(), - Looper.getMainLooper()) { + mInputEventReceiver = new InputEventReceiver( + mInputMonitor.getInputChannel(), Looper.getMainLooper()) { @Override public void onInputEvent(InputEvent event) { if (event instanceof MotionEvent) { MotionEvent motionEvent = (MotionEvent) event; if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - Region touchRegion = new Region(); - - final Rect tmpRect = new Rect(); - mPreviewBorder.getBoundsOnScreen(tmpRect); - tmpRect.inset( - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), - (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, - -SWIPE_PADDING_DP)); - touchRegion.op(tmpRect, Region.Op.UNION); - mActionContainerBackground.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); - if (!touchRegion.contains( + if (!mView.isInTouchRegion( (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE); animateOut(); @@ -513,95 +372,27 @@ public class ClipboardOverlayController { animateOut(); } - private void showSinglePreview(View v) { - mTextPreview.setVisibility(View.GONE); - mImagePreview.setVisibility(View.GONE); - mHiddenPreview.setVisibility(View.GONE); - v.setVisibility(View.VISIBLE); - } - - private void showTextPreview(CharSequence text, TextView textView) { - showSinglePreview(textView); - final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length())); - textView.setText(truncatedText); - updateTextSize(truncatedText, textView); - - textView.addOnLayoutChangeListener( - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - if (right - left != oldRight - oldLeft) { - updateTextSize(truncatedText, textView); - } - }); - mEditChip.setVisibility(View.GONE); - } - - private void updateTextSize(CharSequence text, TextView textView) { - Paint paint = new Paint(textView.getPaint()); - Resources res = textView.getResources(); - float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font); - float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font); - if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) { - // If the text is a single word and would fit within the TextView at the min font size, - // find the biggest font size that will fit. - float fontSizePx = minFontSize; - while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize - && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) { - fontSizePx += FONT_SEARCH_STEP_PX; - } - // Need to turn off autosizing, otherwise setTextSize is a no-op. - textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE); - // It's possible to hit the max font size and not fill the width, so centering - // horizontally looks better in this case. - textView.setGravity(Gravity.CENTER); - textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx); - } else { - // Otherwise just stick with autosize. - textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize, - (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX); - textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); - } - } - - private static boolean fitsInView(CharSequence text, TextView textView, Paint paint, - float fontSizePx) { - paint.setTextSize(fontSizePx); - float size = paint.measureText(text.toString()); - float availableWidth = textView.getWidth() - textView.getPaddingLeft() - - textView.getPaddingRight(); - return size < availableWidth; - } - - private static boolean isOneWord(CharSequence text) { - return text.toString().split("\\s+", 2).length == 1; - } - private void showEditableText(CharSequence text, boolean hidden) { - TextView textView = hidden ? mHiddenPreview : mTextPreview; - showTextPreview(text, textView); - View.OnClickListener listener = v -> editText(); - setAccessibilityActionToEdit(textView); + mView.showTextPreview(text, hidden); + mView.setEditAccessibilityAction(true); + mOnPreviewTapped = this::editText; if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { - mEditChip.setVisibility(View.VISIBLE); - mActionContainerBackground.setVisibility(View.VISIBLE); - mEditChip.setContentDescription( - mContext.getString(R.string.clipboard_edit_text_description)); - mEditChip.setOnClickListener(listener); + mOnEditTapped = this::editText; + mView.showEditChip(mContext.getString(R.string.clipboard_edit_text_description)); } - textView.setOnClickListener(listener); } private boolean tryShowEditableImage(Uri uri, boolean isSensitive) { - View.OnClickListener listener = v -> editImage(uri); + Runnable listener = () -> editImage(uri); ContentResolver resolver = mContext.getContentResolver(); String mimeType = resolver.getType(uri); boolean isEditableImage = mimeType != null && mimeType.startsWith("image"); if (isSensitive) { - mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden)); - showSinglePreview(mHiddenPreview); + mView.showImagePreview(null); if (isEditableImage) { - mHiddenPreview.setOnClickListener(listener); - setAccessibilityActionToEdit(mHiddenPreview); + mOnPreviewTapped = listener; + mView.setEditAccessibilityAction(true); } } else if (isEditableImage) { // if the MIMEtype is image, try to load try { @@ -609,44 +400,36 @@ public class ClipboardOverlayController { // The width of the view is capped, height maintains aspect ratio, so allow it to be // taller if needed. Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null); - showSinglePreview(mImagePreview); - mImagePreview.setImageBitmap(thumbnail); - mImagePreview.setOnClickListener(listener); - setAccessibilityActionToEdit(mImagePreview); + mView.showImagePreview(thumbnail); + mView.setEditAccessibilityAction(true); + mOnPreviewTapped = listener; } catch (IOException e) { Log.e(TAG, "Thumbnail loading failed", e); - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); + mView.showDefaultTextPreview(); isEditableImage = false; } } else { - showTextPreview( - mContext.getResources().getString(R.string.clipboard_overlay_text_copied), - mTextPreview); + mView.showDefaultTextPreview(); } if (isEditableImage && DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { - mEditChip.setVisibility(View.VISIBLE); - mActionContainerBackground.setVisibility(View.VISIBLE); - mEditChip.setOnClickListener(listener); - mEditChip.setContentDescription( - mContext.getString(R.string.clipboard_edit_image_description)); + mView.showEditChip(mContext.getString(R.string.clipboard_edit_image_description)); } return isEditableImage; } - private void setAccessibilityActionToEdit(View view) { - ViewCompat.replaceAccessibilityAction(view, - AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, - mContext.getString(R.string.clipboard_edit), null); - } - private void animateIn() { - if (mAccessibilityManager.isEnabled()) { - mDismissButton.setVisibility(View.VISIBLE); + if (mEnterAnimator != null && mEnterAnimator.isRunning()) { + return; } - mEnterAnimator = getEnterAnimation(); + mEnterAnimator = mView.getEnterAnimation(); + mEnterAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mTimeoutHandler.resetTimeout(); + } + }); mEnterAnimator.start(); } @@ -654,7 +437,7 @@ public class ClipboardOverlayController { if (mExitAnimator != null && mExitAnimator.isRunning()) { return; } - Animator anim = getExitAnimation(); + Animator anim = mView.getExitAnimation(); anim.addListener(new AnimatorListenerAdapter() { private boolean mCancelled; @@ -676,122 +459,11 @@ public class ClipboardOverlayController { anim.start(); } - private Animator getEnterAnimation() { - TimeInterpolator linearInterpolator = new LinearInterpolator(); - TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f); - AnimatorSet enterAnim = new AnimatorSet(); - - ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); - rootAnim.setInterpolator(linearInterpolator); - rootAnim.setDuration(66); - rootAnim.addUpdateListener(animation -> { - mView.setAlpha(animation.getAnimatedFraction()); - }); - - ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); - scaleAnim.setInterpolator(scaleInterpolator); - scaleAnim.setDuration(333); - scaleAnim.addUpdateListener(animation -> { - float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); - mClipboardPreview.setScaleX(previewScale); - mClipboardPreview.setScaleY(previewScale); - mPreviewBorder.setScaleX(previewScale); - mPreviewBorder.setScaleY(previewScale); - - float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); - mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); - mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); - float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction()); - float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); - mActionContainer.setScaleX(actionsScaleX); - mActionContainer.setScaleY(actionsScaleY); - mActionContainerBackground.setScaleX(actionsScaleX); - mActionContainerBackground.setScaleY(actionsScaleY); - }); - - ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); - alphaAnim.setInterpolator(linearInterpolator); - alphaAnim.setDuration(283); - alphaAnim.addUpdateListener(animation -> { - float alpha = animation.getAnimatedFraction(); - mClipboardPreview.setAlpha(alpha); - mPreviewBorder.setAlpha(alpha); - mDismissButton.setAlpha(alpha); - mActionContainer.setAlpha(alpha); - }); - - mActionContainer.setAlpha(0); - mPreviewBorder.setAlpha(0); - mClipboardPreview.setAlpha(0); - enterAnim.play(rootAnim).with(scaleAnim); - enterAnim.play(alphaAnim).after(50).after(rootAnim); - - enterAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - mView.setAlpha(1); - mTimeoutHandler.resetTimeout(); - } - }); - return enterAnim; - } - - private Animator getExitAnimation() { - TimeInterpolator linearInterpolator = new LinearInterpolator(); - TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f); - AnimatorSet exitAnim = new AnimatorSet(); - - ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); - rootAnim.setInterpolator(linearInterpolator); - rootAnim.setDuration(100); - rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction())); - - ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); - scaleAnim.setInterpolator(scaleInterpolator); - scaleAnim.setDuration(250); - scaleAnim.addUpdateListener(animation -> { - float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); - mClipboardPreview.setScaleX(previewScale); - mClipboardPreview.setScaleY(previewScale); - mPreviewBorder.setScaleX(previewScale); - mPreviewBorder.setScaleY(previewScale); - - float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); - mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); - mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); - float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction()); - float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); - mActionContainer.setScaleX(actionScaleX); - mActionContainer.setScaleY(actionScaleY); - mActionContainerBackground.setScaleX(actionScaleX); - mActionContainerBackground.setScaleY(actionScaleY); - }); - - ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); - alphaAnim.setInterpolator(linearInterpolator); - alphaAnim.setDuration(166); - alphaAnim.addUpdateListener(animation -> { - float alpha = 1 - animation.getAnimatedFraction(); - mClipboardPreview.setAlpha(alpha); - mPreviewBorder.setAlpha(alpha); - mDismissButton.setAlpha(alpha); - mActionContainer.setAlpha(alpha); - }); - - exitAnim.play(alphaAnim).with(scaleAnim); - exitAnim.play(rootAnim).after(150).after(alphaAnim); - return exitAnim; - } - private void hideImmediate() { // Note this may be called multiple times if multiple dismissal events happen at the same // time. mTimeoutHandler.cancelTimeout(); - final View decorView = mWindow.peekDecorView(); - if (decorView != null && decorView.isAttachedToWindow()) { - mWindowManager.removeViewImmediate(decorView); - } + mWindow.remove(); if (mCloseDialogsReceiver != null) { mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver); mCloseDialogsReceiver = null; @@ -813,129 +485,20 @@ public class ClipboardOverlayController { } } - private void resetActionChips() { - for (OverlayActionChip chip : mActionChips) { - mActionContainer.removeView(chip); - } - mActionChips.clear(); - } - private void reset() { - mView.setTranslationX(0); - mView.setAlpha(0); - mActionContainerBackground.setVisibility(View.GONE); - mShareChip.setVisibility(View.GONE); - mEditChip.setVisibility(View.GONE); - mRemoteCopyChip.setVisibility(View.GONE); - resetActionChips(); + mOnRemoteCopyTapped = null; + mOnShareTapped = null; + mOnEditTapped = null; + mOnPreviewTapped = null; + mView.reset(); mTimeoutHandler.cancelTimeout(); mClipboardLogger.reset(); } - @MainThread - private void attachWindow() { - View decorView = mWindow.getDecorView(); - if (decorView.isAttachedToWindow() || mBlockAttach) { - return; - } - mBlockAttach = true; - mWindowManager.addView(decorView, mWindowLayoutParams); - decorView.requestApplyInsets(); - mView.requestApplyInsets(); - decorView.getViewTreeObserver().addOnWindowAttachListener( - new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - mBlockAttach = false; - } - - @Override - public void onWindowDetached() { - } - } - ); - } - - private void withWindowAttached(Runnable action) { - View decorView = mWindow.getDecorView(); - if (decorView.isAttachedToWindow()) { - action.run(); - } else { - decorView.getViewTreeObserver().addOnWindowAttachListener( - new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - mBlockAttach = false; - decorView.getViewTreeObserver().removeOnWindowAttachListener(this); - action.run(); - } - - @Override - public void onWindowDetached() { - } - }); - } - } - - private void updateInsets(WindowInsets insets) { - int orientation = mContext.getResources().getConfiguration().orientation; - FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams(); - if (p == null) { - return; - } - DisplayCutout cutout = insets.getDisplayCutout(); - Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); - Insets imeInsets = insets.getInsets(WindowInsets.Type.ime()); - if (cutout == null) { - p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom)); - } else { - Insets waterfall = cutout.getWaterfallInsets(); - if (orientation == ORIENTATION_PORTRAIT) { - p.setMargins( - waterfall.left, - Math.max(cutout.getSafeInsetTop(), waterfall.top), - waterfall.right, - Math.max(imeInsets.bottom, - Math.max(cutout.getSafeInsetBottom(), - Math.max(navBarInsets.bottom, waterfall.bottom)))); - } else { - p.setMargins( - waterfall.left, - waterfall.top, - waterfall.right, - Math.max(imeInsets.bottom, - Math.max(navBarInsets.bottom, waterfall.bottom))); - } - } - mView.setLayoutParams(p); - mView.requestLayout(); - } - private Display getDefaultDisplay() { return mDisplayManager.getDisplay(DEFAULT_DISPLAY); } - /** - * Updates the window focusability. If the window is already showing, then it updates the - * window immediately, otherwise the layout params will be applied when the window is next - * shown. - */ - private void setWindowFocusable(boolean focusable) { - int flags = mWindowLayoutParams.flags; - if (focusable) { - mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } else { - mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } - if (mWindowLayoutParams.flags == flags) { - return; - } - final View decorView = mWindow.peekDecorView(); - if (decorView != null && decorView.isAttachedToWindow()) { - mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); - } - } - static class ClipboardLogger { private final UiEventLogger mUiEventLogger; private boolean mGuarded = false; diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java new file mode 100644 index 000000000000..3a040829ba0c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacy.java @@ -0,0 +1,963 @@ +/* + * Copyright (C) 2021 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 static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.view.Display.DEFAULT_DISPLAY; +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; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EDIT_TAPPED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_REMOTE_COPY_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.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT; + +import static java.util.Objects.requireNonNull; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.MainThread; +import android.app.ICompatCameraControlCallback; +import android.app.RemoteAction; +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Insets; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Icon; +import android.hardware.display.DisplayManager; +import android.hardware.input.InputManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Looper; +import android.provider.DeviceConfig; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.MathUtils; +import android.util.Size; +import android.util.TypedValue; +import android.view.Display; +import android.view.DisplayCutout; +import android.view.Gravity; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.LinearInterpolator; +import android.view.animation.PathInterpolator; +import android.view.textclassifier.TextClassification; +import android.view.textclassifier.TextClassificationManager; +import android.view.textclassifier.TextClassifier; +import android.view.textclassifier.TextLinks; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.android.internal.logging.UiEventLogger; +import com.android.internal.policy.PhoneWindow; +import com.android.systemui.R; +import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.screenshot.DraggableConstraintLayout; +import com.android.systemui.screenshot.FloatingWindowUtil; +import com.android.systemui.screenshot.OverlayActionChip; +import com.android.systemui.screenshot.TimeoutHandler; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * Controls state and UI for the overlay that appears when something is added to the clipboard + */ +public class ClipboardOverlayControllerLegacy implements ClipboardListener.ClipboardOverlay { + private static final String TAG = "ClipboardOverlayCtrlr"; + private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY"; + + /** Constants for screenshot/copy deconflicting */ + public static final String SCREENSHOT_ACTION = "com.android.systemui.SCREENSHOT"; + public static final String SELF_PERMISSION = "com.android.systemui.permission.SELF"; + public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY"; + + private static final String EXTRA_EDIT_SOURCE_CLIPBOARD = "edit_source_clipboard"; + + private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000; + private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe + private static final int FONT_SEARCH_STEP_PX = 4; + + private final Context mContext; + private final ClipboardLogger mClipboardLogger; + private final BroadcastDispatcher mBroadcastDispatcher; + private final DisplayManager mDisplayManager; + private final DisplayMetrics mDisplayMetrics; + private final WindowManager mWindowManager; + private final WindowManager.LayoutParams mWindowLayoutParams; + private final PhoneWindow mWindow; + private final TimeoutHandler mTimeoutHandler; + private final AccessibilityManager mAccessibilityManager; + private final TextClassifier mTextClassifier; + + private final DraggableConstraintLayout mView; + private final View mClipboardPreview; + private final ImageView mImagePreview; + private final TextView mTextPreview; + private final TextView mHiddenPreview; + private final View mPreviewBorder; + private final OverlayActionChip mEditChip; + private final OverlayActionChip mShareChip; + private final OverlayActionChip mRemoteCopyChip; + private final View mActionContainerBackground; + private final View mDismissButton; + private final LinearLayout mActionContainer; + private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>(); + + private Runnable mOnSessionCompleteListener; + + private InputMonitor mInputMonitor; + private InputEventReceiver mInputEventReceiver; + + private BroadcastReceiver mCloseDialogsReceiver; + private BroadcastReceiver mScreenshotReceiver; + + private boolean mBlockAttach = false; + private Animator mExitAnimator; + private Animator mEnterAnimator; + private final int mOrientation; + private boolean mKeyboardVisible; + + + public ClipboardOverlayControllerLegacy(Context context, + BroadcastDispatcher broadcastDispatcher, + BroadcastSender broadcastSender, + TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) { + mBroadcastDispatcher = broadcastDispatcher; + mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class)); + final Context displayContext = context.createDisplayContext(getDefaultDisplay()); + mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null); + + mClipboardLogger = new ClipboardLogger(uiEventLogger); + + mAccessibilityManager = AccessibilityManager.getInstance(mContext); + mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class)) + .getTextClassifier(); + + mWindowManager = mContext.getSystemService(WindowManager.class); + + mDisplayMetrics = new DisplayMetrics(); + mContext.getDisplay().getRealMetrics(mDisplayMetrics); + + mTimeoutHandler = timeoutHandler; + mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS); + + // Setup the window that we are going to use + mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); + mWindowLayoutParams.setTitle("ClipboardOverlay"); + + mWindow = FloatingWindowUtil.getFloatingWindow(mContext); + mWindow.setWindowManager(mWindowManager, null, null); + + setWindowFocusable(false); + + mView = (DraggableConstraintLayout) + LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay_legacy, null); + mActionContainerBackground = + requireNonNull(mView.findViewById(R.id.actions_container_background)); + mActionContainer = requireNonNull(mView.findViewById(R.id.actions)); + mClipboardPreview = requireNonNull(mView.findViewById(R.id.clipboard_preview)); + mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview)); + mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview)); + mHiddenPreview = requireNonNull(mView.findViewById(R.id.hidden_preview)); + mPreviewBorder = requireNonNull(mView.findViewById(R.id.preview_border)); + mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip)); + mShareChip = requireNonNull(mView.findViewById(R.id.share_chip)); + mRemoteCopyChip = requireNonNull(mView.findViewById(R.id.remote_copy_chip)); + mEditChip.setAlpha(1); + mShareChip.setAlpha(1); + mRemoteCopyChip.setAlpha(1); + mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button)); + + mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share)); + mView.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() { + @Override + public void onInteraction() { + mTimeoutHandler.resetTimeout(); + } + + @Override + public void onSwipeDismissInitiated(Animator animator) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); + mExitAnimator = animator; + } + + @Override + public void onDismissComplete() { + hideImmediate(); + } + }); + + mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> { + int availableHeight = mTextPreview.getHeight() + - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom()); + mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight()); + return true; + }); + + mDismissButton.setOnClickListener(view -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + animateOut(); + }); + + mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); + mRemoteCopyChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true); + mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); + mOrientation = mContext.getResources().getConfiguration().orientation; + + attachWindow(); + withWindowAttached(() -> { + mWindow.setContentView(mView); + WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime()); + updateInsets(insets); + mWindow.peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + WindowInsets insets = + mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime()); + if (keyboardVisible != mKeyboardVisible) { + mKeyboardVisible = keyboardVisible; + updateInsets(insets); + } + } + }); + mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( + new ViewRootImpl.ActivityConfigCallback() { + @Override + public void onConfigurationChanged(Configuration overrideConfig, + int newDisplayId) { + if (mContext.getResources().getConfiguration().orientation + != mOrientation) { + mClipboardLogger.logSessionComplete( + CLIPBOARD_OVERLAY_DISMISSED_OTHER); + hideImmediate(); + } + } + + @Override + public void requestCompatCameraControl( + boolean showControl, boolean transformationApplied, + ICompatCameraControlCallback callback) { + Log.w(TAG, "unexpected requestCompatCameraControl call"); + } + }); + }); + + mTimeoutHandler.setOnTimeoutRunnable(() -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT); + animateOut(); + }); + + mCloseDialogsReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER); + animateOut(); + } + } + }; + + mBroadcastDispatcher.registerReceiver(mCloseDialogsReceiver, + new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS)); + mScreenshotReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (SCREENSHOT_ACTION.equals(intent.getAction())) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER); + animateOut(); + } + } + }; + + mBroadcastDispatcher.registerReceiver(mScreenshotReceiver, + new IntentFilter(SCREENSHOT_ACTION), null, null, Context.RECEIVER_EXPORTED, + SELF_PERMISSION); + monitorOutsideTouches(); + + Intent copyIntent = new Intent(COPY_OVERLAY_ACTION); + // Set package name so the system knows it's safe + copyIntent.setPackage(mContext.getPackageName()); + broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION); + } + + @Override // ClipboardListener.ClipboardOverlay + public void setClipData(ClipData clipData, String clipSource) { + if (mExitAnimator != null && mExitAnimator.isRunning()) { + mExitAnimator.cancel(); + } + reset(); + String accessibilityAnnouncement; + + boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null + && clipData.getDescription().getExtras() + .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE); + if (clipData == null || clipData.getItemCount() == 0) { + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) { + ClipData.Item item = clipData.getItemAt(0); + if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) { + if (item.getTextLinks() != null) { + AsyncTask.execute(() -> classifyText(clipData.getItemAt(0), clipSource)); + } + } + if (isSensitive) { + showEditableText( + mContext.getResources().getString(R.string.clipboard_asterisks), true); + } else { + showEditableText(item.getText(), false); + } + showShareChip(clipData); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied); + } else if (clipData.getItemAt(0).getUri() != null) { + if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) { + showShareChip(clipData); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied); + } else { + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + } + } else { + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + } + Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext); + // Only show remote copy if it's available. + PackageManager packageManager = mContext.getPackageManager(); + if (packageManager.resolveActivity( + remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) { + mRemoteCopyChip.setContentDescription( + mContext.getString(R.string.clipboard_send_nearby_description)); + mRemoteCopyChip.setVisibility(View.VISIBLE); + mRemoteCopyChip.setOnClickListener((v) -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED); + mContext.startActivity(remoteCopyIntent); + animateOut(); + }); + mActionContainerBackground.setVisibility(View.VISIBLE); + } else { + mRemoteCopyChip.setVisibility(View.GONE); + } + withWindowAttached(() -> { + if (mEnterAnimator == null || !mEnterAnimator.isRunning()) { + mView.post(this::animateIn); + } + mView.announceForAccessibility(accessibilityAnnouncement); + }); + mTimeoutHandler.resetTimeout(); + } + + @Override // ClipboardListener.ClipboardOverlay + public void setOnSessionCompleteListener(Runnable runnable) { + mOnSessionCompleteListener = runnable; + } + + private void classifyText(ClipData.Item item, String source) { + ArrayList<RemoteAction> actions = new ArrayList<>(); + for (TextLinks.TextLink link : item.getTextLinks().getLinks()) { + TextClassification classification = mTextClassifier.classifyText( + item.getText(), link.getStart(), link.getEnd(), null); + actions.addAll(classification.getActions()); + } + mView.post(() -> { + resetActionChips(); + if (actions.size() > 0) { + mActionContainerBackground.setVisibility(View.VISIBLE); + for (RemoteAction action : actions) { + Intent targetIntent = action.getActionIntent().getIntent(); + ComponentName component = targetIntent.getComponent(); + if (component != null && !TextUtils.equals(source, + component.getPackageName())) { + OverlayActionChip chip = constructActionChip(action); + mActionContainer.addView(chip); + mActionChips.add(chip); + break; // only show at most one action chip + } + } + } + }); + } + + private void showShareChip(ClipData clip) { + mShareChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mShareChip.setOnClickListener((v) -> shareContent(clip)); + } + + private OverlayActionChip constructActionChip(RemoteAction action) { + OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate( + R.layout.overlay_action_chip, mActionContainer, false); + chip.setText(action.getTitle()); + chip.setContentDescription(action.getTitle()); + chip.setIcon(action.getIcon(), false); + chip.setPendingIntent(action.getActionIntent(), () -> { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED); + animateOut(); + }); + chip.setAlpha(1); + return chip; + } + + private void monitorOutsideTouches() { + InputManager inputManager = mContext.getSystemService(InputManager.class); + mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0); + mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(), + Looper.getMainLooper()) { + @Override + public void onInputEvent(InputEvent event) { + if (event instanceof MotionEvent) { + MotionEvent motionEvent = (MotionEvent) event; + if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + Region touchRegion = new Region(); + + final Rect tmpRect = new Rect(); + mPreviewBorder.getBoundsOnScreen(tmpRect); + tmpRect.inset( + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, + -SWIPE_PADDING_DP)); + touchRegion.op(tmpRect, Region.Op.UNION); + mActionContainerBackground.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); + if (!touchRegion.contains( + (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE); + animateOut(); + } + } + } + finishInputEvent(event, true /* handled */); + } + }; + } + + private void editImage(Uri uri) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED); + mContext.startActivity(IntentCreator.getImageEditIntent(uri, mContext)); + animateOut(); + } + + private void editText() { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED); + mContext.startActivity(IntentCreator.getTextEditorIntent(mContext)); + animateOut(); + } + + private void shareContent(ClipData clip) { + mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SHARE_TAPPED); + mContext.startActivity(IntentCreator.getShareIntent(clip, mContext)); + animateOut(); + } + + private void showSinglePreview(View v) { + mTextPreview.setVisibility(View.GONE); + mImagePreview.setVisibility(View.GONE); + mHiddenPreview.setVisibility(View.GONE); + v.setVisibility(View.VISIBLE); + } + + private void showTextPreview(CharSequence text, TextView textView) { + showSinglePreview(textView); + final CharSequence truncatedText = text.subSequence(0, Math.min(500, text.length())); + textView.setText(truncatedText); + updateTextSize(truncatedText, textView); + + textView.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (right - left != oldRight - oldLeft) { + updateTextSize(truncatedText, textView); + } + }); + mEditChip.setVisibility(View.GONE); + } + + private void updateTextSize(CharSequence text, TextView textView) { + Paint paint = new Paint(textView.getPaint()); + Resources res = textView.getResources(); + float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font); + float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font); + if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) { + // If the text is a single word and would fit within the TextView at the min font size, + // find the biggest font size that will fit. + float fontSizePx = minFontSize; + while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize + && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) { + fontSizePx += FONT_SEARCH_STEP_PX; + } + // Need to turn off autosizing, otherwise setTextSize is a no-op. + textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE); + // It's possible to hit the max font size and not fill the width, so centering + // horizontally looks better in this case. + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx); + } else { + // Otherwise just stick with autosize. + textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize, + (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX); + textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); + } + } + + private static boolean fitsInView(CharSequence text, TextView textView, Paint paint, + float fontSizePx) { + paint.setTextSize(fontSizePx); + float size = paint.measureText(text.toString()); + float availableWidth = textView.getWidth() - textView.getPaddingLeft() + - textView.getPaddingRight(); + return size < availableWidth; + } + + private static boolean isOneWord(CharSequence text) { + return text.toString().split("\\s+", 2).length == 1; + } + + private void showEditableText(CharSequence text, boolean hidden) { + TextView textView = hidden ? mHiddenPreview : mTextPreview; + showTextPreview(text, textView); + View.OnClickListener listener = v -> editText(); + setAccessibilityActionToEdit(textView); + if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { + mEditChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mEditChip.setContentDescription( + mContext.getString(R.string.clipboard_edit_text_description)); + mEditChip.setOnClickListener(listener); + } + textView.setOnClickListener(listener); + } + + private boolean tryShowEditableImage(Uri uri, boolean isSensitive) { + View.OnClickListener listener = v -> editImage(uri); + ContentResolver resolver = mContext.getContentResolver(); + String mimeType = resolver.getType(uri); + boolean isEditableImage = mimeType != null && mimeType.startsWith("image"); + if (isSensitive) { + mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden)); + showSinglePreview(mHiddenPreview); + if (isEditableImage) { + mHiddenPreview.setOnClickListener(listener); + setAccessibilityActionToEdit(mHiddenPreview); + } + } else if (isEditableImage) { // if the MIMEtype is image, try to load + try { + int size = mContext.getResources().getDimensionPixelSize(R.dimen.overlay_x_scale); + // The width of the view is capped, height maintains aspect ratio, so allow it to be + // taller if needed. + Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null); + showSinglePreview(mImagePreview); + mImagePreview.setImageBitmap(thumbnail); + mImagePreview.setOnClickListener(listener); + setAccessibilityActionToEdit(mImagePreview); + } catch (IOException e) { + Log.e(TAG, "Thumbnail loading failed", e); + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + isEditableImage = false; + } + } else { + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); + } + if (isEditableImage && DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) { + mEditChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mEditChip.setOnClickListener(listener); + mEditChip.setContentDescription( + mContext.getString(R.string.clipboard_edit_image_description)); + } + return isEditableImage; + } + + private void setAccessibilityActionToEdit(View view) { + ViewCompat.replaceAccessibilityAction(view, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + mContext.getString(R.string.clipboard_edit), null); + } + + private void animateIn() { + if (mAccessibilityManager.isEnabled()) { + mDismissButton.setVisibility(View.VISIBLE); + } + mEnterAnimator = getEnterAnimation(); + mEnterAnimator.start(); + } + + private void animateOut() { + if (mExitAnimator != null && mExitAnimator.isRunning()) { + return; + } + Animator anim = getExitAnimation(); + anim.addListener(new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (!mCancelled) { + hideImmediate(); + } + } + }); + mExitAnimator = anim; + anim.start(); + } + + private Animator getEnterAnimation() { + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f); + AnimatorSet enterAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(66); + rootAnim.addUpdateListener(animation -> { + mView.setAlpha(animation.getAnimatedFraction()); + }); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(333); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction()); + float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionsScaleX); + mActionContainer.setScaleY(actionsScaleY); + mActionContainerBackground.setScaleX(actionsScaleX); + mActionContainerBackground.setScaleY(actionsScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(283); + alphaAnim.addUpdateListener(animation -> { + float alpha = animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + mActionContainer.setAlpha(0); + mPreviewBorder.setAlpha(0); + mClipboardPreview.setAlpha(0); + enterAnim.play(rootAnim).with(scaleAnim); + enterAnim.play(alphaAnim).after(50).after(rootAnim); + + enterAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mView.setAlpha(1); + mTimeoutHandler.resetTimeout(); + } + }); + return enterAnim; + } + + private Animator getExitAnimation() { + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f); + AnimatorSet exitAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(100); + rootAnim.addUpdateListener(anim -> mView.setAlpha(1 - anim.getAnimatedFraction())); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(250); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction()); + float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionScaleX); + mActionContainer.setScaleY(actionScaleY); + mActionContainerBackground.setScaleX(actionScaleX); + mActionContainerBackground.setScaleY(actionScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(166); + alphaAnim.addUpdateListener(animation -> { + float alpha = 1 - animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + exitAnim.play(alphaAnim).with(scaleAnim); + exitAnim.play(rootAnim).after(150).after(alphaAnim); + return exitAnim; + } + + private void hideImmediate() { + // Note this may be called multiple times if multiple dismissal events happen at the same + // time. + mTimeoutHandler.cancelTimeout(); + final View decorView = mWindow.peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.removeViewImmediate(decorView); + } + if (mCloseDialogsReceiver != null) { + mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver); + mCloseDialogsReceiver = null; + } + if (mScreenshotReceiver != null) { + mBroadcastDispatcher.unregisterReceiver(mScreenshotReceiver); + mScreenshotReceiver = null; + } + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + } + if (mInputMonitor != null) { + mInputMonitor.dispose(); + mInputMonitor = null; + } + if (mOnSessionCompleteListener != null) { + mOnSessionCompleteListener.run(); + } + } + + private void resetActionChips() { + for (OverlayActionChip chip : mActionChips) { + mActionContainer.removeView(chip); + } + mActionChips.clear(); + } + + private void reset() { + mView.setTranslationX(0); + mView.setAlpha(0); + mActionContainerBackground.setVisibility(View.GONE); + mShareChip.setVisibility(View.GONE); + mEditChip.setVisibility(View.GONE); + mRemoteCopyChip.setVisibility(View.GONE); + resetActionChips(); + mTimeoutHandler.cancelTimeout(); + mClipboardLogger.reset(); + } + + @MainThread + private void attachWindow() { + View decorView = mWindow.getDecorView(); + if (decorView.isAttachedToWindow() || mBlockAttach) { + return; + } + mBlockAttach = true; + mWindowManager.addView(decorView, mWindowLayoutParams); + decorView.requestApplyInsets(); + mView.requestApplyInsets(); + decorView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + mBlockAttach = false; + } + + @Override + public void onWindowDetached() { + } + } + ); + } + + private void withWindowAttached(Runnable action) { + View decorView = mWindow.getDecorView(); + if (decorView.isAttachedToWindow()) { + action.run(); + } else { + decorView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + mBlockAttach = false; + decorView.getViewTreeObserver().removeOnWindowAttachListener(this); + action.run(); + } + + @Override + public void onWindowDetached() { + } + }); + } + } + + private void updateInsets(WindowInsets insets) { + int orientation = mContext.getResources().getConfiguration().orientation; + FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mView.getLayoutParams(); + if (p == null) { + return; + } + DisplayCutout cutout = insets.getDisplayCutout(); + Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); + Insets imeInsets = insets.getInsets(WindowInsets.Type.ime()); + if (cutout == null) { + p.setMargins(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom)); + } else { + Insets waterfall = cutout.getWaterfallInsets(); + if (orientation == ORIENTATION_PORTRAIT) { + p.setMargins( + waterfall.left, + Math.max(cutout.getSafeInsetTop(), waterfall.top), + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(cutout.getSafeInsetBottom(), + Math.max(navBarInsets.bottom, waterfall.bottom)))); + } else { + p.setMargins( + waterfall.left, + waterfall.top, + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(navBarInsets.bottom, waterfall.bottom))); + } + } + mView.setLayoutParams(p); + mView.requestLayout(); + } + + private Display getDefaultDisplay() { + return mDisplayManager.getDisplay(DEFAULT_DISPLAY); + } + + /** + * Updates the window focusability. If the window is already showing, then it updates the + * window immediately, otherwise the layout params will be applied when the window is next + * shown. + */ + private void setWindowFocusable(boolean focusable) { + int flags = mWindowLayoutParams.flags; + if (focusable) { + mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } + if (mWindowLayoutParams.flags == flags) { + return; + } + final View decorView = mWindow.peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); + } + } + + static class ClipboardLogger { + private final UiEventLogger mUiEventLogger; + private boolean mGuarded = false; + + ClipboardLogger(UiEventLogger uiEventLogger) { + mUiEventLogger = uiEventLogger; + } + + void logSessionComplete(@NonNull UiEventLogger.UiEventEnum event) { + if (!mGuarded) { + mGuarded = true; + mUiEventLogger.log(event); + } + } + + void reset() { + mGuarded = false; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java index 8b0b2a59dd92..0d989a78947d 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerFactory.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerLegacyFactory.java @@ -27,17 +27,17 @@ import com.android.systemui.screenshot.TimeoutHandler; import javax.inject.Inject; /** - * A factory that churns out ClipboardOverlayControllers on demand. + * A factory that churns out ClipboardOverlayControllerLegacys on demand. */ @SysUISingleton -public class ClipboardOverlayControllerFactory { +public class ClipboardOverlayControllerLegacyFactory { private final UiEventLogger mUiEventLogger; private final BroadcastDispatcher mBroadcastDispatcher; private final BroadcastSender mBroadcastSender; @Inject - public ClipboardOverlayControllerFactory(BroadcastDispatcher broadcastDispatcher, + public ClipboardOverlayControllerLegacyFactory(BroadcastDispatcher broadcastDispatcher, BroadcastSender broadcastSender, UiEventLogger uiEventLogger) { this.mBroadcastDispatcher = broadcastDispatcher; this.mBroadcastSender = broadcastSender; @@ -45,10 +45,10 @@ public class ClipboardOverlayControllerFactory { } /** - * One new ClipboardOverlayController, coming right up! + * One new ClipboardOverlayControllerLegacy, coming right up! */ - public ClipboardOverlayController create(Context context) { - return new ClipboardOverlayController(context, mBroadcastDispatcher, mBroadcastSender, + public ClipboardOverlayControllerLegacy create(Context context) { + return new ClipboardOverlayControllerLegacy(context, mBroadcastDispatcher, mBroadcastSender, new TimeoutHandler(context), mUiEventLogger); } } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java new file mode 100644 index 000000000000..2d3315759371 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java @@ -0,0 +1,482 @@ +/* + * 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 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; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.app.RemoteAction; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Insets; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Icon; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.MathUtils; +import android.util.TypedValue; +import android.view.DisplayCutout; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowInsets; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.LinearInterpolator; +import android.view.animation.PathInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.android.systemui.R; +import com.android.systemui.screenshot.DraggableConstraintLayout; +import com.android.systemui.screenshot.FloatingWindowUtil; +import com.android.systemui.screenshot.OverlayActionChip; + +import java.util.ArrayList; + +/** + * Handles the visual elements and animations for the clipboard overlay. + */ +public class ClipboardOverlayView extends DraggableConstraintLayout { + + interface ClipboardOverlayCallbacks extends SwipeDismissCallbacks { + void onDismissButtonTapped(); + + void onRemoteCopyButtonTapped(); + + void onEditButtonTapped(); + + void onShareButtonTapped(); + + void onPreviewTapped(); + } + + private static final String TAG = "ClipboardView"; + + private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe + private static final int FONT_SEARCH_STEP_PX = 4; + + private final DisplayMetrics mDisplayMetrics; + private final AccessibilityManager mAccessibilityManager; + private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>(); + + private View mClipboardPreview; + private ImageView mImagePreview; + private TextView mTextPreview; + private TextView mHiddenPreview; + private View mPreviewBorder; + private OverlayActionChip mEditChip; + private OverlayActionChip mShareChip; + private OverlayActionChip mRemoteCopyChip; + private View mActionContainerBackground; + private View mDismissButton; + private LinearLayout mActionContainer; + + public ClipboardOverlayView(Context context) { + this(context, null); + } + + public ClipboardOverlayView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ClipboardOverlayView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mDisplayMetrics = new DisplayMetrics(); + mContext.getDisplay().getRealMetrics(mDisplayMetrics); + mAccessibilityManager = AccessibilityManager.getInstance(mContext); + } + + @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)); + + mEditChip.setAlpha(1); + mShareChip.setAlpha(1); + mRemoteCopyChip.setAlpha(1); + mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share)); + + mEditChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); + mRemoteCopyChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true); + mShareChip.setIcon( + Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); + + mRemoteCopyChip.setContentDescription( + mContext.getString(R.string.clipboard_send_nearby_description)); + + mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> { + int availableHeight = mTextPreview.getHeight() + - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom()); + mTextPreview.setMaxLines(availableHeight / mTextPreview.getLineHeight()); + return true; + }); + super.onFinishInflate(); + } + + @Override + public void setCallbacks(SwipeDismissCallbacks callbacks) { + super.setCallbacks(callbacks); + ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks; + mEditChip.setOnClickListener(v -> clipboardCallbacks.onEditButtonTapped()); + mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped()); + mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped()); + mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped()); + mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped()); + } + + void setEditAccessibilityAction(boolean editable) { + if (editable) { + ViewCompat.replaceAccessibilityAction(mClipboardPreview, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + mContext.getString(R.string.clipboard_edit), null); + } else { + ViewCompat.replaceAccessibilityAction(mClipboardPreview, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + null, null); + } + } + + 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(); + } + + boolean isInTouchRegion(int x, int y) { + Region touchRegion = new Region(); + final Rect tmpRect = new Rect(); + + mPreviewBorder.getBoundsOnScreen(tmpRect); + tmpRect.inset( + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP), + (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP)); + touchRegion.op(tmpRect, Region.Op.UNION); + + mActionContainerBackground.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); + + return touchRegion.contains(x, y); + } + + void setRemoteCopyVisibility(boolean visible) { + if (visible) { + mRemoteCopyChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + } else { + mRemoteCopyChip.setVisibility(View.GONE); + } + } + + void showDefaultTextPreview() { + String copied = mContext.getString(R.string.clipboard_overlay_text_copied); + showTextPreview(copied, false); + } + + void showTextPreview(CharSequence text, boolean hidden) { + TextView textView = hidden ? mHiddenPreview : mTextPreview; + showSinglePreview(textView); + textView.setText(text.subSequence(0, Math.min(500, text.length()))); + updateTextSize(text, textView); + textView.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (right - left != oldRight - oldLeft) { + updateTextSize(text, textView); + } + }); + mEditChip.setVisibility(View.GONE); + } + + void showImagePreview(@Nullable Bitmap thumbnail) { + if (thumbnail == null) { + mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden)); + showSinglePreview(mHiddenPreview); + } else { + mImagePreview.setImageBitmap(thumbnail); + showSinglePreview(mImagePreview); + } + } + + void showEditChip(String contentDescription) { + mEditChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + mEditChip.setContentDescription(contentDescription); + } + + void showShareChip() { + mShareChip.setVisibility(View.VISIBLE); + mActionContainerBackground.setVisibility(View.VISIBLE); + } + + void reset() { + setTranslationX(0); + setAlpha(0); + mActionContainerBackground.setVisibility(View.GONE); + mDismissButton.setVisibility(View.GONE); + mShareChip.setVisibility(View.GONE); + mEditChip.setVisibility(View.GONE); + mRemoteCopyChip.setVisibility(View.GONE); + setEditAccessibilityAction(false); + resetActionChips(); + } + + void resetActionChips() { + for (OverlayActionChip chip : mActionChips) { + mActionContainer.removeView(chip); + } + mActionChips.clear(); + } + + Animator getEnterAnimation() { + if (mAccessibilityManager.isEnabled()) { + mDismissButton.setVisibility(View.VISIBLE); + } + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f); + AnimatorSet enterAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(66); + rootAnim.addUpdateListener(animation -> { + setAlpha(animation.getAnimatedFraction()); + }); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(333); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction()); + float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionsScaleX); + mActionContainer.setScaleY(actionsScaleY); + mActionContainerBackground.setScaleX(actionsScaleX); + mActionContainerBackground.setScaleY(actionsScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(283); + alphaAnim.addUpdateListener(animation -> { + float alpha = animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + mActionContainer.setAlpha(0); + mPreviewBorder.setAlpha(0); + mClipboardPreview.setAlpha(0); + enterAnim.play(rootAnim).with(scaleAnim); + enterAnim.play(alphaAnim).after(50).after(rootAnim); + + enterAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + setAlpha(1); + } + }); + return enterAnim; + } + + Animator getExitAnimation() { + TimeInterpolator linearInterpolator = new LinearInterpolator(); + TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f); + AnimatorSet exitAnim = new AnimatorSet(); + + ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1); + rootAnim.setInterpolator(linearInterpolator); + rootAnim.setDuration(100); + rootAnim.addUpdateListener(anim -> setAlpha(1 - anim.getAnimatedFraction())); + + ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1); + scaleAnim.setInterpolator(scaleInterpolator); + scaleAnim.setDuration(250); + scaleAnim.addUpdateListener(animation -> { + float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mClipboardPreview.setScaleX(previewScale); + mClipboardPreview.setScaleY(previewScale); + mPreviewBorder.setScaleX(previewScale); + mPreviewBorder.setScaleY(previewScale); + + float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX(); + mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX()); + mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX()); + float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction()); + float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction()); + mActionContainer.setScaleX(actionScaleX); + mActionContainer.setScaleY(actionScaleY); + mActionContainerBackground.setScaleX(actionScaleX); + mActionContainerBackground.setScaleY(actionScaleY); + }); + + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setInterpolator(linearInterpolator); + alphaAnim.setDuration(166); + alphaAnim.addUpdateListener(animation -> { + float alpha = 1 - animation.getAnimatedFraction(); + mClipboardPreview.setAlpha(alpha); + mPreviewBorder.setAlpha(alpha); + mDismissButton.setAlpha(alpha); + mActionContainer.setAlpha(alpha); + }); + + exitAnim.play(alphaAnim).with(scaleAnim); + exitAnim.play(rootAnim).after(150).after(alphaAnim); + return exitAnim; + } + + void setActionChip(RemoteAction action, Runnable onFinish) { + mActionContainerBackground.setVisibility(View.VISIBLE); + OverlayActionChip chip = constructActionChip(action, onFinish); + mActionContainer.addView(chip); + mActionChips.add(chip); + } + + private void showSinglePreview(View v) { + mTextPreview.setVisibility(View.GONE); + mImagePreview.setVisibility(View.GONE); + mHiddenPreview.setVisibility(View.GONE); + v.setVisibility(View.VISIBLE); + } + + private OverlayActionChip constructActionChip(RemoteAction action, Runnable onFinish) { + OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate( + R.layout.overlay_action_chip, mActionContainer, false); + chip.setText(action.getTitle()); + chip.setContentDescription(action.getTitle()); + chip.setIcon(action.getIcon(), false); + chip.setPendingIntent(action.getActionIntent(), onFinish); + chip.setAlpha(1); + return chip; + } + + private static void updateTextSize(CharSequence text, TextView textView) { + Paint paint = new Paint(textView.getPaint()); + Resources res = textView.getResources(); + float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font); + float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font); + if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) { + // If the text is a single word and would fit within the TextView at the min font size, + // find the biggest font size that will fit. + float fontSizePx = minFontSize; + while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize + && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) { + fontSizePx += FONT_SEARCH_STEP_PX; + } + // Need to turn off autosizing, otherwise setTextSize is a no-op. + textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE); + // It's possible to hit the max font size and not fill the width, so centering + // horizontally looks better in this case. + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx); + } else { + // Otherwise just stick with autosize. + textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize, + (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX); + textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); + } + } + + private static boolean fitsInView(CharSequence text, TextView textView, Paint paint, + float fontSizePx) { + paint.setTextSize(fontSizePx); + float size = paint.measureText(text.toString()); + float availableWidth = textView.getWidth() - textView.getPaddingLeft() + - textView.getPaddingRight(); + return size < availableWidth; + } + + private static boolean isOneWord(CharSequence text) { + return text.toString().split("\\s+", 2).length == 1; + } + + private static Rect computeMargins(WindowInsets insets, int orientation) { + DisplayCutout cutout = insets.getDisplayCutout(); + Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); + Insets imeInsets = insets.getInsets(WindowInsets.Type.ime()); + if (cutout == null) { + return new Rect(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom)); + } else { + Insets waterfall = cutout.getWaterfallInsets(); + if (orientation == ORIENTATION_PORTRAIT) { + return new Rect( + waterfall.left, + Math.max(cutout.getSafeInsetTop(), waterfall.top), + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(cutout.getSafeInsetBottom(), + Math.max(navBarInsets.bottom, waterfall.bottom)))); + } else { + return new Rect( + waterfall.left, + waterfall.top, + waterfall.right, + Math.max(imeInsets.bottom, + Math.max(navBarInsets.bottom, waterfall.bottom))); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java new file mode 100644 index 000000000000..9dac9b393d60 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java @@ -0,0 +1,174 @@ +/* + * 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.annotation.MainThread; +import android.annotation.NonNull; +import android.app.ICompatCameraControlCallback; +import android.content.Context; +import android.content.res.Configuration; +import android.util.Log; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; + +import com.android.internal.policy.PhoneWindow; +import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext; +import com.android.systemui.screenshot.FloatingWindowUtil; + +import java.util.function.BiConsumer; + +import javax.inject.Inject; + +/** + * Handles attaching the window and the window insets for the clipboard overlay. + */ +public class ClipboardOverlayWindow extends PhoneWindow + implements ViewRootImpl.ActivityConfigCallback { + private static final String TAG = "ClipboardOverlayWindow"; + + private final Context mContext; + private final WindowManager mWindowManager; + private final WindowManager.LayoutParams mWindowLayoutParams; + + private boolean mKeyboardVisible; + private final int mOrientation; + private BiConsumer<WindowInsets, Integer> mOnKeyboardChangeListener; + private Runnable mOnOrientationChangeListener; + + @Inject + ClipboardOverlayWindow(@OverlayWindowContext Context context) { + super(context); + mContext = context; + mOrientation = mContext.getResources().getConfiguration().orientation; + + // Setup the window that we are going to use + requestFeature(Window.FEATURE_NO_TITLE); + requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS); + setBackgroundDrawableResource(android.R.color.transparent); + mWindowManager = mContext.getSystemService(WindowManager.class); + mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); + mWindowLayoutParams.setTitle("ClipboardOverlay"); + setWindowManager(mWindowManager, null, null); + setWindowFocusable(false); + } + + /** + * Set callbacks for keyboard state change and orientation change and attach the window + * + * @param onKeyboardChangeListener callback for IME visibility changes + * @param onOrientationChangeListener callback for device orientation changes + */ + public void init(@NonNull BiConsumer<WindowInsets, Integer> onKeyboardChangeListener, + @NonNull Runnable onOrientationChangeListener) { + mOnKeyboardChangeListener = onKeyboardChangeListener; + mOnOrientationChangeListener = onOrientationChangeListener; + + attach(); + withWindowAttached(() -> { + WindowInsets currentInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + mKeyboardVisible = currentInsets.isVisible(WindowInsets.Type.ime()); + peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(() -> { + WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime()); + if (keyboardVisible != mKeyboardVisible) { + mKeyboardVisible = keyboardVisible; + mOnKeyboardChangeListener.accept(insets, mOrientation); + } + }); + peekDecorView().getViewRootImpl().setActivityConfigCallback(this); + }); + } + + @Override // ViewRootImpl.ActivityConfigCallback + public void onConfigurationChanged(Configuration overrideConfig, int newDisplayId) { + if (mContext.getResources().getConfiguration().orientation != mOrientation) { + mOnOrientationChangeListener.run(); + } + } + + @Override // ViewRootImpl.ActivityConfigCallback + public void requestCompatCameraControl(boolean showControl, boolean transformationApplied, + ICompatCameraControlCallback callback) { + Log.w(TAG, "unexpected requestCompatCameraControl call"); + } + + void remove() { + final View decorView = peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.removeViewImmediate(decorView); + } + } + + WindowInsets getWindowInsets() { + return mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + } + + void withWindowAttached(Runnable action) { + View decorView = getDecorView(); + if (decorView.isAttachedToWindow()) { + action.run(); + } else { + decorView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + decorView.getViewTreeObserver().removeOnWindowAttachListener(this); + action.run(); + } + + @Override + public void onWindowDetached() { + } + }); + } + } + + @MainThread + private void attach() { + View decorView = getDecorView(); + if (decorView.isAttachedToWindow()) { + return; + } + mWindowManager.addView(decorView, mWindowLayoutParams); + decorView.requestApplyInsets(); + } + + /** + * Updates the window focusability. If the window is already showing, then it updates the + * window immediately, otherwise the layout params will be applied when the window is next + * shown. + */ + private void setWindowFocusable(boolean focusable) { + int flags = mWindowLayoutParams.flags; + if (focusable) { + mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } + if (mWindowLayoutParams.flags == flags) { + return; + } + final View decorView = peekDecorView(); + if (decorView != null && decorView.isAttachedToWindow()) { + mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java new file mode 100644 index 000000000000..22448130f7e5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java @@ -0,0 +1,68 @@ +/* + * 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.dagger; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.view.Display; +import android.view.LayoutInflater; + +import com.android.systemui.R; +import com.android.systemui.clipboardoverlay.ClipboardOverlayView; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +import dagger.Module; +import dagger.Provides; + +/** Module for {@link com.android.systemui.clipboardoverlay}. */ +@Module +public interface ClipboardOverlayModule { + + /** + * + */ + @Provides + @OverlayWindowContext + static Context provideWindowContext(DisplayManager displayManager, Context context) { + Display display = displayManager.getDisplay(DEFAULT_DISPLAY); + return context.createWindowContext(display, TYPE_SCREENSHOT, null); + } + + /** + * + */ + @Provides + static ClipboardOverlayView provideClipboardOverlayView(@OverlayWindowContext Context context) { + return (ClipboardOverlayView) LayoutInflater.from(context).inflate( + R.layout.clipboard_overlay, null); + } + + @Qualifier + @Documented + @Retention(RUNTIME) + @interface OverlayWindowContext { + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index dc3dadb32669..d7638d663dc9 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -33,6 +33,7 @@ import com.android.systemui.biometrics.AlternateUdfpsTouchProvider; import com.android.systemui.biometrics.UdfpsDisplayModeProvider; import com.android.systemui.biometrics.dagger.BiometricsModule; import com.android.systemui.classifier.FalsingModule; +import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule; import com.android.systemui.controls.dagger.ControlsModule; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.dagger.DemoModeModule; @@ -118,6 +119,7 @@ import dagger.Provides; AssistModule.class, BiometricsModule.class, BouncerViewModule.class, + ClipboardOverlayModule.class, ClockModule.class, CoroutinesModule.class, DreamModule.class, @@ -165,12 +167,16 @@ public abstract class SystemUIModule { @Binds abstract BootCompleteCache bindBootCompleteCache(BootCompleteCacheImpl bootCompleteCache); - /** */ + /** + * + */ @Binds public abstract ContextComponentHelper bindComponentHelper( ContextComponentResolver componentHelper); - /** */ + /** + * + */ @Binds public abstract NotificationRowBinder bindNotificationRowBinder( NotificationRowBinderImpl notificationRowBinder); @@ -209,6 +215,7 @@ public abstract class SystemUIModule { abstract SystemClock bindSystemClock(SystemClockImpl systemClock); // TODO: This should provided by the WM component + /** Provides Optional of BubbleManager */ @SysUISingleton @Provides diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 5506f4c4c8ed..6e2f22fa29f7 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -304,6 +304,9 @@ public class Flags { // 1500 - chooser public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500); + // 1700 - clipboard + public static final UnreleasedFlag CLIPBOARD_OVERLAY_REFACTOR = new UnreleasedFlag(1700); + // Pay no attention to the reflection behind the curtain. // ========================== Curtain ========================== // | | diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java index 91214a85ddd5..e7e6918325a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java @@ -38,6 +38,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.util.DeviceConfigProxyFake; import org.junit.Before; @@ -47,6 +49,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import javax.inject.Provider; @SmallTest @RunWith(AndroidJUnit4.class) @@ -55,11 +60,15 @@ public class ClipboardListenerTest extends SysuiTestCase { @Mock private ClipboardManager mClipboardManager; @Mock - private ClipboardOverlayControllerFactory mClipboardOverlayControllerFactory; + private ClipboardOverlayControllerLegacyFactory mClipboardOverlayControllerLegacyFactory; + @Mock + private ClipboardOverlayControllerLegacy mOverlayControllerLegacy; @Mock private ClipboardOverlayController mOverlayController; @Mock private UiEventLogger mUiEventLogger; + @Mock + private FeatureFlags mFeatureFlags; private DeviceConfigProxyFake mDeviceConfigProxy; private ClipData mSampleClipData; @@ -72,12 +81,17 @@ public class ClipboardListenerTest extends SysuiTestCase { @Captor private ArgumentCaptor<String> mStringCaptor; + @Spy + private Provider<ClipboardOverlayController> mOverlayControllerProvider; + @Before public void setup() { + mOverlayControllerProvider = () -> mOverlayController; + MockitoAnnotations.initMocks(this); - when(mClipboardOverlayControllerFactory.create(any())).thenReturn( - mOverlayController); + when(mClipboardOverlayControllerLegacyFactory.create(any())) + .thenReturn(mOverlayControllerLegacy); when(mClipboardManager.hasPrimaryClip()).thenReturn(true); @@ -94,7 +108,8 @@ public class ClipboardListenerTest extends SysuiTestCase { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "false", false); ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); listener.start(); verifyZeroInteractions(mClipboardManager); verifyZeroInteractions(mUiEventLogger); @@ -105,7 +120,8 @@ public class ClipboardListenerTest extends SysuiTestCase { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); listener.start(); verify(mClipboardManager).addPrimaryClipChangedListener(any()); verifyZeroInteractions(mUiEventLogger); @@ -113,16 +129,58 @@ public class ClipboardListenerTest extends SysuiTestCase { @Test public void test_consecutiveCopies() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false); + mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); listener.start(); listener.onPrimaryClipChanged(); - verify(mClipboardOverlayControllerFactory).create(any()); + verify(mClipboardOverlayControllerLegacyFactory).create(any()); - verify(mOverlayController).setClipData(mClipDataCaptor.capture(), mStringCaptor.capture()); + verify(mOverlayControllerLegacy).setClipData( + mClipDataCaptor.capture(), mStringCaptor.capture()); + + assertEquals(mSampleClipData, mClipDataCaptor.getValue()); + assertEquals(mSampleSource, mStringCaptor.getValue()); + + verify(mOverlayControllerLegacy).setOnSessionCompleteListener(mRunnableCaptor.capture()); + + // Should clear the overlay controller + mRunnableCaptor.getValue().run(); + + listener.onPrimaryClipChanged(); + + verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any()); + + // Not calling the runnable here, just change the clip again and verify that the overlay is + // NOT recreated. + + listener.onPrimaryClipChanged(); + + verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any()); + verifyZeroInteractions(mOverlayControllerProvider); + } + + @Test + public void test_consecutiveCopies_new() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true); + + mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, + "true", false); + ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); + listener.start(); + listener.onPrimaryClipChanged(); + + verify(mOverlayControllerProvider).get(); + + verify(mOverlayController).setClipData( + mClipDataCaptor.capture(), mStringCaptor.capture()); assertEquals(mSampleClipData, mClipDataCaptor.getValue()); assertEquals(mSampleSource, mStringCaptor.getValue()); @@ -134,14 +192,15 @@ public class ClipboardListenerTest extends SysuiTestCase { listener.onPrimaryClipChanged(); - verify(mClipboardOverlayControllerFactory, times(2)).create(any()); + verify(mOverlayControllerProvider, times(2)).get(); // Not calling the runnable here, just change the clip again and verify that the overlay is // NOT recreated. listener.onPrimaryClipChanged(); - verify(mClipboardOverlayControllerFactory, times(2)).create(any()); + verify(mOverlayControllerProvider, times(2)).get(); + verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory); } @Test @@ -169,4 +228,40 @@ public class ClipboardListenerTest extends SysuiTestCase { assertTrue(ClipboardListener.shouldSuppressOverlay(suppressableClipData, ClipboardListener.SHELL_PACKAGE, false)); } + + @Test + public void test_logging_enterAndReenter() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false); + + ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); + listener.start(); + + listener.onPrimaryClipChanged(); + listener.onPrimaryClipChanged(); + + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); + } + + @Test + public void test_logging_enterAndReenter_new() { + when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true); + + ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardManager, mUiEventLogger, mFeatureFlags); + listener.start(); + + listener.onPrimaryClipChanged(); + listener.onPrimaryClipChanged(); + + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java new file mode 100644 index 000000000000..d96ca91e36bd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -0,0 +1,183 @@ +/* + * 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 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 org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.animation.Animator; +import android.content.ClipData; +import android.content.ClipDescription; +import android.net.Uri; +import android.os.PersistableBundle; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.screenshot.TimeoutHandler; + +import org.junit.Before; +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; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ClipboardOverlayControllerTest extends SysuiTestCase { + + private ClipboardOverlayController mOverlayController; + @Mock + private ClipboardOverlayView mClipboardOverlayView; + @Mock + private ClipboardOverlayWindow mClipboardOverlayWindow; + @Mock + private BroadcastSender mBroadcastSender; + @Mock + private TimeoutHandler mTimeoutHandler; + @Mock + private UiEventLogger mUiEventLogger; + + @Mock + private Animator mAnimator; + + private ClipData mSampleClipData; + + @Captor + private ArgumentCaptor<ClipboardOverlayView.ClipboardOverlayCallbacks> mOverlayCallbacksCaptor; + private ClipboardOverlayView.ClipboardOverlayCallbacks mCallbacks; + + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator); + when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator); + + mSampleClipData = new ClipData("Test", new String[]{"text/plain"}, + new ClipData.Item("Test Item")); + + mOverlayController = new ClipboardOverlayController( + mContext, + mClipboardOverlayView, + mClipboardOverlayWindow, + getFakeBroadcastDispatcher(), + mBroadcastSender, + mTimeoutHandler, + mUiEventLogger); + verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture()); + mCallbacks = mOverlayCallbacksCaptor.getValue(); + } + + @Test + public void test_setClipData_nullData() { + ClipData clipData = null; + mOverlayController.setClipData(clipData, ""); + + verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); + verify(mClipboardOverlayView, times(0)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_invalidImageData() { + ClipData clipData = new ClipData("", new String[]{"image/png"}, + new ClipData.Item(Uri.parse(""))); + + mOverlayController.setClipData(clipData, ""); + + verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); + verify(mClipboardOverlayView, times(0)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_textData() { + mOverlayController.setClipData(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() { + 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.setClipData(data, ""); + + verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true); + verify(mClipboardOverlayView, times(1)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_repeatedCalls() { + when(mAnimator.isRunning()).thenReturn(true); + + mOverlayController.setClipData(mSampleClipData, ""); + mOverlayController.setClipData(mSampleClipData, ""); + + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_viewCallbacks_onShareTapped() { + mOverlayController.setClipData(mSampleClipData, ""); + + mCallbacks.onShareButtonTapped(); + + verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED); + verify(mClipboardOverlayView, times(1)).getExitAnimation(); + } + + @Test + public void test_viewCallbacks_onDismissTapped() { + mOverlayController.setClipData(mSampleClipData, ""); + + mCallbacks.onDismissButtonTapped(); + + verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + verify(mClipboardOverlayView, times(1)).getExitAnimation(); + } + + @Test + public void test_multipleDismissals_dismissesOnce() { + mCallbacks.onSwipeDismissInitiated(mAnimator); + mCallbacks.onDismissButtonTapped(); + mCallbacks.onSwipeDismissInitiated(mAnimator); + mCallbacks.onDismissButtonTapped(); + + verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED); + verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java deleted file mode 100644 index c7c2cd8d7b4b..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEventTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.provider.DeviceConfig; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.internal.logging.UiEventLogger; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.util.DeviceConfigProxyFake; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class ClipboardOverlayEventTest extends SysuiTestCase { - - @Mock - private ClipboardManager mClipboardManager; - @Mock - private ClipboardOverlayControllerFactory mClipboardOverlayControllerFactory; - @Mock - private ClipboardOverlayController mOverlayController; - @Mock - private UiEventLogger mUiEventLogger; - - private final String mSampleSource = "Example source"; - - private ClipboardListener mClipboardListener; - - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - when(mClipboardOverlayControllerFactory.create(any())).thenReturn( - mOverlayController); - when(mClipboardManager.hasPrimaryClip()).thenReturn(true); - - ClipData sampleClipData = new ClipData("Test", new String[]{"text/plain"}, - new ClipData.Item("Test Item")); - when(mClipboardManager.getPrimaryClip()).thenReturn(sampleClipData); - when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource); - - DeviceConfigProxyFake deviceConfigProxy = new DeviceConfigProxyFake(); - deviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, - "true", false); - - mClipboardListener = new ClipboardListener(getContext(), deviceConfigProxy, - mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger); - } - - @Test - public void test_enterAndReenter() { - mClipboardListener.start(); - - mClipboardListener.onPrimaryClipChanged(); - mClipboardListener.onPrimaryClipChanged(); - - verify(mUiEventLogger, times(1)).log( - ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); - verify(mUiEventLogger, times(1)).log( - ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); - } -} |