From 761c0ff9aca6706ab9f9c12cf769d99fb862c2d3 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Thu, 10 Feb 2022 10:05:16 +0100 Subject: [PATCH 1/2] [Android 10+] Add image preview of the content shared where possible These previews will be only available for images cached in the cache used by Picasso. The Bitmap of the content is compressed in JPEG 90 and saved inside the application cache folder under the name android_share_sheet_image_preview.jpg. The current image will be, of course, always overwritten by the next one and cleared when the application cache is cleared. --- .../schabi/newpipe/util/PicassoHelper.java | 7 ++ .../external_communication/ShareUtils.java | 119 ++++++++++++++++-- 2 files changed, 117 insertions(+), 9 deletions(-) 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..2434d2da4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java @@ -24,6 +24,9 @@ import java.util.function.Consumer; import okhttp3.OkHttpClient; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + public final class PicassoHelper { public static final String PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG"; private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY @@ -158,6 +161,10 @@ public final class PicassoHelper { }); } + @Nullable + public static Bitmap getImageFromCacheIfPresent(@NonNull final String imageUrl) { + return picassoCache.get(imageUrl); + } 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..1c2f20bf1 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() { } @@ -252,13 +265,16 @@ 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()) { + shareIntent.setClipData(generateClipDataForImagePreview(context, imagePreviewUrl)); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } openAppChooser(context, shareIntent, false); } @@ -266,11 +282,16 @@ 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. + * content and an image preview the content, if its URL is not null or empty and its + * corresponding image is in the image cache. + *

+ * *

* This calls {@link #shareText(Context, String, String, String)} with an empty string for the - * imagePreviewUrl parameter. + * {@code imagePreviewUrl} parameter. + *

* * @param context the context to use * @param title the title of the content @@ -301,4 +322,84 @@ 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 to not manage 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 yet loaded, no {@link ClipData} will be generated and {@code null} + * will be returned in this case. + *

+ * + *

+ * 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 100} 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}. + *

+ * + *

+ * This method 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 { + // URLs in the internal cache finish with \n so we need to add \n to image URLs + final Bitmap bitmap = PicassoHelper.getImageFromCacheIfPresent(thumbnailUrl + "\n"); + + 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; + } } From bd5eda92a714cd51ac7479bebdb423dc7fbb85bf Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 22 May 2022 21:34:10 +0200 Subject: [PATCH 2/2] Improvements to sharing content with thumbnail --- .../schabi/newpipe/util/PicassoHelper.java | 10 ++-- .../external_communication/ShareUtils.java | 52 +++++++++---------- 2 files changed, 30 insertions(+), 32 deletions(-) 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 2434d2da4..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; @@ -24,9 +26,6 @@ import java.util.function.Consumer; import okhttp3.OkHttpClient; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - public final class PicassoHelper { public static final String PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG"; private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY @@ -162,8 +161,9 @@ public final class PicassoHelper { } @Nullable - public static Bitmap getImageFromCacheIfPresent(@NonNull final String imageUrl) { - return picassoCache.get(imageUrl); + 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, 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 1c2f20bf1..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 @@ -244,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 @@ -272,8 +274,12 @@ public final class ShareUtils { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !TextUtils.isEmpty(imagePreviewUrl) && PicassoHelper.getShouldLoadImages()) { - shareIntent.setClipData(generateClipDataForImagePreview(context, imagePreviewUrl)); - shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + final ClipData clipData = generateClipDataForImagePreview(context, imagePreviewUrl); + if (clipData != null) { + shareIntent.setClipData(clipData); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } } openAppChooser(context, shareIntent, false); @@ -283,14 +289,9 @@ 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 and an image preview the content, if its URL is not null or empty and its - * corresponding image is in the image cache. - *

- * - *

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

* * @param context the context to use @@ -327,11 +328,11 @@ public final class ShareUtils { * Generate a {@link ClipData} with the image of the content shared, if it's in the app cache. * *

- * In order to not manage 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 yet loaded, no {@link ClipData} will be generated and {@code null} - * will be returned in this case. + * 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. *

* *

@@ -339,7 +340,7 @@ public final class ShareUtils { * 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 100} compression level. + * compressed in JPEG format, with a {@code 90} compression level. *

* *

@@ -354,8 +355,8 @@ public final class ShareUtils { *

* *

- * This method has only an effect on the system share sheet (if OEMs didn't change Android - * system standard behavior) on Android API 29 and higher. + * 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 @@ -367,9 +368,7 @@ public final class ShareUtils { @NonNull final Context context, @NonNull final String thumbnailUrl) { try { - // URLs in the internal cache finish with \n so we need to add \n to image URLs - final Bitmap bitmap = PicassoHelper.getImageFromCacheIfPresent(thumbnailUrl + "\n"); - + final Bitmap bitmap = PicassoHelper.getImageFromCacheIfPresent(thumbnailUrl); if (bitmap == null) { return null; } @@ -386,20 +385,19 @@ public final class ShareUtils { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream); fileOutputStream.close(); - final ClipData clipData = ClipData.newUri(applicationContext.getContentResolver(), - "", + 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; } - - return null; } }