|
1 | 1 | package org.schabi.newpipe.util.external_communication; |
2 | 2 |
|
| 3 | +import static org.schabi.newpipe.MainActivity.DEBUG; |
| 4 | + |
3 | 5 | import android.content.ActivityNotFoundException; |
4 | 6 | import android.content.ClipData; |
5 | 7 | import android.content.ClipboardManager; |
6 | 8 | import android.content.Context; |
7 | 9 | import android.content.Intent; |
8 | 10 | import android.content.pm.PackageManager; |
9 | 11 | import android.content.pm.ResolveInfo; |
| 12 | +import android.graphics.Bitmap; |
10 | 13 | import android.net.Uri; |
11 | 14 | import android.os.Build; |
12 | 15 | import android.text.TextUtils; |
| 16 | +import android.util.Log; |
13 | 17 | import android.widget.Toast; |
14 | 18 |
|
15 | 19 | import androidx.annotation.NonNull; |
| 20 | +import androidx.annotation.Nullable; |
16 | 21 | import androidx.core.content.ContextCompat; |
| 22 | +import androidx.core.content.FileProvider; |
17 | 23 |
|
| 24 | +import org.schabi.newpipe.BuildConfig; |
18 | 25 | import org.schabi.newpipe.R; |
| 26 | +import org.schabi.newpipe.util.PicassoHelper; |
| 27 | + |
| 28 | +import java.io.File; |
| 29 | +import java.io.FileOutputStream; |
19 | 30 |
|
20 | 31 | public final class ShareUtils { |
| 32 | + private static final String TAG = ShareUtils.class.getSimpleName(); |
| 33 | + |
21 | 34 | private ShareUtils() { |
22 | 35 | } |
23 | 36 |
|
@@ -252,25 +265,33 @@ public static void shareText(@NonNull final Context context, |
252 | 265 | shareIntent.putExtra(Intent.EXTRA_SUBJECT, title); |
253 | 266 | } |
254 | 267 |
|
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 | + } |
262 | 278 |
|
263 | 279 | openAppChooser(context, shareIntent, false); |
264 | 280 | } |
265 | 281 |
|
266 | 282 | /** |
267 | 283 | * Open the android share sheet to share a content. |
268 | 284 | * |
| 285 | + * <p> |
269 | 286 | * 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 | + * |
271 | 291 | * <p> |
272 | 292 | * This calls {@link #shareText(Context, String, String, String)} with an empty string for the |
273 | | - * imagePreviewUrl parameter. |
| 293 | + * {@code imagePreviewUrl} parameter. |
| 294 | + * </p> |
274 | 295 | * |
275 | 296 | * @param context the context to use |
276 | 297 | * @param title the title of the content |
@@ -301,4 +322,84 @@ public static void copyToClipboard(@NonNull final Context context, final String |
301 | 322 | clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text)); |
302 | 323 | Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show(); |
303 | 324 | } |
| 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 | + } |
304 | 405 | } |
0 commit comments