Separate clipboard classes into View/Controller to facilitate testing
Bug: 242169425 Test: manual (using sample clipboard app); atest ClipboardOverlayControllerTest Change-Id: Ie7cca264dfd18cb710b21100831390e24387d289
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
~ See the License for the specific language governing permissions and
|
~ See the License for the specific language governing permissions and
|
||||||
~ limitations under the License.
|
~ limitations under the License.
|
||||||
-->
|
-->
|
||||||
<com.android.systemui.screenshot.DraggableConstraintLayout
|
<com.android.systemui.clipboardoverlay.ClipboardOverlayView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
@@ -157,4 +157,4 @@
|
|||||||
android:layout_margin="@dimen/overlay_dismiss_button_margin"
|
android:layout_margin="@dimen/overlay_dismiss_button_margin"
|
||||||
android:src="@drawable/overlay_cancel"/>
|
android:src="@drawable/overlay_cancel"/>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</com.android.systemui.screenshot.DraggableConstraintLayout>
|
</com.android.systemui.clipboardoverlay.ClipboardOverlayView>
|
||||||
160
packages/SystemUI/res/layout/clipboard_overlay_legacy.xml
Normal file
160
packages/SystemUI/res/layout/clipboard_overlay_legacy.xml
Normal file
@@ -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>
|
||||||
@@ -31,9 +31,12 @@ import com.android.internal.annotations.VisibleForTesting;
|
|||||||
import com.android.internal.logging.UiEventLogger;
|
import com.android.internal.logging.UiEventLogger;
|
||||||
import com.android.systemui.CoreStartable;
|
import com.android.systemui.CoreStartable;
|
||||||
import com.android.systemui.dagger.SysUISingleton;
|
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 com.android.systemui.util.DeviceConfigProxy;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ClipboardListener brings up a clipboard overlay when something is copied to the clipboard.
|
* 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 Context mContext;
|
||||||
private final DeviceConfigProxy mDeviceConfig;
|
private final DeviceConfigProxy mDeviceConfig;
|
||||||
private final ClipboardOverlayControllerFactory mOverlayFactory;
|
private final Provider<ClipboardOverlayController> mOverlayProvider;
|
||||||
|
private final ClipboardOverlayControllerLegacyFactory mOverlayFactory;
|
||||||
private final ClipboardManager mClipboardManager;
|
private final ClipboardManager mClipboardManager;
|
||||||
private final UiEventLogger mUiEventLogger;
|
private final UiEventLogger mUiEventLogger;
|
||||||
private ClipboardOverlayController mClipboardOverlayController;
|
private final FeatureFlags mFeatureFlags;
|
||||||
|
private boolean mUsingNewOverlay;
|
||||||
|
private ClipboardOverlay mClipboardOverlay;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy,
|
public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy,
|
||||||
ClipboardOverlayControllerFactory overlayFactory, ClipboardManager clipboardManager,
|
Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
|
||||||
UiEventLogger uiEventLogger) {
|
ClipboardOverlayControllerLegacyFactory overlayFactory,
|
||||||
|
ClipboardManager clipboardManager,
|
||||||
|
UiEventLogger uiEventLogger,
|
||||||
|
FeatureFlags featureFlags) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mDeviceConfig = deviceConfigProxy;
|
mDeviceConfig = deviceConfigProxy;
|
||||||
|
mOverlayProvider = clipboardOverlayControllerProvider;
|
||||||
mOverlayFactory = overlayFactory;
|
mOverlayFactory = overlayFactory;
|
||||||
mClipboardManager = clipboardManager;
|
mClipboardManager = clipboardManager;
|
||||||
mUiEventLogger = uiEventLogger;
|
mUiEventLogger = uiEventLogger;
|
||||||
|
mFeatureFlags = featureFlags;
|
||||||
|
|
||||||
|
mUsingNewOverlay = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -89,16 +102,22 @@ public class ClipboardListener implements
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mClipboardOverlayController == null) {
|
boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
|
||||||
mClipboardOverlayController = mOverlayFactory.create(mContext);
|
if (mClipboardOverlay == null || enabled != mUsingNewOverlay) {
|
||||||
|
mUsingNewOverlay = enabled;
|
||||||
|
if (enabled) {
|
||||||
|
mClipboardOverlay = mOverlayProvider.get();
|
||||||
|
} else {
|
||||||
|
mClipboardOverlay = mOverlayFactory.create(mContext);
|
||||||
|
}
|
||||||
mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource);
|
mUiEventLogger.log(CLIPBOARD_OVERLAY_ENTERED, 0, clipSource);
|
||||||
} else {
|
} else {
|
||||||
mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
|
mUiEventLogger.log(CLIPBOARD_OVERLAY_UPDATED, 0, clipSource);
|
||||||
}
|
}
|
||||||
mClipboardOverlayController.setClipData(clipData, clipSource);
|
mClipboardOverlay.setClipData(clipData, clipSource);
|
||||||
mClipboardOverlayController.setOnSessionCompleteListener(() -> {
|
mClipboardOverlay.setOnSessionCompleteListener(() -> {
|
||||||
// Session is complete, free memory until it's needed again.
|
// 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() {
|
private static boolean isEmulator() {
|
||||||
return SystemProperties.getBoolean("ro.boot.qemu", false);
|
return SystemProperties.getBoolean("ro.boot.qemu", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ClipboardOverlay {
|
||||||
|
void setClipData(ClipData clipData, String clipSource);
|
||||||
|
|
||||||
|
void setOnSessionCompleteListener(Runnable runnable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
package com.android.systemui.clipboardoverlay;
|
package com.android.systemui.clipboardoverlay;
|
||||||
|
|
||||||
import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
|
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.Display.DEFAULT_DISPLAY;
|
||||||
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
|
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.Animator;
|
||||||
import android.animation.AnimatorListenerAdapter;
|
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.app.RemoteAction;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
@@ -52,14 +46,7 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Bitmap;
|
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.display.DisplayManager;
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@@ -67,57 +54,37 @@ import android.os.AsyncTask;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.provider.DeviceConfig;
|
import android.provider.DeviceConfig;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.MathUtils;
|
|
||||||
import android.util.Size;
|
import android.util.Size;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.DisplayCutout;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.InputEvent;
|
import android.view.InputEvent;
|
||||||
import android.view.InputEventReceiver;
|
import android.view.InputEventReceiver;
|
||||||
import android.view.InputMonitor;
|
import android.view.InputMonitor;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MotionEvent;
|
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.TextClassification;
|
||||||
import android.view.textclassifier.TextClassificationManager;
|
import android.view.textclassifier.TextClassificationManager;
|
||||||
import android.view.textclassifier.TextClassifier;
|
import android.view.textclassifier.TextClassifier;
|
||||||
import android.view.textclassifier.TextLinks;
|
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.annotation.NonNull;
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
|
||||||
|
|
||||||
import com.android.internal.logging.UiEventLogger;
|
import com.android.internal.logging.UiEventLogger;
|
||||||
import com.android.internal.policy.PhoneWindow;
|
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||||
import com.android.systemui.broadcast.BroadcastSender;
|
import com.android.systemui.broadcast.BroadcastSender;
|
||||||
import com.android.systemui.screenshot.DraggableConstraintLayout;
|
import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext;
|
||||||
import com.android.systemui.screenshot.FloatingWindowUtil;
|
|
||||||
import com.android.systemui.screenshot.OverlayActionChip;
|
|
||||||
import com.android.systemui.screenshot.TimeoutHandler;
|
import com.android.systemui.screenshot.TimeoutHandler;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
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
|
* 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";
|
private static final String TAG = "ClipboardOverlayCtrlr";
|
||||||
|
|
||||||
/** Constants for screenshot/copy deconflicting */
|
/** Constants for screenshot/copy deconflicting */
|
||||||
@@ -126,36 +93,22 @@ public class ClipboardOverlayController {
|
|||||||
public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY";
|
public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY";
|
||||||
|
|
||||||
private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000;
|
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 Context mContext;
|
||||||
private final ClipboardLogger mClipboardLogger;
|
private final ClipboardLogger mClipboardLogger;
|
||||||
private final BroadcastDispatcher mBroadcastDispatcher;
|
private final BroadcastDispatcher mBroadcastDispatcher;
|
||||||
private final DisplayManager mDisplayManager;
|
private final DisplayManager mDisplayManager;
|
||||||
private final DisplayMetrics mDisplayMetrics;
|
private final ClipboardOverlayWindow mWindow;
|
||||||
private final WindowManager mWindowManager;
|
|
||||||
private final WindowManager.LayoutParams mWindowLayoutParams;
|
|
||||||
private final PhoneWindow mWindow;
|
|
||||||
private final TimeoutHandler mTimeoutHandler;
|
private final TimeoutHandler mTimeoutHandler;
|
||||||
private final AccessibilityManager mAccessibilityManager;
|
|
||||||
private final TextClassifier mTextClassifier;
|
private final TextClassifier mTextClassifier;
|
||||||
|
|
||||||
private final DraggableConstraintLayout mView;
|
private final ClipboardOverlayView 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 Runnable mOnSessionCompleteListener;
|
||||||
|
private Runnable mOnRemoteCopyTapped;
|
||||||
|
private Runnable mOnShareTapped;
|
||||||
|
private Runnable mOnEditTapped;
|
||||||
|
private Runnable mOnPreviewTapped;
|
||||||
|
|
||||||
private InputMonitor mInputMonitor;
|
private InputMonitor mInputMonitor;
|
||||||
private InputEventReceiver mInputEventReceiver;
|
private InputEventReceiver mInputEventReceiver;
|
||||||
@@ -163,14 +116,66 @@ public class ClipboardOverlayController {
|
|||||||
private BroadcastReceiver mCloseDialogsReceiver;
|
private BroadcastReceiver mCloseDialogsReceiver;
|
||||||
private BroadcastReceiver mScreenshotReceiver;
|
private BroadcastReceiver mScreenshotReceiver;
|
||||||
|
|
||||||
private boolean mBlockAttach = false;
|
|
||||||
private Animator mExitAnimator;
|
private Animator mExitAnimator;
|
||||||
private Animator mEnterAnimator;
|
private Animator mEnterAnimator;
|
||||||
private final int mOrientation;
|
|
||||||
private boolean mKeyboardVisible;
|
|
||||||
|
|
||||||
|
private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks =
|
||||||
|
new ClipboardOverlayView.ClipboardOverlayCallbacks() {
|
||||||
|
@Override
|
||||||
|
public void onInteraction() {
|
||||||
|
mTimeoutHandler.resetTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
public ClipboardOverlayController(Context context,
|
@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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ClipboardOverlayController(@OverlayWindowContext Context context,
|
||||||
|
ClipboardOverlayView clipboardOverlayView,
|
||||||
|
ClipboardOverlayWindow clipboardOverlayWindow,
|
||||||
BroadcastDispatcher broadcastDispatcher,
|
BroadcastDispatcher broadcastDispatcher,
|
||||||
BroadcastSender broadcastSender,
|
BroadcastSender broadcastSender,
|
||||||
TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) {
|
TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) {
|
||||||
@@ -181,121 +186,26 @@ public class ClipboardOverlayController {
|
|||||||
|
|
||||||
mClipboardLogger = new ClipboardLogger(uiEventLogger);
|
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))
|
mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class))
|
||||||
.getTextClassifier();
|
.getTextClassifier();
|
||||||
|
|
||||||
mWindowManager = mContext.getSystemService(WindowManager.class);
|
|
||||||
|
|
||||||
mDisplayMetrics = new DisplayMetrics();
|
|
||||||
mContext.getDisplay().getRealMetrics(mDisplayMetrics);
|
|
||||||
|
|
||||||
mTimeoutHandler = timeoutHandler;
|
mTimeoutHandler = timeoutHandler;
|
||||||
mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
|
mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
|
||||||
|
|
||||||
// Setup the window that we are going to use
|
mView.setCallbacks(mClipboardCallbacks);
|
||||||
mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
|
|
||||||
mWindowLayoutParams.setTitle("ClipboardOverlay");
|
|
||||||
|
|
||||||
mWindow = FloatingWindowUtil.getFloatingWindow(mContext);
|
|
||||||
mWindow.setWindowManager(mWindowManager, null, null);
|
|
||||||
|
|
||||||
setWindowFocusable(false);
|
mWindow.withWindowAttached(() -> {
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
mWindow.setContentView(mView);
|
||||||
WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
|
mView.setInsets(mWindow.getWindowInsets(),
|
||||||
mKeyboardVisible = insets.isVisible(WindowInsets.Type.ime());
|
mContext.getResources().getConfiguration().orientation);
|
||||||
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(() -> {
|
mTimeoutHandler.setOnTimeoutRunnable(() -> {
|
||||||
@@ -336,21 +246,19 @@ public class ClipboardOverlayController {
|
|||||||
broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
|
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()) {
|
if (mExitAnimator != null && mExitAnimator.isRunning()) {
|
||||||
mExitAnimator.cancel();
|
mExitAnimator.cancel();
|
||||||
}
|
}
|
||||||
reset();
|
reset();
|
||||||
String accessibilityAnnouncement;
|
String accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
|
||||||
|
|
||||||
boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
|
boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
|
||||||
&& clipData.getDescription().getExtras()
|
&& clipData.getDescription().getExtras()
|
||||||
.getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
|
.getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
|
||||||
if (clipData == null || clipData.getItemCount() == 0) {
|
if (clipData == null || clipData.getItemCount() == 0) {
|
||||||
showTextPreview(
|
mView.showDefaultTextPreview();
|
||||||
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())) {
|
} else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
|
||||||
ClipData.Item item = clipData.getItemAt(0);
|
ClipData.Item item = clipData.getItemAt(0);
|
||||||
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
|
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
|
||||||
@@ -360,53 +268,47 @@ public class ClipboardOverlayController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isSensitive) {
|
if (isSensitive) {
|
||||||
showEditableText(
|
showEditableText(mContext.getString(R.string.clipboard_asterisks), true);
|
||||||
mContext.getResources().getString(R.string.clipboard_asterisks), true);
|
|
||||||
} else {
|
} else {
|
||||||
showEditableText(item.getText(), false);
|
showEditableText(item.getText(), false);
|
||||||
}
|
}
|
||||||
showShareChip(clipData);
|
mOnShareTapped = () -> shareContent(clipData);
|
||||||
|
mView.showShareChip();
|
||||||
accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied);
|
accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied);
|
||||||
} else if (clipData.getItemAt(0).getUri() != null) {
|
} else if (clipData.getItemAt(0).getUri() != null) {
|
||||||
if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) {
|
if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) {
|
||||||
showShareChip(clipData);
|
mOnShareTapped = () -> shareContent(clipData);
|
||||||
|
mView.showShareChip();
|
||||||
accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied);
|
accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied);
|
||||||
} else {
|
|
||||||
accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showTextPreview(
|
mView.showDefaultTextPreview();
|
||||||
mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
|
|
||||||
mTextPreview);
|
|
||||||
accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
|
|
||||||
}
|
}
|
||||||
|
maybeShowRemoteCopy(clipData);
|
||||||
|
animateIn();
|
||||||
|
mView.announceForAccessibility(accessibilityAnnouncement);
|
||||||
|
mTimeoutHandler.resetTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeShowRemoteCopy(ClipData clipData) {
|
||||||
Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext);
|
Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext);
|
||||||
// Only show remote copy if it's available.
|
// Only show remote copy if it's available.
|
||||||
PackageManager packageManager = mContext.getPackageManager();
|
PackageManager packageManager = mContext.getPackageManager();
|
||||||
if (packageManager.resolveActivity(
|
if (packageManager.resolveActivity(
|
||||||
remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) {
|
remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) {
|
||||||
mRemoteCopyChip.setContentDescription(
|
mView.setRemoteCopyVisibility(true);
|
||||||
mContext.getString(R.string.clipboard_send_nearby_description));
|
mOnRemoteCopyTapped = () -> {
|
||||||
mRemoteCopyChip.setVisibility(View.VISIBLE);
|
|
||||||
mRemoteCopyChip.setOnClickListener((v) -> {
|
|
||||||
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED);
|
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED);
|
||||||
mContext.startActivity(remoteCopyIntent);
|
mContext.startActivity(remoteCopyIntent);
|
||||||
animateOut();
|
animateOut();
|
||||||
});
|
};
|
||||||
mActionContainerBackground.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
} 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;
|
mOnSessionCompleteListener = runnable;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,72 +320,29 @@ public class ClipboardOverlayController {
|
|||||||
actions.addAll(classification.getActions());
|
actions.addAll(classification.getActions());
|
||||||
}
|
}
|
||||||
mView.post(() -> {
|
mView.post(() -> {
|
||||||
resetActionChips();
|
Optional<RemoteAction> action = actions.stream().filter(remoteAction -> {
|
||||||
if (actions.size() > 0) {
|
ComponentName component = remoteAction.getActionIntent().getIntent().getComponent();
|
||||||
mActionContainerBackground.setVisibility(View.VISIBLE);
|
return component != null && !TextUtils.equals(source, component.getPackageName());
|
||||||
for (RemoteAction action : actions) {
|
}).findFirst();
|
||||||
Intent targetIntent = action.getActionIntent().getIntent();
|
mView.resetActionChips();
|
||||||
ComponentName component = targetIntent.getComponent();
|
action.ifPresent(remoteAction -> mView.setActionChip(remoteAction, () -> {
|
||||||
if (component != null && !TextUtils.equals(source,
|
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
|
||||||
component.getPackageName())) {
|
animateOut();
|
||||||
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() {
|
private void monitorOutsideTouches() {
|
||||||
InputManager inputManager = mContext.getSystemService(InputManager.class);
|
InputManager inputManager = mContext.getSystemService(InputManager.class);
|
||||||
mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
|
mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
|
||||||
mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
|
mInputEventReceiver = new InputEventReceiver(
|
||||||
Looper.getMainLooper()) {
|
mInputMonitor.getInputChannel(), Looper.getMainLooper()) {
|
||||||
@Override
|
@Override
|
||||||
public void onInputEvent(InputEvent event) {
|
public void onInputEvent(InputEvent event) {
|
||||||
if (event instanceof MotionEvent) {
|
if (event instanceof MotionEvent) {
|
||||||
MotionEvent motionEvent = (MotionEvent) event;
|
MotionEvent motionEvent = (MotionEvent) event;
|
||||||
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||||
Region touchRegion = new Region();
|
if (!mView.isInTouchRegion(
|
||||||
|
|
||||||
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())) {
|
(int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
|
||||||
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
|
mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
|
||||||
animateOut();
|
animateOut();
|
||||||
@@ -513,95 +372,27 @@ public class ClipboardOverlayController {
|
|||||||
animateOut();
|
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) {
|
private void showEditableText(CharSequence text, boolean hidden) {
|
||||||
TextView textView = hidden ? mHiddenPreview : mTextPreview;
|
mView.showTextPreview(text, hidden);
|
||||||
showTextPreview(text, textView);
|
mView.setEditAccessibilityAction(true);
|
||||||
View.OnClickListener listener = v -> editText();
|
mOnPreviewTapped = this::editText;
|
||||||
setAccessibilityActionToEdit(textView);
|
|
||||||
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
|
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
|
||||||
CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
|
CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
|
||||||
mEditChip.setVisibility(View.VISIBLE);
|
mOnEditTapped = this::editText;
|
||||||
mActionContainerBackground.setVisibility(View.VISIBLE);
|
mView.showEditChip(mContext.getString(R.string.clipboard_edit_text_description));
|
||||||
mEditChip.setContentDescription(
|
|
||||||
mContext.getString(R.string.clipboard_edit_text_description));
|
|
||||||
mEditChip.setOnClickListener(listener);
|
|
||||||
}
|
}
|
||||||
textView.setOnClickListener(listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean tryShowEditableImage(Uri uri, boolean isSensitive) {
|
private boolean tryShowEditableImage(Uri uri, boolean isSensitive) {
|
||||||
View.OnClickListener listener = v -> editImage(uri);
|
Runnable listener = () -> editImage(uri);
|
||||||
ContentResolver resolver = mContext.getContentResolver();
|
ContentResolver resolver = mContext.getContentResolver();
|
||||||
String mimeType = resolver.getType(uri);
|
String mimeType = resolver.getType(uri);
|
||||||
boolean isEditableImage = mimeType != null && mimeType.startsWith("image");
|
boolean isEditableImage = mimeType != null && mimeType.startsWith("image");
|
||||||
if (isSensitive) {
|
if (isSensitive) {
|
||||||
mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
|
mView.showImagePreview(null);
|
||||||
showSinglePreview(mHiddenPreview);
|
|
||||||
if (isEditableImage) {
|
if (isEditableImage) {
|
||||||
mHiddenPreview.setOnClickListener(listener);
|
mOnPreviewTapped = listener;
|
||||||
setAccessibilityActionToEdit(mHiddenPreview);
|
mView.setEditAccessibilityAction(true);
|
||||||
}
|
}
|
||||||
} else if (isEditableImage) { // if the MIMEtype is image, try to load
|
} else if (isEditableImage) { // if the MIMEtype is image, try to load
|
||||||
try {
|
try {
|
||||||
@@ -609,44 +400,36 @@ public class ClipboardOverlayController {
|
|||||||
// The width of the view is capped, height maintains aspect ratio, so allow it to be
|
// The width of the view is capped, height maintains aspect ratio, so allow it to be
|
||||||
// taller if needed.
|
// taller if needed.
|
||||||
Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
|
Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
|
||||||
showSinglePreview(mImagePreview);
|
mView.showImagePreview(thumbnail);
|
||||||
mImagePreview.setImageBitmap(thumbnail);
|
mView.setEditAccessibilityAction(true);
|
||||||
mImagePreview.setOnClickListener(listener);
|
mOnPreviewTapped = listener;
|
||||||
setAccessibilityActionToEdit(mImagePreview);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Thumbnail loading failed", e);
|
Log.e(TAG, "Thumbnail loading failed", e);
|
||||||
showTextPreview(
|
mView.showDefaultTextPreview();
|
||||||
mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
|
|
||||||
mTextPreview);
|
|
||||||
isEditableImage = false;
|
isEditableImage = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showTextPreview(
|
mView.showDefaultTextPreview();
|
||||||
mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
|
|
||||||
mTextPreview);
|
|
||||||
}
|
}
|
||||||
if (isEditableImage && DeviceConfig.getBoolean(
|
if (isEditableImage && DeviceConfig.getBoolean(
|
||||||
DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
|
DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_EDIT_BUTTON, false)) {
|
||||||
mEditChip.setVisibility(View.VISIBLE);
|
mView.showEditChip(mContext.getString(R.string.clipboard_edit_image_description));
|
||||||
mActionContainerBackground.setVisibility(View.VISIBLE);
|
|
||||||
mEditChip.setOnClickListener(listener);
|
|
||||||
mEditChip.setContentDescription(
|
|
||||||
mContext.getString(R.string.clipboard_edit_image_description));
|
|
||||||
}
|
}
|
||||||
return isEditableImage;
|
return isEditableImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setAccessibilityActionToEdit(View view) {
|
|
||||||
ViewCompat.replaceAccessibilityAction(view,
|
|
||||||
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
|
|
||||||
mContext.getString(R.string.clipboard_edit), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateIn() {
|
private void animateIn() {
|
||||||
if (mAccessibilityManager.isEnabled()) {
|
if (mEnterAnimator != null && mEnterAnimator.isRunning()) {
|
||||||
mDismissButton.setVisibility(View.VISIBLE);
|
return;
|
||||||
}
|
}
|
||||||
mEnterAnimator = getEnterAnimation();
|
mEnterAnimator = mView.getEnterAnimation();
|
||||||
|
mEnterAnimator.addListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
super.onAnimationEnd(animation);
|
||||||
|
mTimeoutHandler.resetTimeout();
|
||||||
|
}
|
||||||
|
});
|
||||||
mEnterAnimator.start();
|
mEnterAnimator.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,7 +437,7 @@ public class ClipboardOverlayController {
|
|||||||
if (mExitAnimator != null && mExitAnimator.isRunning()) {
|
if (mExitAnimator != null && mExitAnimator.isRunning()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Animator anim = getExitAnimation();
|
Animator anim = mView.getExitAnimation();
|
||||||
anim.addListener(new AnimatorListenerAdapter() {
|
anim.addListener(new AnimatorListenerAdapter() {
|
||||||
private boolean mCancelled;
|
private boolean mCancelled;
|
||||||
|
|
||||||
@@ -676,122 +459,11 @@ public class ClipboardOverlayController {
|
|||||||
anim.start();
|
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() {
|
private void hideImmediate() {
|
||||||
// Note this may be called multiple times if multiple dismissal events happen at the same
|
// Note this may be called multiple times if multiple dismissal events happen at the same
|
||||||
// time.
|
// time.
|
||||||
mTimeoutHandler.cancelTimeout();
|
mTimeoutHandler.cancelTimeout();
|
||||||
final View decorView = mWindow.peekDecorView();
|
mWindow.remove();
|
||||||
if (decorView != null && decorView.isAttachedToWindow()) {
|
|
||||||
mWindowManager.removeViewImmediate(decorView);
|
|
||||||
}
|
|
||||||
if (mCloseDialogsReceiver != null) {
|
if (mCloseDialogsReceiver != null) {
|
||||||
mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver);
|
mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver);
|
||||||
mCloseDialogsReceiver = null;
|
mCloseDialogsReceiver = null;
|
||||||
@@ -813,129 +485,20 @@ public class ClipboardOverlayController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetActionChips() {
|
|
||||||
for (OverlayActionChip chip : mActionChips) {
|
|
||||||
mActionContainer.removeView(chip);
|
|
||||||
}
|
|
||||||
mActionChips.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reset() {
|
private void reset() {
|
||||||
mView.setTranslationX(0);
|
mOnRemoteCopyTapped = null;
|
||||||
mView.setAlpha(0);
|
mOnShareTapped = null;
|
||||||
mActionContainerBackground.setVisibility(View.GONE);
|
mOnEditTapped = null;
|
||||||
mShareChip.setVisibility(View.GONE);
|
mOnPreviewTapped = null;
|
||||||
mEditChip.setVisibility(View.GONE);
|
mView.reset();
|
||||||
mRemoteCopyChip.setVisibility(View.GONE);
|
|
||||||
resetActionChips();
|
|
||||||
mTimeoutHandler.cancelTimeout();
|
mTimeoutHandler.cancelTimeout();
|
||||||
mClipboardLogger.reset();
|
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() {
|
private Display getDefaultDisplay() {
|
||||||
return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
|
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 {
|
static class ClipboardLogger {
|
||||||
private final UiEventLogger mUiEventLogger;
|
private final UiEventLogger mUiEventLogger;
|
||||||
private boolean mGuarded = false;
|
private boolean mGuarded = false;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,17 +27,17 @@ import com.android.systemui.screenshot.TimeoutHandler;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A factory that churns out ClipboardOverlayControllers on demand.
|
* A factory that churns out ClipboardOverlayControllerLegacys on demand.
|
||||||
*/
|
*/
|
||||||
@SysUISingleton
|
@SysUISingleton
|
||||||
public class ClipboardOverlayControllerFactory {
|
public class ClipboardOverlayControllerLegacyFactory {
|
||||||
|
|
||||||
private final UiEventLogger mUiEventLogger;
|
private final UiEventLogger mUiEventLogger;
|
||||||
private final BroadcastDispatcher mBroadcastDispatcher;
|
private final BroadcastDispatcher mBroadcastDispatcher;
|
||||||
private final BroadcastSender mBroadcastSender;
|
private final BroadcastSender mBroadcastSender;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ClipboardOverlayControllerFactory(BroadcastDispatcher broadcastDispatcher,
|
public ClipboardOverlayControllerLegacyFactory(BroadcastDispatcher broadcastDispatcher,
|
||||||
BroadcastSender broadcastSender, UiEventLogger uiEventLogger) {
|
BroadcastSender broadcastSender, UiEventLogger uiEventLogger) {
|
||||||
this.mBroadcastDispatcher = broadcastDispatcher;
|
this.mBroadcastDispatcher = broadcastDispatcher;
|
||||||
this.mBroadcastSender = broadcastSender;
|
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) {
|
public ClipboardOverlayControllerLegacy create(Context context) {
|
||||||
return new ClipboardOverlayController(context, mBroadcastDispatcher, mBroadcastSender,
|
return new ClipboardOverlayControllerLegacy(context, mBroadcastDispatcher, mBroadcastSender,
|
||||||
new TimeoutHandler(context), mUiEventLogger);
|
new TimeoutHandler(context), mUiEventLogger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import com.android.systemui.biometrics.AlternateUdfpsTouchProvider;
|
|||||||
import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
|
import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
|
||||||
import com.android.systemui.biometrics.dagger.BiometricsModule;
|
import com.android.systemui.biometrics.dagger.BiometricsModule;
|
||||||
import com.android.systemui.classifier.FalsingModule;
|
import com.android.systemui.classifier.FalsingModule;
|
||||||
|
import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
|
||||||
import com.android.systemui.controls.dagger.ControlsModule;
|
import com.android.systemui.controls.dagger.ControlsModule;
|
||||||
import com.android.systemui.dagger.qualifiers.Main;
|
import com.android.systemui.dagger.qualifiers.Main;
|
||||||
import com.android.systemui.demomode.dagger.DemoModeModule;
|
import com.android.systemui.demomode.dagger.DemoModeModule;
|
||||||
@@ -118,6 +119,7 @@ import dagger.Provides;
|
|||||||
AssistModule.class,
|
AssistModule.class,
|
||||||
BiometricsModule.class,
|
BiometricsModule.class,
|
||||||
BouncerViewModule.class,
|
BouncerViewModule.class,
|
||||||
|
ClipboardOverlayModule.class,
|
||||||
ClockModule.class,
|
ClockModule.class,
|
||||||
CoroutinesModule.class,
|
CoroutinesModule.class,
|
||||||
DreamModule.class,
|
DreamModule.class,
|
||||||
@@ -165,12 +167,16 @@ public abstract class SystemUIModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract BootCompleteCache bindBootCompleteCache(BootCompleteCacheImpl bootCompleteCache);
|
abstract BootCompleteCache bindBootCompleteCache(BootCompleteCacheImpl bootCompleteCache);
|
||||||
|
|
||||||
/** */
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
@Binds
|
@Binds
|
||||||
public abstract ContextComponentHelper bindComponentHelper(
|
public abstract ContextComponentHelper bindComponentHelper(
|
||||||
ContextComponentResolver componentHelper);
|
ContextComponentResolver componentHelper);
|
||||||
|
|
||||||
/** */
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
@Binds
|
@Binds
|
||||||
public abstract NotificationRowBinder bindNotificationRowBinder(
|
public abstract NotificationRowBinder bindNotificationRowBinder(
|
||||||
NotificationRowBinderImpl notificationRowBinder);
|
NotificationRowBinderImpl notificationRowBinder);
|
||||||
@@ -209,6 +215,7 @@ public abstract class SystemUIModule {
|
|||||||
abstract SystemClock bindSystemClock(SystemClockImpl systemClock);
|
abstract SystemClock bindSystemClock(SystemClockImpl systemClock);
|
||||||
|
|
||||||
// TODO: This should provided by the WM component
|
// TODO: This should provided by the WM component
|
||||||
|
|
||||||
/** Provides Optional of BubbleManager */
|
/** Provides Optional of BubbleManager */
|
||||||
@SysUISingleton
|
@SysUISingleton
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -309,6 +309,9 @@ public class Flags {
|
|||||||
public static final UnreleasedFlag A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
|
public static final UnreleasedFlag A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
|
||||||
new UnreleasedFlag(1600);
|
new UnreleasedFlag(1600);
|
||||||
|
|
||||||
|
// 1700 - clipboard
|
||||||
|
public static final UnreleasedFlag CLIPBOARD_OVERLAY_REFACTOR = new UnreleasedFlag(1700);
|
||||||
|
|
||||||
// Pay no attention to the reflection behind the curtain.
|
// Pay no attention to the reflection behind the curtain.
|
||||||
// ========================== Curtain ==========================
|
// ========================== Curtain ==========================
|
||||||
// | |
|
// | |
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import androidx.test.runner.AndroidJUnit4;
|
|||||||
|
|
||||||
import com.android.internal.logging.UiEventLogger;
|
import com.android.internal.logging.UiEventLogger;
|
||||||
import com.android.systemui.SysuiTestCase;
|
import com.android.systemui.SysuiTestCase;
|
||||||
|
import com.android.systemui.flags.FeatureFlags;
|
||||||
|
import com.android.systemui.flags.Flags;
|
||||||
import com.android.systemui.util.DeviceConfigProxyFake;
|
import com.android.systemui.util.DeviceConfigProxyFake;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@@ -47,6 +49,9 @@ import org.mockito.ArgumentCaptor;
|
|||||||
import org.mockito.Captor;
|
import org.mockito.Captor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.mockito.Spy;
|
||||||
|
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
@SmallTest
|
@SmallTest
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
@@ -55,11 +60,15 @@ public class ClipboardListenerTest extends SysuiTestCase {
|
|||||||
@Mock
|
@Mock
|
||||||
private ClipboardManager mClipboardManager;
|
private ClipboardManager mClipboardManager;
|
||||||
@Mock
|
@Mock
|
||||||
private ClipboardOverlayControllerFactory mClipboardOverlayControllerFactory;
|
private ClipboardOverlayControllerLegacyFactory mClipboardOverlayControllerLegacyFactory;
|
||||||
|
@Mock
|
||||||
|
private ClipboardOverlayControllerLegacy mOverlayControllerLegacy;
|
||||||
@Mock
|
@Mock
|
||||||
private ClipboardOverlayController mOverlayController;
|
private ClipboardOverlayController mOverlayController;
|
||||||
@Mock
|
@Mock
|
||||||
private UiEventLogger mUiEventLogger;
|
private UiEventLogger mUiEventLogger;
|
||||||
|
@Mock
|
||||||
|
private FeatureFlags mFeatureFlags;
|
||||||
private DeviceConfigProxyFake mDeviceConfigProxy;
|
private DeviceConfigProxyFake mDeviceConfigProxy;
|
||||||
|
|
||||||
private ClipData mSampleClipData;
|
private ClipData mSampleClipData;
|
||||||
@@ -72,12 +81,17 @@ public class ClipboardListenerTest extends SysuiTestCase {
|
|||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<String> mStringCaptor;
|
private ArgumentCaptor<String> mStringCaptor;
|
||||||
|
|
||||||
|
@Spy
|
||||||
|
private Provider<ClipboardOverlayController> mOverlayControllerProvider;
|
||||||
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
|
mOverlayControllerProvider = () -> mOverlayController;
|
||||||
|
|
||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
when(mClipboardOverlayControllerFactory.create(any())).thenReturn(
|
when(mClipboardOverlayControllerLegacyFactory.create(any()))
|
||||||
mOverlayController);
|
.thenReturn(mOverlayControllerLegacy);
|
||||||
when(mClipboardManager.hasPrimaryClip()).thenReturn(true);
|
when(mClipboardManager.hasPrimaryClip()).thenReturn(true);
|
||||||
|
|
||||||
|
|
||||||
@@ -94,7 +108,8 @@ public class ClipboardListenerTest extends SysuiTestCase {
|
|||||||
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
|
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
|
||||||
"false", false);
|
"false", false);
|
||||||
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
|
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
|
||||||
mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger);
|
mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
|
||||||
|
mClipboardManager, mUiEventLogger, mFeatureFlags);
|
||||||
listener.start();
|
listener.start();
|
||||||
verifyZeroInteractions(mClipboardManager);
|
verifyZeroInteractions(mClipboardManager);
|
||||||
verifyZeroInteractions(mUiEventLogger);
|
verifyZeroInteractions(mUiEventLogger);
|
||||||
@@ -105,7 +120,8 @@ public class ClipboardListenerTest extends SysuiTestCase {
|
|||||||
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
|
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
|
||||||
"true", false);
|
"true", false);
|
||||||
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
|
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
|
||||||
mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger);
|
mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
|
||||||
|
mClipboardManager, mUiEventLogger, mFeatureFlags);
|
||||||
listener.start();
|
listener.start();
|
||||||
verify(mClipboardManager).addPrimaryClipChangedListener(any());
|
verify(mClipboardManager).addPrimaryClipChangedListener(any());
|
||||||
verifyZeroInteractions(mUiEventLogger);
|
verifyZeroInteractions(mUiEventLogger);
|
||||||
@@ -113,16 +129,58 @@ public class ClipboardListenerTest extends SysuiTestCase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_consecutiveCopies() {
|
public void test_consecutiveCopies() {
|
||||||
|
when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false);
|
||||||
|
|
||||||
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
|
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
|
||||||
"true", false);
|
"true", false);
|
||||||
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
|
ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
|
||||||
mClipboardOverlayControllerFactory, mClipboardManager, mUiEventLogger);
|
mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
|
||||||
|
mClipboardManager, mUiEventLogger, mFeatureFlags);
|
||||||
listener.start();
|
listener.start();
|
||||||
listener.onPrimaryClipChanged();
|
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(mSampleClipData, mClipDataCaptor.getValue());
|
||||||
assertEquals(mSampleSource, mStringCaptor.getValue());
|
assertEquals(mSampleSource, mStringCaptor.getValue());
|
||||||
@@ -134,14 +192,15 @@ public class ClipboardListenerTest extends SysuiTestCase {
|
|||||||
|
|
||||||
listener.onPrimaryClipChanged();
|
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 calling the runnable here, just change the clip again and verify that the overlay is
|
||||||
// NOT recreated.
|
// NOT recreated.
|
||||||
|
|
||||||
listener.onPrimaryClipChanged();
|
listener.onPrimaryClipChanged();
|
||||||
|
|
||||||
verify(mClipboardOverlayControllerFactory, times(2)).create(any());
|
verify(mOverlayControllerProvider, times(2)).get();
|
||||||
|
verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -169,4 +228,40 @@ public class ClipboardListenerTest extends SysuiTestCase {
|
|||||||
assertTrue(ClipboardListener.shouldSuppressOverlay(suppressableClipData,
|
assertTrue(ClipboardListener.shouldSuppressOverlay(suppressableClipData,
|
||||||
ClipboardListener.SHELL_PACKAGE, false));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user