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

Commit af5c88b8 authored by Matt Pietal's avatar Matt Pietal Committed by Android (Google) Code Review
Browse files

Merge "Sharesheet - file preview support"

parents 63ad256e 46d828c9
Loading
Loading
Loading
Loading
+139 −52
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -69,6 +70,8 @@ import android.os.ResultReceiver;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.service.chooser.ChooserTarget;
import android.service.chooser.ChooserTargetService;
import android.service.chooser.IChooserTargetResult;
@@ -87,7 +90,6 @@ import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
@@ -373,50 +375,6 @@ public class ChooserActivity extends ResolverActivity {
        super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents,
                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();
        final long systemCost = mChooserShownTime - intentReceivedTime;

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

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

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

        getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW)
@@ -481,6 +443,49 @@ public class ChooserActivity extends ResolverActivity {
        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) {
        switch (previewType) {
            case CONTENT_PREVIEW_TEXT:
@@ -501,6 +506,8 @@ public class ChooserActivity extends ResolverActivity {
        ViewGroup contentPreviewLayout = findViewById(R.id.content_preview_text_area);
        contentPreviewLayout.setVisibility(View.VISIBLE);

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

        CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
        if (sharingText == null) {
            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);
        if (previewTitle == null || previewTitle.trim().isEmpty()) {
        if (TextUtils.isEmpty(previewTitle)) {
            findViewById(R.id.content_preview_title_layout).setVisibility(View.GONE);
        } else {
            TextView previewTitleView = findViewById(R.id.content_preview_title);
@@ -561,6 +568,7 @@ public class ChooserActivity extends ResolverActivity {
            if (imageUris.size() == 0) {
                Log.i(TAG, "Attempted to display image preview area with zero"
                        + " available images detected in EXTRA_STREAM list");
                contentPreviewLayout.setVisibility(View.GONE);
                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) {
        // 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) {
        RoundedRectImageView imageView = findViewById(imageResourceId);
        imageView.setVisibility(View.VISIBLE);
        Bitmap bmp = loadThumbnail(uri, new Size(200, 200));
        if (bmp != null) {
            imageView.setVisibility(View.VISIBLE);
            imageView.setImageBitmap(bmp);
        }

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

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

package com.android.internal.util;

import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
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.Paint;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.Size;

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

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

        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
        if (currentSize.getHeight() > requestedSize.getHeight()
                || currentSize.getWidth() > requestedSize.getWidth()) {
            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
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
            while ((halfHeight / inSampleSize) >= requestedSize.getHeight()
                    && (halfWidth / inSampleSize) >= requestedSize.getWidth()) {
                inSampleSize *= 2;
            }
        }
@@ -190,27 +192,27 @@ public class ImageUtils {

    /**
     * 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
     */
    public static Bitmap decodeSampledBitmapFromStream(ContentResolver resolver,
            Uri uri, int reqWidth, int reqHeight) throws IOException {
    public static Bitmap loadThumbnail(ContentResolver resolver, Uri uri, Size size)
            throws IOException {

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

            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,
        // and not all InputStreams support marks
        try (InputStream is = resolver.openInputStream(uri)) {
            options.inJustDecodeBounds = false;
            return BitmapFactory.decodeStream(is, null, options);
                    final int sample = calculateSampleSize(info.getSize(), size);
                    if (sample > 1) {
                        decoder.setTargetSampleSize(sample);
                    }
                });
        }
    }
}
+24 −0
Original line number 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 Diff line number Diff line
@@ -213,6 +213,65 @@
        </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
        android:layout_width="match_parent"
        android:layout_height="match_parent"
+5 −0
Original line number Diff line number Diff line
@@ -5284,4 +5284,9 @@
    <!-- Strings for car -->
    <!-- String displayed when loading a user in the car [CHAR LIMIT=30] -->
    <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>
Loading