diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java index 82bb7237cab02..edda87527b1de 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java @@ -97,8 +97,9 @@ public class ClipboardListener implements return; } - if (!isUserSetupComplete()) { - // just show a toast, user should not access intents from this state + if (!isUserSetupComplete() // user should not access intents from this state + || clipData == null // shouldn't happen, but just in case + || clipData.getItemCount() == 0) { if (shouldShowToast(clipData)) { mUiEventLogger.log(CLIPBOARD_TOAST_SHOWN, 0, clipSource); mClipboardToast.showCopiedToast(); diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt index c7aaf09d65519..789833c6d8499 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardModel.kt @@ -19,19 +19,23 @@ import android.content.ClipData import android.content.ClipDescription.EXTRA_IS_SENSITIVE import android.content.Context import android.graphics.Bitmap +import android.net.Uri import android.text.TextUtils import android.util.Log import android.util.Size +import android.view.textclassifier.TextLinks import com.android.systemui.R import java.io.IOException data class ClipboardModel( - val clipData: ClipData?, + val clipData: ClipData, val source: String, - val type: Type = Type.OTHER, - val item: ClipData.Item? = null, - val isSensitive: Boolean = false, - val isRemote: Boolean = false, + val type: Type, + val text: CharSequence?, + val textLinks: TextLinks?, + val uri: Uri?, + val isSensitive: Boolean, + val isRemote: Boolean, ) { private var _bitmap: Bitmap? = null @@ -41,17 +45,16 @@ data class ClipboardModel( } return source == other.source && type == other.type && - item?.text == other.item?.text && - item?.uri == other.item?.uri && + text == other.text && + uri == other.uri && isSensitive == other.isSensitive } fun loadThumbnail(context: Context): Bitmap? { - if (_bitmap == null && type == Type.IMAGE && item?.uri != null) { + if (_bitmap == null && type == Type.IMAGE && uri != null) { try { val size = context.resources.getDimensionPixelSize(R.dimen.overlay_x_scale) - _bitmap = - context.contentResolver.loadThumbnail(item.uri, Size(size, size * 4), null) + _bitmap = context.contentResolver.loadThumbnail(uri, Size(size, size * 4), null) } catch (e: IOException) { Log.e(TAG, "Thumbnail loading failed!", e) } @@ -66,27 +69,34 @@ data class ClipboardModel( fun fromClipData( context: Context, utils: ClipboardOverlayUtils, - clipData: ClipData?, + clipData: ClipData, source: String ): ClipboardModel { - if (clipData == null || clipData.itemCount == 0) { - return ClipboardModel(clipData, source) - } val sensitive = clipData.description?.extras?.getBoolean(EXTRA_IS_SENSITIVE) ?: false val item = clipData.getItemAt(0)!! val type = getType(context, item) val remote = utils.isRemoteCopy(context, clipData, source) - return ClipboardModel(clipData, source, type, item, sensitive, remote) + return ClipboardModel( + clipData, + source, + type, + item.text, + item.textLinks, + item.uri, + sensitive, + remote + ) } private fun getType(context: Context, item: ClipData.Item): Type { return if (!TextUtils.isEmpty(item.text)) { Type.TEXT - } else if ( - item.uri != null && - context.contentResolver.getType(item.uri)?.startsWith("image") == true - ) { - Type.IMAGE + } else if (item.uri != null) { + if (context.contentResolver.getType(item.uri)?.startsWith("image") == true) { + Type.IMAGE + } else { + Type.URI + } } else { Type.OTHER } @@ -96,6 +106,7 @@ data class ClipboardModel( enum class Type { TEXT, IMAGE, + URI, OTHER } } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index b41f30844e274..870378a416ca8 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -309,14 +309,14 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv if ((mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR) && model.isRemote()) || DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) { - if (model.getItem().getTextLinks() != null) { + if (model.getTextLinks() != null) { classifyText(model); } } if (model.isSensitive()) { mView.showTextPreview(mContext.getString(R.string.clipboard_asterisks), true); } else { - mView.showTextPreview(model.getItem().getText(), false); + mView.showTextPreview(model.getText(), false); } mView.setEditAccessibilityAction(true); mOnPreviewTapped = this::editText; @@ -326,12 +326,13 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv mView.showImagePreview( model.isSensitive() ? null : model.loadThumbnail(mContext)); mView.setEditAccessibilityAction(true); - mOnPreviewTapped = () -> editImage(model.getItem().getUri()); + mOnPreviewTapped = () -> editImage(model.getUri()); } else { // image loading failed mView.showDefaultTextPreview(); } break; + case URI: case OTHER: mView.showDefaultTextPreview(); break; @@ -371,8 +372,8 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv private void classifyText(ClipboardModel model) { mBgExecutor.execute(() -> { - Optional remoteAction = - mClipboardUtils.getAction(model.getItem(), model.getSource()); + Optional remoteAction = mClipboardUtils.getAction( + model.getText(), model.getTextLinks(), model.getSource()); if (model.equals(mClipboardModel)) { remoteAction.ifPresent(action -> { mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN); @@ -419,10 +420,10 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied); } else if (clipData.getItemAt(0).getUri() != null) { if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) { - mOnShareTapped = () -> shareContent(clipData); - mView.showShareChip(); accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied); } + mOnShareTapped = () -> shareContent(clipData); + mView.showShareChip(); } else { mView.showDefaultTextPreview(); } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java index 785e4a0743e40..a85f8b9357f51 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java @@ -65,6 +65,23 @@ class ClipboardOverlayUtils { return false; } + public Optional getAction(CharSequence text, TextLinks textLinks, String source) { + return getActions(text, textLinks).stream().filter(remoteAction -> { + ComponentName component = remoteAction.getActionIntent().getIntent().getComponent(); + return component != null && !TextUtils.equals(source, component.getPackageName()); + }).findFirst(); + } + + private ArrayList getActions(CharSequence text, TextLinks textLinks) { + ArrayList actions = new ArrayList<>(); + for (TextLinks.TextLink link : textLinks.getLinks()) { + TextClassification classification = mTextClassifier.classifyText( + text, link.getStart(), link.getEnd(), null); + actions.addAll(classification.getActions()); + } + return actions; + } + public Optional getAction(ClipData.Item item, String source) { return getActions(item).stream().filter(remoteAction -> { ComponentName component = remoteAction.getActionIntent().getIntent().getComponent(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java index 7177919909f9a..fd6e31ba3beed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java @@ -51,6 +51,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import java.util.ArrayList; + import javax.inject.Provider; @SmallTest @@ -194,6 +196,33 @@ public class ClipboardListenerTest extends SysuiTestCase { verifyZeroInteractions(mOverlayControllerProvider); } + @Test + public void test_nullClipData_showsNothing() { + when(mClipboardManager.getPrimaryClip()).thenReturn(null); + + mClipboardListener.start(); + mClipboardListener.onPrimaryClipChanged(); + + verifyZeroInteractions(mUiEventLogger); + verifyZeroInteractions(mClipboardToast); + verifyZeroInteractions(mOverlayControllerProvider); + } + + @Test + public void test_emptyClipData_showsToast() { + ClipDescription description = new ClipDescription("Test", new String[0]); + ClipData noItems = new ClipData(description, new ArrayList<>()); + when(mClipboardManager.getPrimaryClip()).thenReturn(noItems); + + mClipboardListener.start(); + mClipboardListener.onPrimaryClipChanged(); + + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN, 0, mSampleSource); + verify(mClipboardToast, times(1)).showCopiedToast(); + verifyZeroInteractions(mOverlayControllerProvider); + } + @Test public void test_minimizedLayoutFlagOff_usesLegacy() { mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt index faef35e7bfcbb..c0dada4725b88 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt @@ -52,18 +52,6 @@ class ClipboardModelTest : SysuiTestCase() { mSampleClipData = ClipData("Test", arrayOf("text/plain"), ClipData.Item("Test Item")) } - @Test - fun test_nullClipData() { - val model = ClipboardModel.fromClipData(mContext, mClipboardUtils, null, "test source") - assertNull(model.clipData) - assertEquals("test source", model.source) - assertEquals(ClipboardModel.Type.OTHER, model.type) - assertNull(model.item) - assertFalse(model.isSensitive) - assertFalse(model.isRemote) - assertNull(model.loadThumbnail(mContext)) - } - @Test fun test_textClipData() { val source = "test source" @@ -71,7 +59,9 @@ class ClipboardModelTest : SysuiTestCase() { assertEquals(mSampleClipData, model.clipData) assertEquals(source, model.source) assertEquals(ClipboardModel.Type.TEXT, model.type) - assertEquals(mSampleClipData.getItemAt(0), model.item) + assertEquals(mSampleClipData.getItemAt(0).text, model.text) + assertEquals(mSampleClipData.getItemAt(0).textLinks, model.textLinks) + assertEquals(mSampleClipData.getItemAt(0).uri, model.uri) assertFalse(model.isSensitive) assertFalse(model.isRemote) assertNull(model.loadThumbnail(mContext)) @@ -84,7 +74,7 @@ class ClipboardModelTest : SysuiTestCase() { b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) description.extras = b val data = ClipData(description, mSampleClipData.getItemAt(0)) - val (_, _, _, _, sensitive) = + val (_, _, _, _, _, _, sensitive) = ClipboardModel.fromClipData(mContext, mClipboardUtils, data, "") assertTrue(sensitive) } 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 0ac26676a9c70..2099281d694ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -138,30 +138,36 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { mOverlayController.hideImmediate(); } - @Test - public void test_setClipData_nullData_legacy() { - ClipData clipData = null; - mOverlayController.setClipDataLegacy(clipData, ""); - - verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); - verify(mClipboardOverlayView, times(0)).showShareChip(); - verify(mClipboardOverlayView, times(1)).getEnterAnimation(); - } - @Test public void test_setClipData_invalidImageData_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); ClipData clipData = new ClipData("", new String[]{"image/png"}, new ClipData.Item(Uri.parse(""))); mOverlayController.setClipDataLegacy(clipData, ""); verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); - verify(mClipboardOverlayView, times(0)).showShareChip(); + verify(mClipboardOverlayView, times(1)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_nonImageUri_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); + ClipData clipData = new ClipData("", new String[]{"resource/png"}, + new ClipData.Item(Uri.parse(""))); + + mOverlayController.setClipDataLegacy(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_MINIMIZED_LAYOUT, false); + mOverlayController.setClipDataLegacy(mSampleClipData, ""); verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false); @@ -171,6 +177,8 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_setClipData_sensitiveTextData_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); + ClipDescription description = mSampleClipData.getDescription(); PersistableBundle b = new PersistableBundle(); b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true); @@ -185,6 +193,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_setClipData_repeatedCalls_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); when(mAnimator.isRunning()).thenReturn(true); mOverlayController.setClipDataLegacy(mSampleClipData, ""); @@ -195,6 +204,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_viewCallbacks_onShareTapped_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); mOverlayController.setClipDataLegacy(mSampleClipData, ""); mCallbacks.onShareButtonTapped(); @@ -205,6 +215,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_viewCallbacks_onDismissTapped_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); mOverlayController.setClipDataLegacy(mSampleClipData, ""); mCallbacks.onDismissButtonTapped(); @@ -215,6 +226,8 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_multipleDismissals_dismissesOnce_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); + mCallbacks.onSwipeDismissInitiated(mAnimator); mCallbacks.onDismissButtonTapped(); mCallbacks.onSwipeDismissInitiated(mAnimator); @@ -226,6 +239,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_remoteCopy_withFlagOn_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true); when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true); @@ -236,6 +250,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_remoteCopy_withFlagOff_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true); mOverlayController.setClipDataLegacy(mSampleClipData, ""); @@ -245,6 +260,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_nonRemoteCopy_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true); when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(false); @@ -255,6 +271,8 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_logsUseLastClipSource_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); + mOverlayController.setClipDataLegacy(mSampleClipData, "first.package"); mCallbacks.onDismissButtonTapped(); mOverlayController.setClipDataLegacy(mSampleClipData, "second.package"); @@ -267,6 +285,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_logOnClipboardActionsShown_legacy() { + mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false); ClipData.Item item = mSampleClipData.getItemAt(0); item.setTextLinks(Mockito.mock(TextLinks.class)); mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true); @@ -291,16 +310,6 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { } // start of refactored setClipData tests - @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"}, @@ -309,7 +318,19 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { mOverlayController.setClipData(clipData, ""); verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); - verify(mClipboardOverlayView, times(0)).showShareChip(); + verify(mClipboardOverlayView, times(1)).showShareChip(); + verify(mClipboardOverlayView, times(1)).getEnterAnimation(); + } + + @Test + public void test_setClipData_nonImageUri() { + ClipData clipData = new ClipData("", new String[]{"resource/png"}, + new ClipData.Item(Uri.parse(""))); + + mOverlayController.setClipData(clipData, ""); + + verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); + verify(mClipboardOverlayView, times(1)).showShareChip(); verify(mClipboardOverlayView, times(1)).getEnterAnimation(); } @@ -425,7 +446,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true); when(mClipboardUtils.isRemoteCopy(any(Context.class), any(ClipData.class), anyString())) .thenReturn(true); - when(mClipboardUtils.getAction(any(ClipData.Item.class), anyString())) + when(mClipboardUtils.getAction(any(CharSequence.class), any(TextLinks.class), anyString())) .thenReturn(Optional.of(Mockito.mock(RemoteAction.class))); when(mClipboardOverlayView.post(any(Runnable.class))).thenAnswer(new Answer() { @Override