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

Commit 46d828c9 authored by Matt Pietal's avatar Matt Pietal
Browse files

Sharesheet - file preview support

Support sharing 1 or more non-image type files, with potential
for system generated thumbnail image

Bug: 120419296
Test: atest ChooserActivityTest

Change-Id: I17c44435bb0444035e2ec7675cbc367b75cc3a8e
parent 2148e7f0
Loading
Loading
Loading
Loading
+139 −52
Original line number Original line Diff line number Diff line
@@ -46,6 +46,7 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.content.pm.ShortcutManager;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Canvas;
@@ -69,6 +70,8 @@ import android.os.ResultReceiver;
import android.os.UserHandle;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.UserManager;
import android.os.storage.StorageManager;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.service.chooser.ChooserTarget;
import android.service.chooser.ChooserTarget;
import android.service.chooser.ChooserTargetService;
import android.service.chooser.ChooserTargetService;
import android.service.chooser.IChooserTargetResult;
import android.service.chooser.IChooserTargetResult;
@@ -87,7 +90,6 @@ import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.LayoutParams;
import android.widget.AbsListView;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ListView;
@@ -373,50 +375,6 @@ public class ChooserActivity extends ResolverActivity {
        super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents,
        super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents,
                null, false);
                null, false);


        Button copyButton = findViewById(R.id.copy_button);
        copyButton.setOnClickListener(view -> {
            Intent targetIntent = getTargetIntent();
            if (targetIntent == null) {
                finish();
            } else {
                final String action = targetIntent.getAction();

                ClipData clipData = null;
                if (Intent.ACTION_SEND.equals(action)) {
                    String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT);
                    Uri extraStream = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);

                    if (extraText != null) {
                        clipData = ClipData.newPlainText(null, extraText);
                    } else if (extraStream != null) {
                        clipData = ClipData.newUri(getContentResolver(), null, extraStream);
                    } else {
                        Log.w(TAG, "No data available to copy to clipboard");
                        return;
                    }
                } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
                    final ArrayList<Uri> streams = targetIntent.getParcelableArrayListExtra(
                            Intent.EXTRA_STREAM);
                    clipData = ClipData.newUri(getContentResolver(), null, streams.get(0));
                    for (int i = 1; i < streams.size(); i++) {
                        clipData.addItem(getContentResolver(), new ClipData.Item(streams.get(i)));
                    }
                } else {
                    // expected to only be visible with ACTION_SEND or ACTION_SEND_MULTIPLE
                    // so warn about unexpected action
                    Log.w(TAG, "Action (" + action + ") not supported for copying to clipboard");
                    return;
                }

                ClipboardManager clipboardManager = (ClipboardManager) getSystemService(
                        Context.CLIPBOARD_SERVICE);
                clipboardManager.setPrimaryClip(clipData);
                Toast.makeText(getApplicationContext(), R.string.copied, Toast.LENGTH_SHORT).show();

                finish();
            }
        });

        mChooserShownTime = System.currentTimeMillis();
        mChooserShownTime = System.currentTimeMillis();
        final long systemCost = mChooserShownTime - intentReceivedTime;
        final long systemCost = mChooserShownTime - intentReceivedTime;


@@ -474,6 +432,10 @@ public class ChooserActivity extends ResolverActivity {
            return;
            return;
        }
        }


        if (mChooserListAdapter == null || mChooserListAdapter.getCount() == 0) {
            return;
        }

        int previewType = findPreferredContentPreview(targetIntent, getContentResolver());
        int previewType = findPreferredContentPreview(targetIntent, getContentResolver());


        getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW)
        getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW)
@@ -481,6 +443,49 @@ public class ChooserActivity extends ResolverActivity {
        displayContentPreview(previewType, targetIntent);
        displayContentPreview(previewType, targetIntent);
    }
    }


    private void onCopyButtonClicked(View v) {
        Intent targetIntent = getTargetIntent();
        if (targetIntent == null) {
            finish();
        } else {
            final String action = targetIntent.getAction();

            ClipData clipData = null;
            if (Intent.ACTION_SEND.equals(action)) {
                String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT);
                Uri extraStream = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);

                if (extraText != null) {
                    clipData = ClipData.newPlainText(null, extraText);
                } else if (extraStream != null) {
                    clipData = ClipData.newUri(getContentResolver(), null, extraStream);
                } else {
                    Log.w(TAG, "No data available to copy to clipboard");
                    return;
                }
            } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
                final ArrayList<Uri> streams = targetIntent.getParcelableArrayListExtra(
                        Intent.EXTRA_STREAM);
                clipData = ClipData.newUri(getContentResolver(), null, streams.get(0));
                for (int i = 1; i < streams.size(); i++) {
                    clipData.addItem(getContentResolver(), new ClipData.Item(streams.get(i)));
                }
            } else {
                // expected to only be visible with ACTION_SEND or ACTION_SEND_MULTIPLE
                // so warn about unexpected action
                Log.w(TAG, "Action (" + action + ") not supported for copying to clipboard");
                return;
            }

            ClipboardManager clipboardManager = (ClipboardManager) getSystemService(
                    Context.CLIPBOARD_SERVICE);
            clipboardManager.setPrimaryClip(clipData);
            Toast.makeText(getApplicationContext(), R.string.copied, Toast.LENGTH_SHORT).show();

            finish();
        }
    }

    private void displayContentPreview(@ContentPreviewType int previewType, Intent targetIntent) {
    private void displayContentPreview(@ContentPreviewType int previewType, Intent targetIntent) {
        switch (previewType) {
        switch (previewType) {
            case CONTENT_PREVIEW_TEXT:
            case CONTENT_PREVIEW_TEXT:
@@ -501,6 +506,8 @@ public class ChooserActivity extends ResolverActivity {
        ViewGroup contentPreviewLayout = findViewById(R.id.content_preview_text_area);
        ViewGroup contentPreviewLayout = findViewById(R.id.content_preview_text_area);
        contentPreviewLayout.setVisibility(View.VISIBLE);
        contentPreviewLayout.setVisibility(View.VISIBLE);


        findViewById(R.id.copy_button).setOnClickListener(this::onCopyButtonClicked);

        CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
        CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
        if (sharingText == null) {
        if (sharingText == null) {
            findViewById(R.id.content_preview_text_layout).setVisibility(View.GONE);
            findViewById(R.id.content_preview_text_layout).setVisibility(View.GONE);
@@ -510,7 +517,7 @@ public class ChooserActivity extends ResolverActivity {
        }
        }


        String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
        String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
        if (previewTitle == null || previewTitle.trim().isEmpty()) {
        if (TextUtils.isEmpty(previewTitle)) {
            findViewById(R.id.content_preview_title_layout).setVisibility(View.GONE);
            findViewById(R.id.content_preview_title_layout).setVisibility(View.GONE);
        } else {
        } else {
            TextView previewTitleView = findViewById(R.id.content_preview_title);
            TextView previewTitleView = findViewById(R.id.content_preview_title);
@@ -561,6 +568,7 @@ public class ChooserActivity extends ResolverActivity {
            if (imageUris.size() == 0) {
            if (imageUris.size() == 0) {
                Log.i(TAG, "Attempted to display image preview area with zero"
                Log.i(TAG, "Attempted to display image preview area with zero"
                        + " available images detected in EXTRA_STREAM list");
                        + " available images detected in EXTRA_STREAM list");
                contentPreviewLayout.setVisibility(View.GONE);
                return;
                return;
            }
            }


@@ -580,15 +588,95 @@ public class ChooserActivity extends ResolverActivity {
        }
        }
    }
    }


    private static class FileInfo {
        public final String name;
        public final boolean hasThumbnail;

        FileInfo(String name, boolean hasThumbnail) {
            this.name = name;
            this.hasThumbnail = hasThumbnail;
        }
    }

    private FileInfo extractFileInfo(Uri uri, ContentResolver resolver) {
        String fileName = null;
        boolean hasThumbnail = false;
        Cursor cursor = resolver.query(uri, null, null, null, null);
        if (cursor != null && cursor.getCount() > 0) {
            int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
            int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS);

            cursor.moveToFirst();
            fileName = cursor.getString(nameIndex);
            if (flagsIndex != -1) {
                hasThumbnail = (cursor.getInt(flagsIndex)
                        & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
            }
        }

        if (TextUtils.isEmpty(fileName)) {
            fileName = uri.getPath();
            int index = fileName.lastIndexOf('/');
            if (index != -1) {
                fileName = fileName.substring(index + 1);
            }
        }

        return new FileInfo(fileName, hasThumbnail);
    }

    private void displayFileContentPreview(Intent targetIntent) {
    private void displayFileContentPreview(Intent targetIntent) {
        // support coming
        ViewGroup contentPreviewLayout = findViewById(R.id.content_preview_file_area);
        contentPreviewLayout.setVisibility(View.VISIBLE);

        // TODO(b/120417119): Disable file copy until after moving to sysui,
        // due to permissions issues
        findViewById(R.id.file_copy_button).setVisibility(View.GONE);

        ContentResolver resolver = getContentResolver();
        TextView fileNameView = findViewById(R.id.content_preview_filename);
        String action = targetIntent.getAction();
        if (Intent.ACTION_SEND.equals(action)) {
            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);

            FileInfo fileInfo = extractFileInfo(uri, resolver);
            fileNameView.setText(fileInfo.name);

            if (fileInfo.hasThumbnail) {
                loadUriIntoView(R.id.content_preview_file_thumbnail, uri);
            } else {
                ImageView fileIconView = findViewById(R.id.content_preview_file_icon);
                fileIconView.setVisibility(View.VISIBLE);
                fileIconView.setImageResource(R.drawable.ic_doc_generic);
            }
        } else {
            List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
            if (uris.size() == 0) {
                contentPreviewLayout.setVisibility(View.GONE);
                Log.i(TAG,
                        "Appears to be no uris available in EXTRA_STREAM, removing preview area");
                return;
            }

            FileInfo fileInfo = extractFileInfo(uris.get(0), resolver);
            int remFileCount = uris.size() - 1;
            String fileName = getResources().getQuantityString(R.plurals.file_count,
                    remFileCount, fileInfo.name, remFileCount);

            fileNameView.setText(fileName);
            ImageView fileIconView = findViewById(R.id.content_preview_file_icon);
            fileIconView.setVisibility(View.VISIBLE);
            fileIconView.setImageResource(R.drawable.ic_file_copy);
        }
    }
    }


    private RoundedRectImageView loadUriIntoView(int imageResourceId, Uri uri) {
    private RoundedRectImageView loadUriIntoView(int imageResourceId, Uri uri) {
        RoundedRectImageView imageView = findViewById(imageResourceId);
        RoundedRectImageView imageView = findViewById(imageResourceId);
        imageView.setVisibility(View.VISIBLE);
        Bitmap bmp = loadThumbnail(uri, new Size(200, 200));
        Bitmap bmp = loadThumbnail(uri, new Size(200, 200));
        if (bmp != null) {
            imageView.setVisibility(View.VISIBLE);
            imageView.setImageBitmap(bmp);
            imageView.setImageBitmap(bmp);
        }


        return imageView;
        return imageView;
    }
    }
@@ -1261,9 +1349,8 @@ public class ChooserActivity extends ResolverActivity {
        }
        }


        try {
        try {
            return ImageUtils.decodeSampledBitmapFromStream(getContentResolver(),
            return ImageUtils.loadThumbnail(getContentResolver(), uri, size);
                uri, size.getWidth(), size.getHeight());
        } catch (IOException | NullPointerException | SecurityException ex) {
        } catch (IOException | NullPointerException ex) {
            Log.w(TAG, "Error loading preview thumbnail for uri: " + uri.toString(), ex);
            Log.w(TAG, "Error loading preview thumbnail for uri: " + uri.toString(), ex);
        }
        }
        return null;
        return null;
+32 −30
Original line number Original line Diff line number Diff line
@@ -16,20 +16,25 @@


package com.android.internal.util;
package com.android.internal.util;


import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Canvas;
import android.graphics.ImageDecoder;
import android.graphics.ImageDecoder.ImageInfo;
import android.graphics.ImageDecoder.Source;
import android.graphics.Matrix;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.net.Uri;
import android.os.Bundle;
import android.util.Size;


import java.io.IOException;
import java.io.IOException;
import java.io.InputStream;


/**
/**
 * Utility class for image analysis and processing.
 * Utility class for image analysis and processing.
@@ -166,21 +171,18 @@ public class ImageUtils {
    /**
    /**
     * @see https://developer.android.com/topic/performance/graphics/load-bitmap
     * @see https://developer.android.com/topic/performance/graphics/load-bitmap
     */
     */
    public static int calculateInSampleSize(BitmapFactory.Options options,
    public static int calculateSampleSize(Size currentSize, Size requestedSize) {
            int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        int inSampleSize = 1;


        if (height > reqHeight || width > reqWidth) {
        if (currentSize.getHeight() > requestedSize.getHeight()
            final int halfHeight = height / 2;
                || currentSize.getWidth() > requestedSize.getWidth()) {
            final int halfWidth = width / 2;
            final int halfHeight = currentSize.getHeight() / 2;
            final int halfWidth = currentSize.getWidth() / 2;


            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) >= reqHeight
            while ((halfHeight / inSampleSize) >= requestedSize.getHeight()
                    && (halfWidth / inSampleSize) >= reqWidth) {
                    && (halfWidth / inSampleSize) >= requestedSize.getWidth()) {
                inSampleSize *= 2;
                inSampleSize *= 2;
            }
            }
        }
        }
@@ -190,27 +192,27 @@ public class ImageUtils {


    /**
    /**
     * Load a bitmap, and attempt to downscale to the required size, to save
     * Load a bitmap, and attempt to downscale to the required size, to save
     * on memory.
     * on memory. Updated to use newer and more compatible ImageDecoder.
     *
     *
     * @see https://developer.android.com/topic/performance/graphics/load-bitmap
     * @see https://developer.android.com/topic/performance/graphics/load-bitmap
     */
     */
    public static Bitmap decodeSampledBitmapFromStream(ContentResolver resolver,
    public static Bitmap loadThumbnail(ContentResolver resolver, Uri uri, Size size)
            Uri uri, int reqWidth, int reqHeight) throws IOException {
            throws IOException {


        final BitmapFactory.Options options = new BitmapFactory.Options();
        try (ContentProviderClient client = resolver.acquireContentProviderClient(uri)) {
        try (InputStream is = resolver.openInputStream(uri)) {
            final Bundle opts = new Bundle();
            // First decode with inJustDecodeBounds=true to check dimensions
            opts.putParcelable(ContentResolver.EXTRA_SIZE, Point.convert(size));
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(is, null, options);


            options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
            return ImageDecoder.decodeBitmap(ImageDecoder.createSource(() -> {
        }
                return client.openTypedAssetFile(uri, "image/*", opts, null);
            }), (ImageDecoder decoder, ImageInfo info, Source source) -> {
                    decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);


        // need to do this twice as the InputStream is consumed in the first call,
                    final int sample = calculateSampleSize(info.getSize(), size);
        // and not all InputStreams support marks
                    if (sample > 1) {
        try (InputStream is = resolver.openInputStream(uri)) {
                        decoder.setTargetSampleSize(sample);
            options.inJustDecodeBounds = false;
                    }
            return BitmapFactory.decodeStream(is, null, options);
                });
        }
        }
    }
    }
}
}
+24 −0
Original line number Original line Diff line number Diff line
<!--
    Copyright (C) 2019 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
  <path
      android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM15,5l6,6v10c0,1.1 -0.9,2 -2,2L7.99,23C6.89,23 6,22.1 6,21l0.01,-14c0,-1.1 0.89,-2 1.99,-2h7zM14,12h5.5L14,6.5L14,12z"
      android:fillColor="#FF737373"/>
</vector>
+59 −0
Original line number Original line Diff line number Diff line
@@ -213,6 +213,65 @@
        </LinearLayout>
        </LinearLayout>
    </LinearLayout>
    </LinearLayout>


    <!-- Layout Option 3: File preview, icon, filename, copy-->
    <LinearLayout
        android:id="@+id/content_preview_file_area"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingBottom="@dimen/chooser_view_spacing"
        android:visibility="gone"
        android:background="?attr/colorBackgroundFloating">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingLeft="@dimen/chooser_edge_margin_normal"
            android:paddingRight="@dimen/chooser_edge_margin_normal"
            android:layout_marginBottom="@dimen/chooser_view_spacing"
            android:id="@+id/content_preview_file_layout">

            <view class="com.android.internal.app.ChooserActivity$RoundedRectImageView"
                  android:id="@+id/content_preview_file_thumbnail"
                  android:layout_width="75dp"
                  android:layout_height="75dp"
                  android:layout_marginRight="16dp"
                  android:adjustViewBounds="true"
                  android:layout_gravity="center_vertical"
                  android:gravity="center"
                  android:scaleType="centerCrop"
                  android:visibility="gone"/>
            <ImageView
                  android:id="@+id/content_preview_file_icon"
                  android:layout_width="36dp"
                  android:layout_height="36dp"
                  android:layout_marginRight="16dp"
                  android:adjustViewBounds="true"
                  android:layout_gravity="center_vertical"
                  android:gravity="center"
                  android:scaleType="fitCenter"
                  android:visibility="gone"/>
            <TextView
                android:id="@+id/content_preview_filename"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:ellipsize="middle"
                android:gravity="start|top"
                android:paddingRight="24dp"
                android:singleLine="true"/>
            <Button
                android:id="@+id/file_copy_button"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:gravity="center"
                android:layout_gravity="center_vertical"
                android:background="@drawable/ic_content_copy_gm2"/>
        </LinearLayout>
    </LinearLayout>

    <ListView
    <ListView
        android:layout_width="match_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_height="match_parent"
+5 −0
Original line number Original line Diff line number Diff line
@@ -5284,4 +5284,9 @@
    <!-- Strings for car -->
    <!-- Strings for car -->
    <!-- String displayed when loading a user in the car [CHAR LIMIT=30] -->
    <!-- String displayed when loading a user in the car [CHAR LIMIT=30] -->
    <string name="car_loading_profile">Loading</string>
    <string name="car_loading_profile">Loading</string>

    <plurals name="file_count">
        <item quantity="one"><xliff:g id="file_name">%s</xliff:g> + <xliff:g id="count">%d</xliff:g> file</item>
        <item quantity="other"><xliff:g id="file_name">%s</xliff:g> + <xliff:g id="count">%d</xliff:g> files</item>
    </plurals>
</resources>
</resources>
Loading