2019-01-15 09:41:05 +01:00
|
|
|
package org.thoughtcrime.securesms.linkpreview;
|
|
|
|
|
|
|
|
import android.content.Context;
|
|
|
|
import android.graphics.Bitmap;
|
2020-11-06 04:50:00 +01:00
|
|
|
import android.graphics.BitmapFactory;
|
2019-01-15 09:41:05 +01:00
|
|
|
import android.net.Uri;
|
2021-10-04 09:51:19 +02:00
|
|
|
|
2020-08-19 02:06:26 +02:00
|
|
|
import androidx.annotation.NonNull;
|
2020-11-06 04:50:00 +01:00
|
|
|
import androidx.annotation.Nullable;
|
2019-01-15 09:41:05 +01:00
|
|
|
|
2020-11-06 04:50:00 +01:00
|
|
|
import com.google.android.gms.common.util.IOUtils;
|
2019-01-15 09:41:05 +01:00
|
|
|
|
2021-10-04 09:51:19 +02:00
|
|
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
2021-03-02 02:24:09 +01:00
|
|
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
2021-10-04 09:51:19 +02:00
|
|
|
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
|
|
|
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
2021-02-01 07:00:32 +01:00
|
|
|
import org.session.libsession.utilities.MediaTypes;
|
2021-10-04 09:51:19 +02:00
|
|
|
import org.session.libsession.utilities.concurrent.SignalExecutors;
|
2021-05-18 01:12:33 +02:00
|
|
|
import org.session.libsignal.utilities.Log;
|
2021-10-04 09:51:19 +02:00
|
|
|
import org.session.libsignal.utilities.guava.Optional;
|
|
|
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil.OpenGraph;
|
2019-01-15 09:41:05 +01:00
|
|
|
import org.thoughtcrime.securesms.net.CallRequestController;
|
|
|
|
import org.thoughtcrime.securesms.net.CompositeRequestController;
|
2019-05-06 21:25:53 +02:00
|
|
|
import org.thoughtcrime.securesms.net.ContentProxySafetyInterceptor;
|
2019-01-15 09:41:05 +01:00
|
|
|
import org.thoughtcrime.securesms.net.RequestController;
|
2019-02-26 02:47:30 +01:00
|
|
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
2021-01-15 05:36:30 +01:00
|
|
|
|
2019-01-15 09:41:05 +01:00
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
|
import java.io.IOException;
|
2020-11-06 04:50:00 +01:00
|
|
|
import java.io.InputStream;
|
2019-01-15 09:41:05 +01:00
|
|
|
|
|
|
|
import okhttp3.CacheControl;
|
|
|
|
import okhttp3.Call;
|
|
|
|
import okhttp3.OkHttpClient;
|
|
|
|
import okhttp3.Request;
|
|
|
|
import okhttp3.Response;
|
|
|
|
|
2021-10-04 09:51:19 +02:00
|
|
|
public class LinkPreviewRepository {
|
2019-01-15 09:41:05 +01: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 16:21:30 +02:00
|
|
|
public LinkPreviewRepository(@NonNull Context context) {
|
2019-01-15 09:41:05 +01:00
|
|
|
this.client = new OkHttpClient.Builder()
|
2019-05-06 21:25:53 +02:00
|
|
|
.addNetworkInterceptor(new ContentProxySafetyInterceptor())
|
2019-01-15 09:41:05 +01:00
|
|
|
.cache(null)
|
|
|
|
.build();
|
|
|
|
}
|
|
|
|
|
2020-05-11 08:54:31 +02:00
|
|
|
RequestController getLinkPreview(@NonNull Context context, @NonNull String url, @NonNull Callback<Optional<LinkPreview>> callback) {
|
2019-01-15 09:41:05 +01:00
|
|
|
CompositeRequestController compositeController = new CompositeRequestController();
|
|
|
|
|
2020-11-06 04:50:00 +01:00
|
|
|
if (!LinkPreviewUtil.isValidLinkUrl(url)) {
|
2019-01-15 09:41:05 +01:00
|
|
|
Log.w(TAG, "Tried to get a link preview for a non-whitelisted domain.");
|
|
|
|
callback.onComplete(Optional.absent());
|
|
|
|
return compositeController;
|
|
|
|
}
|
|
|
|
|
2019-04-17 16:21:30 +02:00
|
|
|
RequestController metadataController;
|
2019-01-15 09:41:05 +01:00
|
|
|
|
2021-02-22 00:06:40 +01:00
|
|
|
metadataController = fetchMetadata(url, metadata -> {
|
|
|
|
if (metadata.isEmpty()) {
|
|
|
|
callback.onComplete(Optional.absent());
|
|
|
|
return;
|
|
|
|
}
|
2019-04-17 16:21:30 +02:00
|
|
|
|
2021-02-22 00:06:40 +01:00
|
|
|
if (!metadata.getImageUrl().isPresent()) {
|
|
|
|
callback.onComplete(Optional.of(new LinkPreview(url, metadata.getTitle().get(), Optional.absent())));
|
|
|
|
return;
|
|
|
|
}
|
2019-04-17 16:21:30 +02:00
|
|
|
|
2021-02-22 00:06:40 +01:00
|
|
|
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)));
|
|
|
|
}
|
2019-04-17 16:21:30 +02:00
|
|
|
});
|
2021-02-22 00:06:40 +01:00
|
|
|
|
|
|
|
compositeController.addController(imageController);
|
|
|
|
});
|
2019-01-15 09:41:05 +01:00
|
|
|
|
|
|
|
compositeController.addController(metadataController);
|
|
|
|
return compositeController;
|
|
|
|
}
|
|
|
|
|
|
|
|
private @NonNull RequestController fetchMetadata(@NonNull String url, Callback<Metadata> callback) {
|
2020-11-18 05:17:38 +01:00
|
|
|
Call call = client.newCall(new Request.Builder().url(url).removeHeader("User-Agent").addHeader("User-Agent",
|
2021-06-24 07:46:36 +02:00
|
|
|
"WhatsApp").cacheControl(NO_CACHE).build());
|
2019-01-15 09:41:05 +01:00
|
|
|
|
|
|
|
call.enqueue(new okhttp3.Callback() {
|
|
|
|
@Override
|
2019-05-22 18:51:56 +02:00
|
|
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
2019-01-15 09:41:05 +01:00
|
|
|
Log.w(TAG, "Request failed.", e);
|
|
|
|
callback.onComplete(Metadata.empty());
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2019-05-22 18:51:56 +02:00
|
|
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
|
2019-01-15 09:41:05 +01: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();
|
2020-11-18 05:17:38 +01:00
|
|
|
OpenGraph openGraph = LinkPreviewUtil.parseOpenGraphFields(body);
|
|
|
|
Optional<String> title = openGraph.getTitle();
|
|
|
|
Optional<String> imageUrl = openGraph.getImageUrl();
|
2019-01-15 09:41:05 +01:00
|
|
|
|
2020-11-06 04:50:00 +01:00
|
|
|
if (imageUrl.isPresent() && !LinkPreviewUtil.isValidMediaUrl(imageUrl.get())) {
|
2019-01-15 09:41:05 +01:00
|
|
|
Log.i(TAG, "Image URL was invalid or for a non-whitelisted domain. Skipping.");
|
|
|
|
imageUrl = Optional.absent();
|
|
|
|
}
|
|
|
|
|
2021-02-02 01:29:51 +01:00
|
|
|
if (imageUrl.isPresent() && !LinkPreviewUtil.isValidMimeType(imageUrl.get())) {
|
2020-11-23 02:00:18 +01:00
|
|
|
Log.i(TAG, "Image URL was invalid mime type. Skipping.");
|
|
|
|
imageUrl = Optional.absent();
|
|
|
|
}
|
|
|
|
|
2019-01-15 09:41:05 +01:00
|
|
|
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) {
|
2020-11-06 04:50:00 +01:00
|
|
|
Call call = client.newCall(new Request.Builder().url(imageUrl).build());
|
|
|
|
CallRequestController controller = new CallRequestController(call);
|
2019-01-15 09:41:05 +01:00
|
|
|
|
2019-04-17 16:21:30 +02:00
|
|
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
2019-01-15 09:41:05 +01:00
|
|
|
try {
|
2020-11-06 04:50:00 +01:00
|
|
|
Response response = call.execute();
|
|
|
|
if (!response.isSuccessful() || response.body() == null) {
|
2020-11-18 05:17:38 +01:00
|
|
|
controller.cancel();
|
|
|
|
callback.onComplete(Optional.absent());
|
2020-11-06 04:50:00 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
InputStream bodyStream = response.body().byteStream();
|
|
|
|
controller.setStream(bodyStream);
|
|
|
|
|
|
|
|
byte[] data = IOUtils.readInputStreamFully(bodyStream);
|
|
|
|
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
|
2021-02-01 07:00:32 +01:00
|
|
|
Optional<Attachment> thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.JPEG, MediaTypes.IMAGE_JPEG);
|
2020-11-06 04:50:00 +01:00
|
|
|
|
|
|
|
if (bitmap != null) bitmap.recycle();
|
2019-01-15 09:41:05 +01:00
|
|
|
|
|
|
|
callback.onComplete(thumbnail);
|
2020-11-06 04:50:00 +01:00
|
|
|
} catch (IOException e) {
|
|
|
|
Log.w(TAG, "Exception during link preview image retrieval.", e);
|
2019-01-15 09:41:05 +01:00
|
|
|
controller.cancel();
|
|
|
|
callback.onComplete(Optional.absent());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-11-06 04:50:00 +01:00
|
|
|
return controller;
|
2019-01-15 09:41:05 +01:00
|
|
|
}
|
|
|
|
|
2020-11-06 04:50:00 +01:00
|
|
|
private static Optional<Attachment> bitmapToAttachment(@Nullable Bitmap bitmap,
|
|
|
|
@NonNull Bitmap.CompressFormat format,
|
|
|
|
@NonNull String contentType)
|
|
|
|
{
|
|
|
|
if (bitmap == null) {
|
|
|
|
return Optional.absent();
|
|
|
|
}
|
|
|
|
|
|
|
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
|
|
|
|
|
|
bitmap.compress(format, 80, baos);
|
|
|
|
|
|
|
|
byte[] bytes = baos.toByteArray();
|
|
|
|
Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory();
|
|
|
|
|
2021-06-24 07:46:36 +02:00
|
|
|
return Optional.of(new UriAttachment(uri,
|
|
|
|
uri,
|
|
|
|
contentType,
|
|
|
|
AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED,
|
|
|
|
bytes.length,
|
|
|
|
bitmap.getWidth(),
|
|
|
|
bitmap.getHeight(),
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
false,
|
|
|
|
false,
|
|
|
|
null));
|
2020-11-06 04:50:00 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-01-15 09:41:05 +01: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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-11 08:54:31 +02:00
|
|
|
interface Callback<T> {
|
2019-01-15 09:41:05 +01:00
|
|
|
void onComplete(@NonNull T result);
|
|
|
|
}
|
|
|
|
}
|