summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardImageLoader.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java122
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/Flags.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt83
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java68
5 files changed, 319 insertions, 16 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardImageLoader.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardImageLoader.kt
new file mode 100644
index 000000000000..0542e13ba6da
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardImageLoader.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.clipboardoverlay
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.util.Log
+import android.util.Size
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import java.io.IOException
+import java.util.function.Consumer
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
+
+class ClipboardImageLoader
+@Inject
+constructor(
+ private val context: Context,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ @Application private val mainScope: CoroutineScope
+) {
+ private val TAG: String = "ClipboardImageLoader"
+
+ suspend fun load(uri: Uri, timeoutMs: Long = 300) =
+ withTimeoutOrNull(timeoutMs) {
+ withContext(bgDispatcher) {
+ try {
+ val size = context.resources.getDimensionPixelSize(R.dimen.overlay_x_scale)
+ context.contentResolver.loadThumbnail(uri, Size(size, size * 4), null)
+ } catch (e: IOException) {
+ Log.e(TAG, "Thumbnail loading failed!", e)
+ null
+ }
+ }
+ }
+
+ fun loadAsync(uri: Uri, callback: Consumer<Bitmap?>) {
+ mainScope.launch { callback.accept(load(uri)) }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index 0aeab10101f6..757ebf45e9ad 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -32,6 +32,7 @@ import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBO
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
+import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -90,6 +91,7 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
private final ClipboardOverlayUtils mClipboardUtils;
private final FeatureFlags mFeatureFlags;
private final Executor mBgExecutor;
+ private final ClipboardImageLoader mClipboardImageLoader;
private final ClipboardOverlayView mView;
@@ -109,6 +111,7 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
private Runnable mOnUiUpdate;
+ private boolean mShowingUi;
private boolean mIsMinimized;
private ClipboardModel mClipboardModel;
@@ -175,9 +178,11 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
FeatureFlags featureFlags,
ClipboardOverlayUtils clipboardUtils,
@Background Executor bgExecutor,
+ ClipboardImageLoader clipboardImageLoader,
UiEventLogger uiEventLogger) {
mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
+ mClipboardImageLoader = clipboardImageLoader;
mClipboardLogger = new ClipboardLogger(uiEventLogger);
@@ -260,21 +265,42 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
boolean shouldAnimate = !model.dataMatches(mClipboardModel) || wasExiting;
mClipboardModel = model;
mClipboardLogger.setClipSource(mClipboardModel.getSource());
- if (shouldAnimate) {
- reset();
- mClipboardLogger.setClipSource(mClipboardModel.getSource());
- if (shouldShowMinimized(mWindow.getWindowInsets())) {
- mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED);
- mIsMinimized = true;
- mView.setMinimized(true);
- } else {
- mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED);
+ if (mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT)) {
+ if (shouldAnimate) {
+ reset();
+ mClipboardLogger.setClipSource(mClipboardModel.getSource());
+ if (shouldShowMinimized(mWindow.getWindowInsets())) {
+ mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED);
+ mIsMinimized = true;
+ mView.setMinimized(true);
+ } else {
+ mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED);
+ setExpandedView(this::animateIn);
+ }
+ mView.announceForAccessibility(
+ getAccessibilityAnnouncement(mClipboardModel.getType()));
+ } else if (!mIsMinimized) {
+ setExpandedView(() -> {
+ });
+ }
+ } else {
+ if (shouldAnimate) {
+ reset();
+ mClipboardLogger.setClipSource(mClipboardModel.getSource());
+ if (shouldShowMinimized(mWindow.getWindowInsets())) {
+ mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED);
+ mIsMinimized = true;
+ mView.setMinimized(true);
+ } else {
+ mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED);
+ setExpandedView();
+ animateIn();
+ }
+ mView.announceForAccessibility(
+ getAccessibilityAnnouncement(mClipboardModel.getType()));
+ } else if (!mIsMinimized) {
setExpandedView();
}
- animateIn();
- mView.announceForAccessibility(getAccessibilityAnnouncement(mClipboardModel.getType()));
- } else if (!mIsMinimized) {
- setExpandedView();
}
if (mClipboardModel.isRemote()) {
mTimeoutHandler.cancelTimeout();
@@ -285,6 +311,58 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
}
}
+ private void setExpandedView(Runnable onViewReady) {
+ final ClipboardModel model = mClipboardModel;
+ mView.setMinimized(false);
+ switch (model.getType()) {
+ case TEXT:
+ if (model.isRemote() || DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
+ if (model.getTextLinks() != null) {
+ classifyText(model);
+ }
+ }
+ if (model.isSensitive()) {
+ mView.showTextPreview(mContext.getString(R.string.clipboard_asterisks), true);
+ } else {
+ mView.showTextPreview(model.getText().toString(), false);
+ }
+ mView.setEditAccessibilityAction(true);
+ mOnPreviewTapped = this::editText;
+ onViewReady.run();
+ break;
+ case IMAGE:
+ mView.setEditAccessibilityAction(true);
+ mOnPreviewTapped = () -> editImage(model.getUri());
+ if (model.isSensitive()) {
+ mView.showImagePreview(null);
+ onViewReady.run();
+ } else {
+ mClipboardImageLoader.loadAsync(model.getUri(), (bitmap) -> mView.post(() -> {
+ if (bitmap == null) {
+ mView.showDefaultTextPreview();
+ } else {
+ mView.showImagePreview(bitmap);
+ }
+ onViewReady.run();
+ }));
+ }
+ break;
+ case URI:
+ case OTHER:
+ mView.showDefaultTextPreview();
+ onViewReady.run();
+ break;
+ }
+ if (!model.isRemote()) {
+ maybeShowRemoteCopy(model.getClipData());
+ }
+ if (model.getType() != ClipboardModel.Type.OTHER) {
+ mOnShareTapped = () -> shareContent(model.getClipData());
+ mView.showShareChip();
+ }
+ }
+
private void setExpandedView() {
final ClipboardModel model = mClipboardModel;
mView.setMinimized(false);
@@ -350,8 +428,12 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_EXPANDED_FROM_MINIMIZED);
mIsMinimized = false;
}
- setExpandedView();
- animateIn();
+ if (mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT)) {
+ setExpandedView(() -> animateIn());
+ } else {
+ setExpandedView();
+ animateIn();
+ }
}
});
mEnterAnimator.start();
@@ -412,7 +494,8 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
mInputMonitor.getInputChannel(), Looper.getMainLooper()) {
@Override
public void onInputEvent(InputEvent event) {
- if (event instanceof MotionEvent) {
+ if ((!mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT) || mShowingUi)
+ && event instanceof MotionEvent) {
MotionEvent motionEvent = (MotionEvent) event;
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
if (!mView.isInTouchRegion(
@@ -452,6 +535,12 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
mEnterAnimator = mView.getEnterAnimation();
mEnterAnimator.addListener(new AnimatorListenerAdapter() {
@Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ mShowingUi = true;
+ }
+
+ @Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (mOnUiUpdate != null) {
@@ -518,6 +607,7 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
mOnRemoteCopyTapped = null;
mOnShareTapped = null;
mOnPreviewTapped = null;
+ mShowingUi = false;
mView.reset();
mTimeoutHandler.cancelTimeout();
mClipboardLogger.reset();
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 7cc3f79fb365..6ca409f07f6f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -600,6 +600,8 @@ object Flags {
// 1700 - clipboard
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
+ // TODO(b/278714186) Tracking Bug
+ @JvmField val CLIPBOARD_IMAGE_TIMEOUT = unreleasedFlag(1702, "clipboard_image_timeout")
// 1800 - shade container
// TODO(b/265944639): Tracking Bug
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt
new file mode 100644
index 000000000000..21516d4917b5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.clipboardoverlay
+
+import android.content.ContentResolver
+import android.content.Context
+import android.net.Uri
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.whenever
+import java.io.IOException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class ClipboardImageLoaderTest : SysuiTestCase() {
+ @Mock private lateinit var mockContext: Context
+
+ @Mock private lateinit var mockContentResolver: ContentResolver
+
+ private lateinit var clipboardImageLoader: ClipboardImageLoader
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ @Throws(IOException::class)
+ fun test_imageLoadSuccess() = runTest {
+ val testDispatcher = StandardTestDispatcher(this.testScheduler)
+ clipboardImageLoader =
+ ClipboardImageLoader(mockContext, testDispatcher, CoroutineScope(testDispatcher))
+ val testUri = Uri.parse("testUri")
+ whenever(mockContext.contentResolver).thenReturn(mockContentResolver)
+ whenever(mockContext.resources).thenReturn(context.resources)
+
+ clipboardImageLoader.load(testUri)
+
+ verify(mockContentResolver).loadThumbnail(eq(testUri), any(), any())
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ @Throws(IOException::class)
+ fun test_imageLoadFailure() = runTest {
+ val testDispatcher = StandardTestDispatcher(this.testScheduler)
+ clipboardImageLoader =
+ ClipboardImageLoader(mockContext, testDispatcher, CoroutineScope(testDispatcher))
+ val testUri = Uri.parse("testUri")
+ whenever(mockContext.contentResolver).thenReturn(mockContentResolver)
+ whenever(mockContext.resources).thenReturn(context.resources)
+
+ val res = clipboardImageLoader.load(testUri)
+
+ verify(mockContentResolver).loadThumbnail(eq(testUri), any(), any())
+ assertNull(res)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index fe5fa1fdd39f..39fb7b4cda2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -25,6 +25,7 @@ import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBO
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_EXPANDED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_MINIMIZED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
+import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -90,6 +91,8 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
@Mock
private ClipboardOverlayUtils mClipboardUtils;
@Mock
+ private ClipboardImageLoader mClipboardImageLoader;
+ @Mock
private UiEventLogger mUiEventLogger;
private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@@ -120,6 +123,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
mSampleClipData = new ClipData("Test", new String[]{"text/plain"},
new ClipData.Item("Test Item"));
+ mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, true); // turned off for legacy tests
mOverlayController = new ClipboardOverlayController(
mContext,
@@ -131,6 +135,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
mFeatureFlags,
mClipboardUtils,
mExecutor,
+ mClipboardImageLoader,
mUiEventLogger);
verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture());
mCallbacks = mOverlayCallbacksCaptor.getValue();
@@ -142,6 +147,69 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
}
@Test
+ public void test_setClipData_invalidImageData_legacy() {
+ ClipData clipData = new ClipData("", new String[]{"image/png"},
+ new ClipData.Item(Uri.parse("")));
+ mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
+
+ mOverlayController.setClipData(clipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+ verify(mClipboardOverlayView, times(1)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_nonImageUri_legacy() {
+ ClipData clipData = new ClipData("", new String[]{"resource/png"},
+ new ClipData.Item(Uri.parse("")));
+ mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
+
+ mOverlayController.setClipData(clipData, "");
+
+ verify(mClipboardOverlayView, times(1)).showDefaultTextPreview();
+ verify(mClipboardOverlayView, times(1)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_textData_legacy() {
+ mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
+ mOverlayController.setClipData(mSampleClipData, "abc");
+
+ verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false);
+ verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "abc");
+ verify(mClipboardOverlayView, times(1)).showShareChip();
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
+ public void test_setClipData_sensitiveTextData_legacy() {
+ mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
+ 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_legacy() {
+ when(mAnimator.isRunning()).thenReturn(true);
+ mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false);
+
+ mOverlayController.setClipData(mSampleClipData, "");
+ mOverlayController.setClipData(mSampleClipData, "");
+
+ verify(mClipboardOverlayView, times(1)).getEnterAnimation();
+ }
+
+ @Test
public void test_setClipData_invalidImageData() {
ClipData clipData = new ClipData("", new String[]{"image/png"},
new ClipData.Item(Uri.parse("")));