Skip to content

Commit 8c870cd

Browse files
authored
Merge pull request #8143 from TiA4f8R/image-preview-sharesheet
Add image preview of the content shared where possible in Android share sheet (for Android 10+ devices only)
2 parents cf09cef + bd5eda9 commit 8c870cd

2 files changed

Lines changed: 118 additions & 12 deletions

File tree

app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import android.graphics.Bitmap;
88
import android.graphics.drawable.Drawable;
99

10+
import androidx.annotation.Nullable;
11+
1012
import com.squareup.picasso.Cache;
1113
import com.squareup.picasso.LruCache;
1214
import com.squareup.picasso.OkHttp3Downloader;
@@ -158,6 +160,11 @@ public String key() {
158160
});
159161
}
160162

163+
@Nullable
164+
public static Bitmap getImageFromCacheIfPresent(final String imageUrl) {
165+
// URLs in the internal cache finish with \n so we need to add \n to image URLs
166+
return picassoCache.get(imageUrl + "\n");
167+
}
161168

162169
public static void loadNotificationIcon(final String url,
163170
final Consumer<Bitmap> bitmapConsumer) {

app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,36 @@
11
package org.schabi.newpipe.util.external_communication;
22

3+
import static org.schabi.newpipe.MainActivity.DEBUG;
4+
35
import android.content.ActivityNotFoundException;
46
import android.content.ClipData;
57
import android.content.ClipboardManager;
68
import android.content.Context;
79
import android.content.Intent;
810
import android.content.pm.PackageManager;
911
import android.content.pm.ResolveInfo;
12+
import android.graphics.Bitmap;
1013
import android.net.Uri;
1114
import android.os.Build;
1215
import android.text.TextUtils;
16+
import android.util.Log;
1317
import android.widget.Toast;
1418

1519
import androidx.annotation.NonNull;
20+
import androidx.annotation.Nullable;
1621
import androidx.core.content.ContextCompat;
22+
import androidx.core.content.FileProvider;
1723

24+
import org.schabi.newpipe.BuildConfig;
1825
import org.schabi.newpipe.R;
26+
import org.schabi.newpipe.util.PicassoHelper;
27+
28+
import java.io.File;
29+
import java.io.FileOutputStream;
1930

2031
public 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

Comments
 (0)