diff --git a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java index da86ab1a4..aabc459d0 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java @@ -7,6 +7,8 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; +import androidx.annotation.Nullable; + import com.squareup.picasso.Cache; import com.squareup.picasso.LruCache; import com.squareup.picasso.OkHttp3Downloader; @@ -158,6 +160,11 @@ public final class PicassoHelper { }); } + @Nullable + public static Bitmap getImageFromCacheIfPresent(final String imageUrl) { + // URLs in the internal cache finish with \n so we need to add \n to image URLs + return picassoCache.get(imageUrl + "\n"); + } public static void loadNotificationIcon(final String url, final Consumer bitmapConsumer) { diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java index c4f1675cf..8324146fe 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.util.external_communication; +import static org.schabi.newpipe.MainActivity.DEBUG; + import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; @@ -7,17 +9,28 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.text.TextUtils; +import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.PicassoHelper; + +import java.io.File; +import java.io.FileOutputStream; public final class ShareUtils { + private static final String TAG = ShareUtils.class.getSimpleName(); + private ShareUtils() { } @@ -231,9 +244,11 @@ public final class ShareUtils { /** * Open the android share sheet to share a content. * + *

* For Android 10+ users, a content preview is shown, which includes the title of the shared - * content. - * Support sharing the image of the content needs to done, if possible. + * content and an image preview the content, if its URL is not null or empty and its + * corresponding image is in the image cache. + *

* * @param context the context to use * @param title the title of the content @@ -252,13 +267,20 @@ public final class ShareUtils { shareIntent.putExtra(Intent.EXTRA_SUBJECT, title); } - /* TODO: add the image of the content to Android share sheet with setClipData after - generating a content URI of this image, then use ClipData.newUri(the content resolver, - null, the content URI) and set the ClipData to the share intent with - shareIntent.setClipData(generated ClipData). - if (!imagePreviewUrl.isEmpty()) { - //shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - }*/ + // Content preview in the share sheet has been added in Android 10, so it's not needed to + // set a content preview which will be never displayed + // See https://developer.android.com/training/sharing/send#adding-rich-content-previews + // If loading of images has been disabled, don't try to generate a content preview + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + && !TextUtils.isEmpty(imagePreviewUrl) + && PicassoHelper.getShouldLoadImages()) { + + final ClipData clipData = generateClipDataForImagePreview(context, imagePreviewUrl); + if (clipData != null) { + shareIntent.setClipData(clipData); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } openAppChooser(context, shareIntent, false); } @@ -266,11 +288,11 @@ public final class ShareUtils { /** * Open the android share sheet to share a content. * - * For Android 10+ users, a content preview is shown, which includes the title of the shared - * content. *

* This calls {@link #shareText(Context, String, String, String)} with an empty string for the - * imagePreviewUrl parameter. + * {@code imagePreviewUrl} parameter. This method should be used when the shared content has no + * preview thumbnail. + *

* * @param context the context to use * @param title the title of the content @@ -301,4 +323,81 @@ public final class ShareUtils { clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text)); Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show(); } + + /** + * Generate a {@link ClipData} with the image of the content shared, if it's in the app cache. + * + *

+ * In order not to worry about network issues (timeouts, DNS issues, low connection speed, ...) + * when sharing a content, only images in the {@link com.squareup.picasso.LruCache LruCache} + * used by the Picasso library inside {@link PicassoHelper} are used as preview images. If the + * thumbnail image is not in the cache, no {@link ClipData} will be generated and {@code null} + * will be returned. + *

+ * + *

+ * In order to display the image in the content preview of the Android share sheet, an URI of + * the content, accessible and readable by other apps has to be generated, so a new file inside + * the application cache will be generated, named {@code android_share_sheet_image_preview.jpg} + * (if a file under this name already exists, it will be overwritten). The thumbnail will be + * compressed in JPEG format, with a {@code 90} compression level. + *

+ * + *

+ * Note that if an exception occurs when generating the {@link ClipData}, {@code null} is + * returned. + *

+ * + *

+ * This method will call {@link PicassoHelper#getImageFromCacheIfPresent(String)} to get the + * thumbnail of the content in the {@link com.squareup.picasso.LruCache LruCache} used by + * the Picasso library inside {@link PicassoHelper}. + *

+ * + *

+ * Using the result of this method when sharing has only an effect on the system share sheet (if + * OEMs didn't change Android system standard behavior) on Android API 29 and higher. + *

+ * + * @param context the context to use + * @param thumbnailUrl the URL of the content thumbnail + * @return a {@link ClipData} of the content thumbnail, or {@code null} + */ + @Nullable + private static ClipData generateClipDataForImagePreview( + @NonNull final Context context, + @NonNull final String thumbnailUrl) { + try { + final Bitmap bitmap = PicassoHelper.getImageFromCacheIfPresent(thumbnailUrl); + if (bitmap == null) { + return null; + } + + // Save the image in memory to the application's cache because we need a URI to the + // image to generate a ClipData which will show the share sheet, and so an image file + final Context applicationContext = context.getApplicationContext(); + final String appFolder = applicationContext.getCacheDir().getAbsolutePath(); + final File thumbnailPreviewFile = new File(appFolder + + "/android_share_sheet_image_preview.jpg"); + + // Any existing file will be overwritten with FileOutputStream + final FileOutputStream fileOutputStream = new FileOutputStream(thumbnailPreviewFile); + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream); + fileOutputStream.close(); + + final ClipData clipData = ClipData.newUri(applicationContext.getContentResolver(), "", + FileProvider.getUriForFile(applicationContext, + BuildConfig.APPLICATION_ID + ".provider", + thumbnailPreviewFile)); + + if (DEBUG) { + Log.d(TAG, "ClipData successfully generated for Android share sheet: " + clipData); + } + return clipData; + + } catch (final Exception e) { + Log.w(TAG, "Error when setting preview image for share sheet", e); + return null; + } + } }