Loading core/java/com/android/internal/app/ChooserActivity.java +139 −52 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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) Loading @@ -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: Loading @@ -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); Loading @@ -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); Loading Loading @@ -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; } Loading @@ -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; } Loading Loading @@ -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; Loading core/java/com/android/internal/util/ImageUtils.java +32 −30 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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; } } Loading @@ -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); } }); } } } core/res/res/drawable/ic_file_copy.xml 0 → 100644 +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> core/res/res/layout/chooser_grid.xml +59 −0 Original line number Diff line number Diff line Loading @@ -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" Loading core/res/res/values/strings.xml +5 −0 Original line number Diff line number Diff line Loading @@ -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
core/java/com/android/internal/app/ChooserActivity.java +139 −52 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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) Loading @@ -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: Loading @@ -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); Loading @@ -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); Loading Loading @@ -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; } Loading @@ -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; } Loading Loading @@ -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; Loading
core/java/com/android/internal/util/ImageUtils.java +32 −30 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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; } } Loading @@ -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); } }); } } }
core/res/res/drawable/ic_file_copy.xml 0 → 100644 +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>
core/res/res/layout/chooser_grid.xml +59 −0 Original line number Diff line number Diff line Loading @@ -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" Loading
core/res/res/values/strings.xml +5 −0 Original line number Diff line number Diff line Loading @@ -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>