Skip to content

Commit 761c0ff

Browse files
committed
[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.
1 parent 2dd4f8b commit 761c0ff

2 files changed

Lines changed: 117 additions & 9 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
@@ -24,6 +24,9 @@
2424

2525
import okhttp3.OkHttpClient;
2626

27+
import androidx.annotation.NonNull;
28+
import androidx.annotation.Nullable;
29+
2730
public final class PicassoHelper {
2831
public static final String PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
2932
private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY
@@ -158,6 +161,10 @@ public String key() {
158161
});
159162
}
160163

164+
@Nullable
165+
public static Bitmap getImageFromCacheIfPresent(@NonNull final String imageUrl) {
166+
return picassoCache.get(imageUrl);
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: 110 additions & 9 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

@@ -252,25 +265,33 @@ public static void shareText(@NonNull final Context context,
252265
shareIntent.putExtra(Intent.EXTRA_SUBJECT, title);
253266
}
254267

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-
}*/
268+
// Content preview in the share sheet has been added in Android 10, so it's not needed to
269+
// set a content preview which will be never displayed
270+
// See https://developer.android.com/training/sharing/send#adding-rich-content-previews
271+
// If loading of images has been disabled, don't try to generate a content preview
272+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
273+
&& !TextUtils.isEmpty(imagePreviewUrl)
274+
&& PicassoHelper.getShouldLoadImages()) {
275+
shareIntent.setClipData(generateClipDataForImagePreview(context, imagePreviewUrl));
276+
shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
277+
}
262278

263279
openAppChooser(context, shareIntent, false);
264280
}
265281

266282
/**
267283
* Open the android share sheet to share a content.
268284
*
285+
* <p>
269286
* For Android 10+ users, a content preview is shown, which includes the title of the shared
270-
* content.
287+
* content and an image preview the content, if its URL is not null or empty and its
288+
* corresponding image is in the image cache.
289+
* </p>
290+
*
271291
* <p>
272292
* This calls {@link #shareText(Context, String, String, String)} with an empty string for the
273-
* imagePreviewUrl parameter.
293+
* {@code imagePreviewUrl} parameter.
294+
* </p>
274295
*
275296
* @param context the context to use
276297
* @param title the title of the content
@@ -301,4 +322,84 @@ public static void copyToClipboard(@NonNull final Context context, final String
301322
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text));
302323
Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show();
303324
}
325+
326+
/**
327+
* Generate a {@link ClipData} with the image of the content shared, if it's in the app cache.
328+
*
329+
* <p>
330+
* In order to not manage network issues (timeouts, DNS issues, low connection speed, ...) when
331+
* sharing a content, only images in the {@link com.squareup.picasso.LruCache LruCache} used by
332+
* the Picasso library inside {@link PicassoHelper} are used as preview images. If the
333+
* thumbnail image is not yet loaded, no {@link ClipData} will be generated and {@code null}
334+
* will be returned in this case.
335+
* </p>
336+
*
337+
* <p>
338+
* In order to display the image in the content preview of the Android share sheet, an URI of
339+
* the content, accessible and readable by other apps has to be generated, so a new file inside
340+
* the application cache will be generated, named {@code android_share_sheet_image_preview.jpg}
341+
* (if a file under this name already exists, it will be overwritten). The thumbnail will be
342+
* compressed in JPEG format, with a {@code 100} compression level.
343+
* </p>
344+
*
345+
* <p>
346+
* Note that if an exception occurs when generating the {@link ClipData}, {@code null} is
347+
* returned.
348+
* </p>
349+
*
350+
* <p>
351+
* This method will call {@link PicassoHelper#getImageFromCacheIfPresent(String)} to get the
352+
* thumbnail of the content in the {@link com.squareup.picasso.LruCache LruCache} used by
353+
* the Picasso library inside {@link PicassoHelper}.
354+
* </p>
355+
*
356+
* <p>
357+
* This method has only an effect on the system share sheet (if OEMs didn't change Android
358+
* system standard behavior) on Android API 29 and higher.
359+
* </p>
360+
*
361+
* @param context the context to use
362+
* @param thumbnailUrl the URL of the content thumbnail
363+
* @return a {@link ClipData} of the content thumbnail, or {@code null}
364+
*/
365+
@Nullable
366+
private static ClipData generateClipDataForImagePreview(
367+
@NonNull final Context context,
368+
@NonNull final String thumbnailUrl) {
369+
try {
370+
// URLs in the internal cache finish with \n so we need to add \n to image URLs
371+
final Bitmap bitmap = PicassoHelper.getImageFromCacheIfPresent(thumbnailUrl + "\n");
372+
373+
if (bitmap == null) {
374+
return null;
375+
}
376+
377+
// Save the image in memory to the application's cache because we need a URI to the
378+
// image to generate a ClipData which will show the share sheet, and so an image file
379+
final Context applicationContext = context.getApplicationContext();
380+
final String appFolder = applicationContext.getCacheDir().getAbsolutePath();
381+
final File thumbnailPreviewFile = new File(appFolder
382+
+ "/android_share_sheet_image_preview.jpg");
383+
384+
// Any existing file will be overwritten with FileOutputStream
385+
final FileOutputStream fileOutputStream = new FileOutputStream(thumbnailPreviewFile);
386+
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream);
387+
fileOutputStream.close();
388+
389+
final ClipData clipData = ClipData.newUri(applicationContext.getContentResolver(),
390+
"",
391+
FileProvider.getUriForFile(applicationContext,
392+
BuildConfig.APPLICATION_ID + ".provider",
393+
thumbnailPreviewFile));
394+
if (DEBUG) {
395+
Log.d(TAG, "ClipData successfully generated for Android share sheet: " + clipData);
396+
}
397+
398+
return clipData;
399+
} catch (final Exception e) {
400+
Log.w(TAG, "Error when setting preview image for share sheet", e);
401+
}
402+
403+
return null;
404+
}
304405
}

0 commit comments

Comments
 (0)