Make all copied URIs shareable

We can only display thumbnails for images, but the sharesheet can
handle other types of URIs. Add the share button for these types
of copied data.

Also switches to not show UI if the clipData is null, and just show a toast if the clipboard contents have no items. (These cases should be approximately unreachable.)

Bug: 268057213
Fix: 268057213
Test: atest
Change-Id: I4c1e118c38c48c8ea8fd0d89ee426d7d961460af
This commit is contained in:
Miranda Kephart
2023-02-06 13:05:35 -05:00
parent eb87dfe80a
commit e9e8071989
7 changed files with 120 additions and 67 deletions

View File

@@ -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();

View File

@@ -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
}
}

View File

@@ -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> remoteAction =
mClipboardUtils.getAction(model.getItem(), model.getSource());
Optional<RemoteAction> 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();
}

View File

@@ -65,6 +65,23 @@ class ClipboardOverlayUtils {
return false;
}
public Optional<RemoteAction> 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<RemoteAction> getActions(CharSequence text, TextLinks textLinks) {
ArrayList<RemoteAction> 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<RemoteAction> getAction(ClipData.Item item, String source) {
return getActions(item).stream().filter(remoteAction -> {
ComponentName component = remoteAction.getActionIntent().getIntent().getComponent();

View File

@@ -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);

View File

@@ -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)
}

View File

@@ -138,17 +138,6 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
mOverlayController.hideImmediate();
}
@Test
public void test_setClipData_nullData_legacy() {
mFeatureFlags.set(CLIPBOARD_MINIMIZED_LAYOUT, false);
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);
@@ -158,7 +147,20 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
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();
}
@@ -308,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"},
@@ -326,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();
}
@@ -442,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<Object>() {
@Override