| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  | package org.thoughtcrime.securesms.linkpreview;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import android.content.Context;
 | 
					
						
							|  |  |  | import android.graphics.Bitmap;
 | 
					
						
							|  |  |  | import android.net.Uri;
 | 
					
						
							|  |  |  | import android.support.annotation.NonNull;
 | 
					
						
							|  |  |  | import android.text.Html;
 | 
					
						
							|  |  |  | import android.text.TextUtils;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import com.bumptech.glide.load.engine.DiskCacheStrategy;
 | 
					
						
							|  |  |  | import com.bumptech.glide.request.FutureTarget;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  | import org.thoughtcrime.securesms.ApplicationContext;
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  | import org.thoughtcrime.securesms.attachments.Attachment;
 | 
					
						
							|  |  |  | import org.thoughtcrime.securesms.attachments.UriAttachment;
 | 
					
						
							|  |  |  | import org.thoughtcrime.securesms.database.AttachmentDatabase;
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  | import org.thoughtcrime.securesms.dependencies.InjectableType;
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  | import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
 | 
					
						
							|  |  |  | import org.thoughtcrime.securesms.logging.Log;
 | 
					
						
							|  |  |  | import org.thoughtcrime.securesms.mms.GlideApp;
 | 
					
						
							|  |  |  | import org.thoughtcrime.securesms.net.CallRequestController;
 | 
					
						
							|  |  |  | import org.thoughtcrime.securesms.net.CompositeRequestController;
 | 
					
						
							| 
									
										
										
										
											2019-05-06 12:25:53 -07:00
										 |  |  | import org.thoughtcrime.securesms.net.ContentProxySafetyInterceptor;
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  | import org.thoughtcrime.securesms.net.ContentProxySelector;
 | 
					
						
							|  |  |  | import org.thoughtcrime.securesms.net.RequestController;
 | 
					
						
							| 
									
										
										
										
											2019-02-25 17:47:30 -08:00
										 |  |  | import org.thoughtcrime.securesms.providers.BlobProvider;
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  | import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
 | 
					
						
							|  |  |  | import org.thoughtcrime.securesms.stickers.StickerUrl;
 | 
					
						
							|  |  |  | import org.thoughtcrime.securesms.util.Hex;
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  | import org.thoughtcrime.securesms.util.MediaUtil;
 | 
					
						
							|  |  |  | import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  | import org.whispersystems.libsignal.InvalidMessageException;
 | 
					
						
							|  |  |  | import org.whispersystems.libsignal.util.Pair;
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  | import org.whispersystems.libsignal.util.guava.Optional;
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  | import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
 | 
					
						
							|  |  |  | import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
 | 
					
						
							|  |  |  | import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest.StickerInfo;
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | import java.io.ByteArrayOutputStream;
 | 
					
						
							|  |  |  | import java.io.IOException;
 | 
					
						
							|  |  |  | import java.util.concurrent.CancellationException;
 | 
					
						
							|  |  |  | import java.util.concurrent.ExecutionException;
 | 
					
						
							|  |  |  | import java.util.regex.Matcher;
 | 
					
						
							|  |  |  | import java.util.regex.Pattern;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  | import javax.inject.Inject;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  | import okhttp3.CacheControl;
 | 
					
						
							|  |  |  | import okhttp3.Call;
 | 
					
						
							|  |  |  | import okhttp3.OkHttpClient;
 | 
					
						
							|  |  |  | import okhttp3.Request;
 | 
					
						
							|  |  |  | import okhttp3.Response;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  | public class LinkPreviewRepository implements InjectableType {
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   private static final String TAG = LinkPreviewRepository.class.getSimpleName();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private static final CacheControl NO_CACHE = new CacheControl.Builder().noCache().build();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private final OkHttpClient client;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  |   @Inject SignalServiceMessageReceiver messageReceiver;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public LinkPreviewRepository(@NonNull Context context) {
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |     this.client = new OkHttpClient.Builder()
 | 
					
						
							|  |  |  |                                   .proxySelector(new ContentProxySelector())
 | 
					
						
							| 
									
										
										
										
											2019-05-06 12:25:53 -07:00
										 |  |  |                                   .addNetworkInterceptor(new ContentProxySafetyInterceptor())
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |                                   .cache(null)
 | 
					
						
							|  |  |  |                                   .build();
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     ApplicationContext.getInstance(context).injectDependencies(this);
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-09 13:53:23 +10:00
										 |  |  |   public RequestController getLinkPreview(@NonNull Context context, @NonNull String url, @NonNull Callback<Optional<LinkPreview>> callback) {
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |     CompositeRequestController compositeController = new CompositeRequestController();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!LinkPreviewUtil.isWhitelistedLinkUrl(url)) {
 | 
					
						
							|  |  |  |       Log.w(TAG, "Tried to get a link preview for a non-whitelisted domain.");
 | 
					
						
							|  |  |  |       callback.onComplete(Optional.absent());
 | 
					
						
							|  |  |  |       return compositeController;
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  |     RequestController metadataController;
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  |     if (StickerUrl.isValidShareLink(url)) {
 | 
					
						
							|  |  |  |       metadataController = fetchStickerPackLinkPreview(context, url, callback);
 | 
					
						
							|  |  |  |     } else {
 | 
					
						
							|  |  |  |       metadataController = fetchMetadata(url, metadata -> {
 | 
					
						
							|  |  |  |         if (metadata.isEmpty()) {
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |           callback.onComplete(Optional.absent());
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  |           return;
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |         }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  |         if (!metadata.getImageUrl().isPresent()) {
 | 
					
						
							|  |  |  |           callback.onComplete(Optional.of(new LinkPreview(url, metadata.getTitle().get(), Optional.absent())));
 | 
					
						
							|  |  |  |           return;
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         RequestController imageController = fetchThumbnail(context, metadata.getImageUrl().get(), attachment -> {
 | 
					
						
							|  |  |  |           if (!metadata.getTitle().isPresent() && !attachment.isPresent()) {
 | 
					
						
							|  |  |  |             callback.onComplete(Optional.absent());
 | 
					
						
							|  |  |  |           } else {
 | 
					
						
							|  |  |  |             callback.onComplete(Optional.of(new LinkPreview(url, metadata.getTitle().or(""), attachment)));
 | 
					
						
							|  |  |  |           }
 | 
					
						
							|  |  |  |         });
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         compositeController.addController(imageController);
 | 
					
						
							|  |  |  |       });
 | 
					
						
							|  |  |  |     }
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     compositeController.addController(metadataController);
 | 
					
						
							|  |  |  |     return compositeController;
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private @NonNull RequestController fetchMetadata(@NonNull String url, Callback<Metadata> callback) {
 | 
					
						
							|  |  |  |     Call call = client.newCall(new Request.Builder().url(url).cacheControl(NO_CACHE).build());
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     call.enqueue(new okhttp3.Callback() {
 | 
					
						
							|  |  |  |       @Override
 | 
					
						
							| 
									
										
										
										
											2019-05-22 13:51:56 -03:00
										 |  |  |       public void onFailure(@NonNull Call call, @NonNull IOException e) {
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |         Log.w(TAG, "Request failed.", e);
 | 
					
						
							|  |  |  |         callback.onComplete(Metadata.empty());
 | 
					
						
							|  |  |  |       }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       @Override
 | 
					
						
							| 
									
										
										
										
											2019-05-22 13:51:56 -03:00
										 |  |  |       public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |         if (!response.isSuccessful()) {
 | 
					
						
							|  |  |  |           Log.w(TAG, "Non-successful response. Code: " + response.code());
 | 
					
						
							|  |  |  |           callback.onComplete(Metadata.empty());
 | 
					
						
							|  |  |  |           return;
 | 
					
						
							|  |  |  |         } else if (response.body() == null) {
 | 
					
						
							|  |  |  |           Log.w(TAG, "No response body.");
 | 
					
						
							|  |  |  |           callback.onComplete(Metadata.empty());
 | 
					
						
							|  |  |  |           return;
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         String            body     = response.body().string();
 | 
					
						
							|  |  |  |         Optional<String>  title    = getProperty(body, "title");
 | 
					
						
							|  |  |  |         Optional<String>  imageUrl = getProperty(body, "image");
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (imageUrl.isPresent() && !LinkPreviewUtil.isWhitelistedMediaUrl(imageUrl.get())) {
 | 
					
						
							|  |  |  |           Log.i(TAG, "Image URL was invalid or for a non-whitelisted domain. Skipping.");
 | 
					
						
							|  |  |  |           imageUrl = Optional.absent();
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         callback.onComplete(new Metadata(title, imageUrl));
 | 
					
						
							|  |  |  |       }
 | 
					
						
							|  |  |  |     });
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return new CallRequestController(call);
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private @NonNull RequestController fetchThumbnail(@NonNull Context context, @NonNull String imageUrl, @NonNull Callback<Optional<Attachment>> callback) {
 | 
					
						
							|  |  |  |     FutureTarget<Bitmap> bitmapFuture = GlideApp.with(context).asBitmap()
 | 
					
						
							|  |  |  |                                                               .load(new ChunkedImageUrl(imageUrl))
 | 
					
						
							|  |  |  |                                                               .skipMemoryCache(true)
 | 
					
						
							|  |  |  |                                                               .diskCacheStrategy(DiskCacheStrategy.NONE)
 | 
					
						
							| 
									
										
										
										
											2019-03-28 15:14:06 -03:00
										 |  |  |                                                               .centerInside()
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |                                                               .submit(1024, 1024);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     RequestController controller = () -> bitmapFuture.cancel(false);
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  |     SignalExecutors.UNBOUNDED.execute(() -> {
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |       try {
 | 
					
						
							|  |  |  |         Bitmap                bitmap = bitmapFuture.get();
 | 
					
						
							|  |  |  |         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         byte[]               bytes     = baos.toByteArray();
 | 
					
						
							| 
									
										
										
										
											2019-02-25 17:47:30 -08:00
										 |  |  |         Uri                  uri       = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory();
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |         Optional<Attachment> thumbnail = Optional.of(new UriAttachment(uri,
 | 
					
						
							|  |  |  |                                                                        uri,
 | 
					
						
							|  |  |  |                                                                        MediaUtil.IMAGE_JPEG,
 | 
					
						
							|  |  |  |                                                                        AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
 | 
					
						
							|  |  |  |                                                                        bytes.length,
 | 
					
						
							|  |  |  |                                                                        bitmap.getWidth(),
 | 
					
						
							|  |  |  |                                                                        bitmap.getHeight(),
 | 
					
						
							|  |  |  |                                                                        null,
 | 
					
						
							|  |  |  |                                                                        null,
 | 
					
						
							|  |  |  |                                                                        false,
 | 
					
						
							|  |  |  |                                                                        false,
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  |                                                                        null,
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |                                                                        null));
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         callback.onComplete(thumbnail);
 | 
					
						
							|  |  |  |       } catch (CancellationException | ExecutionException | InterruptedException e) {
 | 
					
						
							|  |  |  |         controller.cancel();
 | 
					
						
							|  |  |  |         callback.onComplete(Optional.absent());
 | 
					
						
							|  |  |  |       } finally {
 | 
					
						
							|  |  |  |         bitmapFuture.cancel(false);
 | 
					
						
							|  |  |  |       }
 | 
					
						
							|  |  |  |     });
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return () -> bitmapFuture.cancel(true);
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private @NonNull Optional<String> getProperty(@NonNull String searchText, @NonNull String property) {
 | 
					
						
							| 
									
										
										
										
											2019-04-17 09:35:47 -04:00
										 |  |  |     Pattern pattern = Pattern.compile("<\\s*meta\\s+property\\s*=\\s*\"\\s*og:" + property + "\\s*\"\\s+[^>]*content\\s*=\\s*\"(.*?)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |     Matcher matcher = pattern.matcher(searchText);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (matcher.find()) {
 | 
					
						
							|  |  |  |       String text = Html.fromHtml(matcher.group(1)).toString();
 | 
					
						
							|  |  |  |       return TextUtils.isEmpty(text) ? Optional.absent() : Optional.of(text);
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return Optional.absent();
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 10:21:30 -04:00
										 |  |  |   private RequestController fetchStickerPackLinkPreview(@NonNull Context context,
 | 
					
						
							|  |  |  |                                                         @NonNull String packUrl,
 | 
					
						
							|  |  |  |                                                         @NonNull Callback<Optional<LinkPreview>> callback)
 | 
					
						
							|  |  |  |   {
 | 
					
						
							|  |  |  |     SignalExecutors.UNBOUNDED.execute(() -> {
 | 
					
						
							|  |  |  |       try {
 | 
					
						
							|  |  |  |         Pair<String, String> stickerParams = StickerUrl.parseShareLink(packUrl).or(new Pair<>("", ""));
 | 
					
						
							|  |  |  |         String               packIdString  = stickerParams.first();
 | 
					
						
							|  |  |  |         String               packKeyString = stickerParams.second();
 | 
					
						
							|  |  |  |         byte[]               packIdBytes   = Hex.fromStringCondensed(packIdString);
 | 
					
						
							|  |  |  |         byte[]               packKeyBytes  = Hex.fromStringCondensed(packKeyString);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         SignalServiceStickerManifest manifest = messageReceiver.retrieveStickerManifest(packIdBytes, packKeyBytes);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         String                title        = manifest.getTitle().or(manifest.getAuthor()).or("");
 | 
					
						
							|  |  |  |         Optional<StickerInfo> firstSticker = Optional.fromNullable(manifest.getStickers().size() > 0 ? manifest.getStickers().get(0) : null);
 | 
					
						
							|  |  |  |         Optional<StickerInfo> cover        = manifest.getCover().or(firstSticker);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (cover.isPresent()) {
 | 
					
						
							|  |  |  |           Bitmap bitmap = GlideApp.with(context).asBitmap()
 | 
					
						
							|  |  |  |                                                 .load(new StickerRemoteUri(packIdString, packKeyString, cover.get().getId()))
 | 
					
						
							|  |  |  |                                                 .skipMemoryCache(true)
 | 
					
						
							|  |  |  |                                                 .diskCacheStrategy(DiskCacheStrategy.NONE)
 | 
					
						
							|  |  |  |                                                 .centerInside()
 | 
					
						
							|  |  |  |                                                 .submit(512, 512)
 | 
					
						
							|  |  |  |                                                 .get();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           ByteArrayOutputStream baos = new ByteArrayOutputStream();
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           bitmap.compress(Bitmap.CompressFormat.WEBP, 80, baos);
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           byte[]               bytes     = baos.toByteArray();
 | 
					
						
							|  |  |  |           Uri                  uri       = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory();
 | 
					
						
							|  |  |  |           Optional<Attachment> thumbnail = Optional.of(new UriAttachment(uri,
 | 
					
						
							|  |  |  |                                                        uri,
 | 
					
						
							|  |  |  |                                                        MediaUtil.IMAGE_WEBP,
 | 
					
						
							|  |  |  |                                                        AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
 | 
					
						
							|  |  |  |                                                        bytes.length,
 | 
					
						
							|  |  |  |                                                        bitmap.getWidth(),
 | 
					
						
							|  |  |  |                                                        bitmap.getHeight(),
 | 
					
						
							|  |  |  |                                                        null,
 | 
					
						
							|  |  |  |                                                        null,
 | 
					
						
							|  |  |  |                                                        false,
 | 
					
						
							|  |  |  |                                                        false,
 | 
					
						
							|  |  |  |                                                        null,
 | 
					
						
							|  |  |  |                                                        null));
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           callback.onComplete(Optional.of(new LinkPreview(packUrl, title, thumbnail)));
 | 
					
						
							|  |  |  |         } else {
 | 
					
						
							|  |  |  |           callback.onComplete(Optional.absent());
 | 
					
						
							|  |  |  |         }
 | 
					
						
							|  |  |  |       } catch (IOException | InvalidMessageException | ExecutionException | InterruptedException e) {
 | 
					
						
							|  |  |  |         Log.w(TAG, "Failed to fetch sticker pack link preview.");
 | 
					
						
							|  |  |  |         callback.onComplete(Optional.absent());
 | 
					
						
							|  |  |  |       }
 | 
					
						
							|  |  |  |     });
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return () -> Log.i(TAG, "Cancelled sticker pack link preview fetch -- no effect.");
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |   private static class Metadata {
 | 
					
						
							|  |  |  |     private final Optional<String> title;
 | 
					
						
							|  |  |  |     private final Optional<String> imageUrl;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Metadata(Optional<String> title, Optional<String> imageUrl) {
 | 
					
						
							|  |  |  |       this.title    = title;
 | 
					
						
							|  |  |  |       this.imageUrl = imageUrl;
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     static Metadata empty() {
 | 
					
						
							|  |  |  |       return new Metadata(Optional.absent(), Optional.absent());
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Optional<String> getTitle() {
 | 
					
						
							|  |  |  |       return title;
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Optional<String> getImageUrl() {
 | 
					
						
							|  |  |  |       return imageUrl;
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     boolean isEmpty() {
 | 
					
						
							|  |  |  |       return !title.isPresent() && !imageUrl.isPresent();
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-09 13:53:23 +10:00
										 |  |  |   public interface Callback<T> {
 | 
					
						
							| 
									
										
										
										
											2019-01-15 00:41:05 -08:00
										 |  |  |     void onComplete(@NonNull T result);
 | 
					
						
							|  |  |  |   }
 | 
					
						
							|  |  |  | }
 |