session-android/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java

229 lines
7.9 KiB
Java
Raw Normal View History

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;
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
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;
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;
import org.session.libsession.utilities.concurrent.SignalExecutors;
2021-05-18 01:12:33 +02:00
import org.session.libsignal.utilities.Log;
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;
import org.thoughtcrime.securesms.net.ContentProxySafetyInterceptor;
2019-01-15 09:41:05 +01:00
import org.thoughtcrime.securesms.net.RequestController;
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;
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;
Add one on one calls over clearnet (#864) * feat: adding basic webrtc deps and test activity * more testing code * feat: add protos and bump version * feat: added basic call functionality * feat: adding UI and flipping cameras * feat: add stats and starting call bottom sheet * feat: hanging up and bottom sheet behaviors should work now * feat: add call stats report on frontend * feat: add relay toggle for answer and offer * fix: add keep screen on and more end call message on back pressed / on finish * refactor: removing and replacing dagger 1 dep with android hilt * feat: include latest proto * feat: update to utilise call ID * feat: add stun and turn * refactor: playing around with deps and transport types * feat: adding call service functionality and permissions for calls * feat: add call manager and more static intent building functions for WebRtcCallService.kt * feat: adding ringers and more audio boilerplate * feat: audio manager call service boilerplate * feat: update kotlin and add in call view model and more management functions * refactor: moving call code around to service and viewmodel interactions * feat: plugging CallManager.kt into view model and service, fixing up dependencies * feat: implementing more WebRtcCallService.kt functions and handlers for actions as well as lifecycle * feat: adding more lifecycle vm and callmanager / call service functionality * feat: adding more command handlers in WebRtcCallService.kt * feat: more commands handled, adding lock manager and bluetooth permissions * feat: adding remainder of basic functionality to services and CallManager.kt * feat: hooking up calls and fixing broken dependencies and compile errors * fix: add timestamp to incoming call * feat: some connection and service launching / ring lifecycle * feat: call establishing and displaying * fix: fixing call connect flows * feat: ringers and better state handling * feat: updating call layout * feat: add fixes to bluetooth and begin the network renegotiation * feat: add call related permissions and more network handover tests * fix: don't display call option in conversation and don't show notification if option not enabled * fix: incoming ringer fix on receiving call, call notification priorities and notification channel update * build: update build number for testing * fix: bluetooth auto-connection and re-connection fixes, removing finished todos, allowing self-send call messages for deduping answers * feat: add pre-offer information and action handling in web rtc call service * refactor: discard offer messages from non-matching pre-offers we are already expecting * build: build numbers and version name update * feat: handle discarding pending calls from linked devices * feat: add signing props to release config build * docs: fix comment on time being 300s (5m) instead of 30s * feat: adding call messages for incoming/outgoing/missed * refactor: handle in-thread call notifications better and replace deny button intent with denyCallIntent instead of hangup * feat: add a hangup via data channel message * feat: process microphone enabled events and remove debuggable from build.gradle * feat: add first call notification * refactor: set the buttons to match iOS in terms of enable disable and colours * refactor: change the call logos in control messages * refactor: more bluetooth improvements * refactor: move start ringer and init of audio manager to CallManager.kt and string fix up * build: remove debuggable for release build * refactor: replace call icons * feat: adding a call time display * refactor: change the call time to update every second * refactor: testing out the full screen intents * refactor: wrapper use corrected session description, set title to recipient displayName, indicate session calls * fix: crash on view with a parent already attached * refactor: aspect ratio fit preserved * refactor: add wantsToAnswer ability in pre-init for fullscreenintent * refactor: prevent calls from non hasSent participants * build: update gradle code * refactor: replace timeout schedule with a seconds count * fix: various bug fixes for calls * fix: remove end call from busy * refactor: use answerCall instead of manual intent building again * build: new version * feat: add silenced notifications for call notification builder. check pre-offer and connecting state for pending connection * build: update build number * fix: text color uses overridden style value * fix: remove wrap content for renderers and look more at recovering from network switches * build: update build number * refactor: remove whitespace * build: update build number * refactor: used shared number for BatchMessageReceiveJob.kt parameter across pollers * fix: glide in update crash * fix: bug fixes for self-send answer / hangup messages * build: update build number * build: update build.gradle number * refactor: compile errors and refactoring to view binding * fix: set the content to binding.root view * build: increase build number * build: update build numbers * feat: adding base for rotation and picking random subset of turn servers * feat: starting the screen rotation processing * feat: setting up rotation for the remote render view * refactor: applying rotation and mirroring based on front / rear cameras that wraps nicely, only scale reworking needed * refactor: calls video stretching but consistent * refactor: state machine and tests for the transition events * feat: new call state processing * refactor: adding reconnecting logic and visuals * feat: state machine reconnect logic wip * feat: add reconnecting and merge fixes * feat: check new session based off current state * feat: reconnection logic works correctly now * refactor: reduce TIMEOUT_SECONDS to 30 from 90 * feat: reset peer connection on DC to prevent ICE messages from old connection or stale state in reconnecting * refactor: add null case * fix: set approved on new outgoing threads, use approved more deeply and invalidate the options menu on recipient modified. Add approvedMe flag toggles for visible message receive * fix: add name update in action bar on modified, change where approvedMe is set * build: increment build number * build: update build number * fix: merge compile errors and increment build number * refactor: remove negotiation based on which party dropped connection * refactor: call reconnection improvement tested cross platform to re-establish * refactor: failed and disconnect events only handled if either the reconnect or the timeout runnables are not set * build: update version number * fix: reduce timeout * fix: fixes the incoming hangup logic for linked devices * refactor: match iOS styling for call activity closer * chore: upgrade build numbers * feat: add in call settings dialog for if calls is disabled in conversation * feat: add a first call missed control message and info popup with link to privacy settings * fix: looking at crash for specific large transaction in NotificationManager * refactor: removing the people in case transaction size reduces to fix notif crash * fix: comment out the entire send multiple to see if it fixes the issue * refactor: revert to including the full notification process in a try/catch to handle weird responses from NotificationManager * fix: add in notification settings prompt for calls and try to fall back to dirty full screen intent / start activity if we're allowed * build: upgrade build number
2022-04-19 06:25:40 +02:00
public LinkPreviewRepository() {
2019-01-15 09:41:05 +01:00
this.client = new OkHttpClient.Builder()
.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;
}
RequestController metadataController;
2019-01-15 09:41:05 +01:00
metadataController = fetchMetadata(url, metadata -> {
if (metadata.isEmpty()) {
callback.onComplete(Optional.absent());
return;
}
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 09:41:05 +01: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).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
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
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();
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())) {
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
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) {
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);
}
}