Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1e52fddd authored by Matt Casey's avatar Matt Casey
Browse files

Fix MIME type of non-plain text share intents

Previously we piped the MIME type to sharesheet, which was incorrect.
Also switch to ClipData.Item's coerceToText() method to avoid NPE when getText() is null.

Also pull all the intent creation code into its own class, test it.

Bug: 243447863
Bug: 236932369
Bug: 222640396

Test: atest IntentCreatorTest
Test: Share text copied from Chrome, note that the full correct app list
      comes up.
Change-Id: Ia438a40423f518275fcd8a09f7de23c511b116fa
parent de117f20
Loading
Loading
Loading
Loading
+4 −43
Original line number Diff line number Diff line
@@ -119,15 +119,12 @@ import java.util.ArrayList;
 */
public class ClipboardOverlayController {
    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;
@@ -383,7 +380,7 @@ public class ClipboardOverlayController {
                    mTextPreview);
            accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
        }
        Intent remoteCopyIntent = getRemoteCopyIntent(clipData);
        Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext);
        // Only show remote copy if it's available.
        PackageManager packageManager = mContext.getPackageManager();
        if (packageManager.resolveActivity(
@@ -500,41 +497,19 @@ public class ClipboardOverlayController {

    private void editImage(Uri uri) {
        mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
        String editorPackage = mContext.getString(R.string.config_screenshotEditor);
        Intent editIntent = new Intent(Intent.ACTION_EDIT);
        if (!TextUtils.isEmpty(editorPackage)) {
            editIntent.setComponent(ComponentName.unflattenFromString(editorPackage));
        }
        editIntent.setDataAndType(uri, "image/*");
        editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        editIntent.putExtra(EXTRA_EDIT_SOURCE_CLIPBOARD, true);
        mContext.startActivity(editIntent);
        mContext.startActivity(IntentCreator.getImageEditIntent(uri, mContext));
        animateOut();
    }

    private void editText() {
        mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
        Intent editIntent = new Intent(mContext, EditTextActivity.class);
        editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        mContext.startActivity(editIntent);
        mContext.startActivity(IntentCreator.getTextEditorIntent(mContext));
        animateOut();
    }

    private void shareContent(ClipData clip) {
        mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SHARE_TAPPED);
        Intent shareIntent = new Intent(Intent.ACTION_SEND);
        shareIntent.setDataAndType(
                clip.getItemAt(0).getUri(), clip.getDescription().getMimeType(0));
        shareIntent.putExtra(Intent.EXTRA_TEXT, clip.getItemAt(0).getText().toString());
        if (clip.getItemAt(0).getUri() != null) {
            shareIntent.putExtra(Intent.EXTRA_STREAM, clip.getItemAt(0).getUri());
            shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        }
        Intent chooserIntent = Intent.createChooser(shareIntent, null)
                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        mContext.startActivity(chooserIntent);
        mContext.startActivity(IntentCreator.getShareIntent(clip, mContext));
        animateOut();
    }

@@ -667,20 +642,6 @@ public class ClipboardOverlayController {
                mContext.getString(R.string.clipboard_edit), null);
    }

    private Intent getRemoteCopyIntent(ClipData clipData) {
        Intent nearbyIntent = new Intent(REMOTE_COPY_ACTION);

        String remoteCopyPackage = mContext.getString(R.string.config_remoteCopyPackage);
        if (!TextUtils.isEmpty(remoteCopyPackage)) {
            nearbyIntent.setComponent(ComponentName.unflattenFromString(remoteCopyPackage));
        }

        nearbyIntent.setClipData(clipData);
        nearbyIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        return nearbyIntent;
    }

    private void animateIn() {
        if (mAccessibilityManager.isEnabled()) {
            mDismissButton.setVisibility(View.VISIBLE);
+86 −0
Original line number Diff line number Diff line
/*
 * 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.content.ClipData;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;

import com.android.systemui.R;

class IntentCreator {
    private static final String EXTRA_EDIT_SOURCE_CLIPBOARD = "edit_source_clipboard";
    private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY";

    static Intent getTextEditorIntent(Context context) {
        Intent intent = new Intent(context, EditTextActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        return intent;
    }

    static Intent getShareIntent(ClipData clipData, Context context) {
        Intent shareIntent = new Intent(Intent.ACTION_SEND);

        // From the ACTION_SEND docs:
        //   "If using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it should be the
        //    MIME type of the data in EXTRA_STREAM"
        if (clipData.getItemAt(0).getUri() != null) {
            shareIntent.setDataAndType(
                    clipData.getItemAt(0).getUri(), clipData.getDescription().getMimeType(0));
            shareIntent.putExtra(Intent.EXTRA_STREAM, clipData.getItemAt(0).getUri());
            shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        } else {
            shareIntent.putExtra(Intent.EXTRA_TEXT, clipData.getItemAt(0).coerceToText(context));
            shareIntent.setType("text/plain");
        }
        Intent chooserIntent = Intent.createChooser(shareIntent, null)
                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

        return chooserIntent;
    }

    static Intent getImageEditIntent(Uri uri, Context context) {
        String editorPackage = context.getString(R.string.config_screenshotEditor);
        Intent editIntent = new Intent(Intent.ACTION_EDIT);
        if (!TextUtils.isEmpty(editorPackage)) {
            editIntent.setComponent(ComponentName.unflattenFromString(editorPackage));
        }
        editIntent.setDataAndType(uri, "image/*");
        editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        editIntent.putExtra(EXTRA_EDIT_SOURCE_CLIPBOARD, true);
        return editIntent;
    }

    static Intent getRemoteCopyIntent(ClipData clipData, Context context) {
        Intent nearbyIntent = new Intent(REMOTE_COPY_ACTION);

        String remoteCopyPackage = context.getString(R.string.config_remoteCopyPackage);
        if (!TextUtils.isEmpty(remoteCopyPackage)) {
            nearbyIntent.setComponent(ComponentName.unflattenFromString(remoteCopyPackage));
        }

        nearbyIntent.setClipData(clipData);
        nearbyIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        return nearbyIntent;
    }
}
+137 −0
Original line number Diff line number Diff line
/*
 * 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.content.ClipData;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;

import org.junit.Test;
import org.junit.runner.RunWith;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class IntentCreatorTest extends SysuiTestCase {
    private static final int EXTERNAL_INTENT_FLAGS = Intent.FLAG_ACTIVITY_NEW_TASK
            | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION;

    @Test
    public void test_getTextEditorIntent() {
        Intent intent = IntentCreator.getTextEditorIntent(getContext());
        assertEquals(new ComponentName(getContext(), EditTextActivity.class),
                intent.getComponent());
        assertFlags(intent, Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    }

    @Test
    public void test_getRemoteCopyIntent() {
        getContext().getOrCreateTestableResources().addOverride(R.string.config_remoteCopyPackage,
                "");

        ClipData clipData = ClipData.newPlainText("Test", "Test Item");
        Intent intent = IntentCreator.getRemoteCopyIntent(clipData, getContext());

        assertEquals(null, intent.getComponent());
        assertFlags(intent, EXTERNAL_INTENT_FLAGS);
        assertEquals(clipData, intent.getClipData());

        // Try again with a remote copy component
        ComponentName fakeComponent = new ComponentName("com.android.remotecopy",
                "com.android.remotecopy.RemoteCopyActivity");
        getContext().getOrCreateTestableResources().addOverride(R.string.config_remoteCopyPackage,
                fakeComponent.flattenToString());

        intent = IntentCreator.getRemoteCopyIntent(clipData, getContext());
        assertEquals(fakeComponent, intent.getComponent());
    }

    @Test
    public void test_getImageEditIntent() {
        getContext().getOrCreateTestableResources().addOverride(R.string.config_screenshotEditor,
                "");
        Uri fakeUri = Uri.parse("content://foo");
        Intent intent = IntentCreator.getImageEditIntent(fakeUri, getContext());

        assertEquals(Intent.ACTION_EDIT, intent.getAction());
        assertEquals("image/*", intent.getType());
        assertEquals(null, intent.getComponent());
        assertFlags(intent, EXTERNAL_INTENT_FLAGS);

        // try again with an editor component
        ComponentName fakeComponent = new ComponentName("com.android.remotecopy",
                "com.android.remotecopy.RemoteCopyActivity");
        getContext().getOrCreateTestableResources().addOverride(R.string.config_screenshotEditor,
                fakeComponent.flattenToString());
        intent = IntentCreator.getImageEditIntent(fakeUri, getContext());
        assertEquals(fakeComponent, intent.getComponent());
    }

    @Test
    public void test_getShareIntent_plaintext() {
        ClipData clipData = ClipData.newPlainText("Test", "Test Item");
        Intent intent = IntentCreator.getShareIntent(clipData, getContext());

        assertEquals(Intent.ACTION_CHOOSER, intent.getAction());
        assertFlags(intent, EXTERNAL_INTENT_FLAGS);
        Intent target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent.class);
        assertEquals("Test Item", target.getStringExtra(Intent.EXTRA_TEXT));
        assertEquals("text/plain", target.getType());
    }

    @Test
    public void test_getShareIntent_html() {
        ClipData clipData = ClipData.newHtmlText("Test", "Some HTML",
                "<b>Some HTML</b>");
        Intent intent = IntentCreator.getShareIntent(clipData, getContext());

        assertEquals(Intent.ACTION_CHOOSER, intent.getAction());
        assertFlags(intent, EXTERNAL_INTENT_FLAGS);
        Intent target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent.class);
        assertEquals("Some HTML", target.getStringExtra(Intent.EXTRA_TEXT));
        assertEquals("text/plain", target.getType());
    }

    @Test
    public void test_getShareIntent_image() {
        Uri uri = Uri.parse("content://something");
        ClipData clipData = new ClipData("Test", new String[]{"image/png"},
                new ClipData.Item(uri));
        Intent intent = IntentCreator.getShareIntent(clipData, getContext());

        assertEquals(Intent.ACTION_CHOOSER, intent.getAction());
        assertFlags(intent, EXTERNAL_INTENT_FLAGS);
        Intent target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent.class);
        assertEquals(uri, target.getData());
        assertEquals("image/png", target.getType());
    }

    // Assert that the given flags are set
    private void assertFlags(Intent intent, int flags) {
        assertTrue((intent.getFlags() & flags) == flags);
    }

}