11package org .schabi .newpipe .util .external_communication ;
22
3+ import static org .schabi .newpipe .MainActivity .DEBUG ;
4+
35import android .content .ActivityNotFoundException ;
46import android .content .ClipData ;
57import android .content .ClipboardManager ;
68import android .content .Context ;
79import android .content .Intent ;
810import android .content .pm .PackageManager ;
911import android .content .pm .ResolveInfo ;
12+ import android .graphics .Bitmap ;
1013import android .net .Uri ;
1114import android .os .Build ;
1215import android .text .TextUtils ;
16+ import android .util .Log ;
1317import android .widget .Toast ;
1418
1519import androidx .annotation .NonNull ;
20+ import androidx .annotation .Nullable ;
1621import androidx .core .content .ContextCompat ;
22+ import androidx .core .content .FileProvider ;
1723
24+ import org .schabi .newpipe .BuildConfig ;
1825import org .schabi .newpipe .R ;
26+ import org .schabi .newpipe .util .PicassoHelper ;
27+
28+ import java .io .File ;
29+ import java .io .FileOutputStream ;
1930
2031public final class ShareUtils {
32+ private static final String TAG = ShareUtils .class .getSimpleName ();
33+
2134 private ShareUtils () {
2235 }
2336
@@ -231,9 +244,11 @@ private static String getDefaultAppPackageName(@NonNull final Context context,
231244 /**
232245 * Open the android share sheet to share a content.
233246 *
247+ * <p>
234248 * For Android 10+ users, a content preview is shown, which includes the title of the shared
235- * content.
236- * Support sharing the image of the content needs to done, if possible.
249+ * content and an image preview the content, if its URL is not null or empty and its
250+ * corresponding image is in the image cache.
251+ * </p>
237252 *
238253 * @param context the context to use
239254 * @param title the title of the content
@@ -252,25 +267,32 @@ public static void shareText(@NonNull final Context context,
252267 shareIntent .putExtra (Intent .EXTRA_SUBJECT , title );
253268 }
254269
255- /* TODO: add the image of the content to Android share sheet with setClipData after
256- generating a content URI of this image, then use ClipData.newUri(the content resolver,
257- null, the content URI) and set the ClipData to the share intent with
258- shareIntent.setClipData(generated ClipData).
259- if (!imagePreviewUrl.isEmpty()) {
260- //shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
261- }*/
270+ // Content preview in the share sheet has been added in Android 10, so it's not needed to
271+ // set a content preview which will be never displayed
272+ // See https://developer.android.com/training/sharing/send#adding-rich-content-previews
273+ // If loading of images has been disabled, don't try to generate a content preview
274+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q
275+ && !TextUtils .isEmpty (imagePreviewUrl )
276+ && PicassoHelper .getShouldLoadImages ()) {
277+
278+ final ClipData clipData = generateClipDataForImagePreview (context , imagePreviewUrl );
279+ if (clipData != null ) {
280+ shareIntent .setClipData (clipData );
281+ shareIntent .setFlags (Intent .FLAG_GRANT_READ_URI_PERMISSION );
282+ }
283+ }
262284
263285 openAppChooser (context , shareIntent , false );
264286 }
265287
266288 /**
267289 * Open the android share sheet to share a content.
268290 *
269- * For Android 10+ users, a content preview is shown, which includes the title of the shared
270- * content.
271291 * <p>
272292 * This calls {@link #shareText(Context, String, String, String)} with an empty string for the
273- * imagePreviewUrl parameter.
293+ * {@code imagePreviewUrl} parameter. This method should be used when the shared content has no
294+ * preview thumbnail.
295+ * </p>
274296 *
275297 * @param context the context to use
276298 * @param title the title of the content
@@ -301,4 +323,81 @@ public static void copyToClipboard(@NonNull final Context context, final String
301323 clipboardManager .setPrimaryClip (ClipData .newPlainText (null , text ));
302324 Toast .makeText (context , R .string .msg_copied , Toast .LENGTH_SHORT ).show ();
303325 }
326+
327+ /**
328+ * Generate a {@link ClipData} with the image of the content shared, if it's in the app cache.
329+ *
330+ * <p>
331+ * In order not to worry about network issues (timeouts, DNS issues, low connection speed, ...)
332+ * when sharing a content, only images in the {@link com.squareup.picasso.LruCache LruCache}
333+ * used by the Picasso library inside {@link PicassoHelper} are used as preview images. If the
334+ * thumbnail image is not in the cache, no {@link ClipData} will be generated and {@code null}
335+ * will be returned.
336+ * </p>
337+ *
338+ * <p>
339+ * In order to display the image in the content preview of the Android share sheet, an URI of
340+ * the content, accessible and readable by other apps has to be generated, so a new file inside
341+ * the application cache will be generated, named {@code android_share_sheet_image_preview.jpg}
342+ * (if a file under this name already exists, it will be overwritten). The thumbnail will be
343+ * compressed in JPEG format, with a {@code 90} compression level.
344+ * </p>
345+ *
346+ * <p>
347+ * Note that if an exception occurs when generating the {@link ClipData}, {@code null} is
348+ * returned.
349+ * </p>
350+ *
351+ * <p>
352+ * This method will call {@link PicassoHelper#getImageFromCacheIfPresent(String)} to get the
353+ * thumbnail of the content in the {@link com.squareup.picasso.LruCache LruCache} used by
354+ * the Picasso library inside {@link PicassoHelper}.
355+ * </p>
356+ *
357+ * <p>
358+ * Using the result of this method when sharing has only an effect on the system share sheet (if
359+ * OEMs didn't change Android system standard behavior) on Android API 29 and higher.
360+ * </p>
361+ *
362+ * @param context the context to use
363+ * @param thumbnailUrl the URL of the content thumbnail
364+ * @return a {@link ClipData} of the content thumbnail, or {@code null}
365+ */
366+ @ Nullable
367+ private static ClipData generateClipDataForImagePreview (
368+ @ NonNull final Context context ,
369+ @ NonNull final String thumbnailUrl ) {
370+ try {
371+ final Bitmap bitmap = PicassoHelper .getImageFromCacheIfPresent (thumbnailUrl );
372+ if (bitmap == null ) {
373+ return null ;
374+ }
375+
376+ // Save the image in memory to the application's cache because we need a URI to the
377+ // image to generate a ClipData which will show the share sheet, and so an image file
378+ final Context applicationContext = context .getApplicationContext ();
379+ final String appFolder = applicationContext .getCacheDir ().getAbsolutePath ();
380+ final File thumbnailPreviewFile = new File (appFolder
381+ + "/android_share_sheet_image_preview.jpg" );
382+
383+ // Any existing file will be overwritten with FileOutputStream
384+ final FileOutputStream fileOutputStream = new FileOutputStream (thumbnailPreviewFile );
385+ bitmap .compress (Bitmap .CompressFormat .JPEG , 90 , fileOutputStream );
386+ fileOutputStream .close ();
387+
388+ final ClipData clipData = ClipData .newUri (applicationContext .getContentResolver (), "" ,
389+ FileProvider .getUriForFile (applicationContext ,
390+ BuildConfig .APPLICATION_ID + ".provider" ,
391+ thumbnailPreviewFile ));
392+
393+ if (DEBUG ) {
394+ Log .d (TAG , "ClipData successfully generated for Android share sheet: " + clipData );
395+ }
396+ return clipData ;
397+
398+ } catch (final Exception e ) {
399+ Log .w (TAG , "Error when setting preview image for share sheet" , e );
400+ return null ;
401+ }
402+ }
304403}
0 commit comments