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:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user