mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-22 16:07:30 +00:00
Merge pull request #382 from RyanRory/link-preview-improvement
Link preview improvement
This commit is contained in:
commit
6140b8bb20
@ -79,10 +79,10 @@
|
|||||||
android:layout_marginEnd="6dp"
|
android:layout_marginEnd="6dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:src="@drawable/ic_close_white_18dp"
|
android:src="@drawable/ic_close_white_18dp"
|
||||||
android:tint="@color/gray70"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="@color/gray70"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<com.github.ybq.android.spinkit.SpinKitView
|
<com.github.ybq.android.spinkit.SpinKitView
|
||||||
|
@ -100,6 +100,16 @@ public class LinkPreviewView extends FrameLayout {
|
|||||||
site.setVisibility(GONE);
|
site.setVisibility(GONE);
|
||||||
thumbnail.setVisibility(GONE);
|
thumbnail.setVisibility(GONE);
|
||||||
spinner.setVisibility(VISIBLE);
|
spinner.setVisibility(VISIBLE);
|
||||||
|
closeButton.setVisibility(GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showCloseButton) {
|
||||||
|
setLinkPreview(glideRequests, linkPreview, showThumbnail);
|
||||||
|
if (showCloseButton) {
|
||||||
|
closeButton.setVisibility(VISIBLE);
|
||||||
|
} else {
|
||||||
|
closeButton.setVisibility(GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
||||||
@ -107,6 +117,7 @@ public class LinkPreviewView extends FrameLayout {
|
|||||||
site.setVisibility(VISIBLE);
|
site.setVisibility(VISIBLE);
|
||||||
thumbnail.setVisibility(VISIBLE);
|
thumbnail.setVisibility(VISIBLE);
|
||||||
spinner.setVisibility(GONE);
|
spinner.setVisibility(GONE);
|
||||||
|
closeButton.setVisibility(VISIBLE);
|
||||||
|
|
||||||
title.setText(linkPreview.getTitle());
|
title.setText(linkPreview.getTitle());
|
||||||
|
|
||||||
|
@ -583,7 +583,7 @@ public class ConversationItem extends TapJackingProofLinearLayout
|
|||||||
mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener);
|
mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener);
|
||||||
mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener);
|
mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener);
|
||||||
|
|
||||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, false);
|
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, false, false);
|
||||||
|
|
||||||
setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
|
setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
|
||||||
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, true);
|
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, true);
|
||||||
@ -591,7 +591,7 @@ public class ConversationItem extends TapJackingProofLinearLayout
|
|||||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
} else {
|
} else {
|
||||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true);
|
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true, false);
|
||||||
linkPreviewStub.get().setDownloadClickedListener(downloadClickListener);
|
linkPreviewStub.get().setDownloadClickedListener(downloadClickListener);
|
||||||
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, false);
|
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, false);
|
||||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
@ -601,7 +601,6 @@ public class ConversationItem extends TapJackingProofLinearLayout
|
|||||||
linkPreviewStub.get().setOnClickListener(linkPreviewClickListener);
|
linkPreviewStub.get().setOnClickListener(linkPreviewClickListener);
|
||||||
linkPreviewStub.get().setOnLongClickListener(passthroughClickListener);
|
linkPreviewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||||
|
|
||||||
|
|
||||||
footer.setVisibility(VISIBLE);
|
footer.setVisibility(VISIBLE);
|
||||||
} else if (hasAudio(messageRecord)) {
|
} else if (hasAudio(messageRecord)) {
|
||||||
audioViewStub.get().setVisibility(View.VISIBLE);
|
audioViewStub.get().setVisibility(View.VISIBLE);
|
||||||
|
@ -10,7 +10,6 @@ import android.text.Html;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
import com.bumptech.glide.request.FutureTarget;
|
|
||||||
import com.google.android.gms.common.util.IOUtils;
|
import com.google.android.gms.common.util.IOUtils;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
@ -18,7 +17,6 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
|||||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.net.CallRequestController;
|
import org.thoughtcrime.securesms.net.CallRequestController;
|
||||||
@ -37,6 +35,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
|
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest.StickerInfo;
|
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest.StickerInfo;
|
||||||
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil.OpenGraph;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -116,7 +115,8 @@ public class LinkPreviewRepository implements InjectableType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull RequestController fetchMetadata(@NonNull String url, Callback<Metadata> callback) {
|
private @NonNull RequestController fetchMetadata(@NonNull String url, Callback<Metadata> callback) {
|
||||||
Call call = client.newCall(new Request.Builder().url(url).cacheControl(NO_CACHE).build());
|
Call call = client.newCall(new Request.Builder().url(url).removeHeader("User-Agent").addHeader("User-Agent",
|
||||||
|
"WhatsApp").cacheControl(NO_CACHE).build());
|
||||||
|
|
||||||
call.enqueue(new okhttp3.Callback() {
|
call.enqueue(new okhttp3.Callback() {
|
||||||
@Override
|
@Override
|
||||||
@ -138,14 +138,20 @@ public class LinkPreviewRepository implements InjectableType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String body = response.body().string();
|
String body = response.body().string();
|
||||||
Optional<String> title = getProperty(body, "title");
|
OpenGraph openGraph = LinkPreviewUtil.parseOpenGraphFields(body);
|
||||||
Optional<String> imageUrl = getProperty(body, "image");
|
Optional<String> title = openGraph.getTitle();
|
||||||
|
Optional<String> imageUrl = openGraph.getImageUrl();
|
||||||
|
|
||||||
if (imageUrl.isPresent() && !LinkPreviewUtil.isValidMediaUrl(imageUrl.get())) {
|
if (imageUrl.isPresent() && !LinkPreviewUtil.isValidMediaUrl(imageUrl.get())) {
|
||||||
Log.i(TAG, "Image URL was invalid or for a non-whitelisted domain. Skipping.");
|
Log.i(TAG, "Image URL was invalid or for a non-whitelisted domain. Skipping.");
|
||||||
imageUrl = Optional.absent();
|
imageUrl = Optional.absent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageUrl.isPresent() && !LinkPreviewUtil.isVaildMimeType(imageUrl.get())) {
|
||||||
|
Log.i(TAG, "Image URL was invalid mime type. Skipping.");
|
||||||
|
imageUrl = Optional.absent();
|
||||||
|
}
|
||||||
|
|
||||||
callback.onComplete(new Metadata(title, imageUrl));
|
callback.onComplete(new Metadata(title, imageUrl));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -161,6 +167,8 @@ public class LinkPreviewRepository implements InjectableType {
|
|||||||
try {
|
try {
|
||||||
Response response = call.execute();
|
Response response = call.execute();
|
||||||
if (!response.isSuccessful() || response.body() == null) {
|
if (!response.isSuccessful() || response.body() == null) {
|
||||||
|
controller.cancel();
|
||||||
|
callback.onComplete(Optional.absent());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,24 +192,6 @@ public class LinkPreviewRepository implements InjectableType {
|
|||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull Optional<String> getProperty(@NonNull String searchText, @NonNull String property) {
|
|
||||||
Pattern pattern = Pattern.compile("<\\s*meta\\s+property\\s*=\\s*\"\\s*og:" + property + "\\s*\"\\s+[^>]*content\\s*=\\s*\"(.*?)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
|
||||||
Matcher matcher = pattern.matcher(searchText);
|
|
||||||
if (matcher.find()) {
|
|
||||||
String text = Html.fromHtml(matcher.group(1)).toString();
|
|
||||||
if (!TextUtils.isEmpty(text)) { return Optional.of(text); }
|
|
||||||
}
|
|
||||||
|
|
||||||
pattern = Pattern.compile("<\\s*" + property + "[^>]*>(.*?)<\\s*/" + property + "[^>]*>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
|
||||||
matcher = pattern.matcher(searchText);
|
|
||||||
if (matcher.find()) {
|
|
||||||
String text = Html.fromHtml(matcher.group(1)).toString();
|
|
||||||
if (!TextUtils.isEmpty(text)) { return Optional.of(text); }
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.absent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private RequestController fetchStickerPackLinkPreview(@NonNull Context context,
|
private RequestController fetchStickerPackLinkPreview(@NonNull Context context,
|
||||||
@NonNull String packUrl,
|
@NonNull String packUrl,
|
||||||
@NonNull Callback<Optional<LinkPreview>> callback)
|
@NonNull Callback<Optional<LinkPreview>> callback)
|
||||||
|
@ -2,6 +2,9 @@ package org.thoughtcrime.securesms.linkpreview;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.text.Html;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.URLSpan;
|
import android.text.style.URLSpan;
|
||||||
@ -10,9 +13,15 @@ import android.text.util.Linkify;
|
|||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
||||||
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@ -20,10 +29,15 @@ import okhttp3.HttpUrl;
|
|||||||
|
|
||||||
public final class LinkPreviewUtil {
|
public final class LinkPreviewUtil {
|
||||||
|
|
||||||
private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$");
|
private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$");
|
private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$");
|
private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern STICKER_URL_PATTERN = Pattern.compile("^.*#pack_id=(.*)&pack_key=(.*)$");
|
private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return All whitelisted URLs in the source text.
|
* @return All whitelisted URLs in the source text.
|
||||||
@ -82,4 +96,135 @@ public final class LinkPreviewUtil {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isVaildMimeType(@NonNull String url) {
|
||||||
|
String[] vaildMimeType = {"jpg", "png", "gif", "jpeg"};
|
||||||
|
if (url.contains(".")) {
|
||||||
|
String extenstion = url.substring(url.lastIndexOf(".") + 1).toLowerCase();
|
||||||
|
return Arrays.asList(vaildMimeType).contains(extenstion);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) {
|
||||||
|
return parseOpenGraphFields(html, text -> Html.fromHtml(text).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html, @NonNull HtmlDecoder htmlDecoder) {
|
||||||
|
if (html == null) {
|
||||||
|
return new OpenGraph(Collections.emptyMap(), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> openGraphTags = new HashMap<>();
|
||||||
|
Matcher openGraphMatcher = OPEN_GRAPH_TAG_PATTERN.matcher(html);
|
||||||
|
|
||||||
|
while (openGraphMatcher.find()) {
|
||||||
|
String tag = openGraphMatcher.group();
|
||||||
|
String property = openGraphMatcher.groupCount() > 0 ? openGraphMatcher.group(1) : null;
|
||||||
|
|
||||||
|
if (property != null) {
|
||||||
|
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
|
||||||
|
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
|
||||||
|
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
|
||||||
|
openGraphTags.put(property.toLowerCase(), content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher articleMatcher = ARTICLE_TAG_PATTERN.matcher(html);
|
||||||
|
|
||||||
|
while (articleMatcher.find()) {
|
||||||
|
String tag = articleMatcher.group();
|
||||||
|
String property = articleMatcher.groupCount() > 0 ? articleMatcher.group(1) : null;
|
||||||
|
|
||||||
|
if (property != null) {
|
||||||
|
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
|
||||||
|
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
|
||||||
|
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
|
||||||
|
openGraphTags.put(property.toLowerCase(), content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String htmlTitle = "";
|
||||||
|
String faviconUrl = "";
|
||||||
|
|
||||||
|
Matcher titleMatcher = TITLE_PATTERN.matcher(html);
|
||||||
|
if (titleMatcher.find() && titleMatcher.groupCount() > 0) {
|
||||||
|
htmlTitle = htmlDecoder.fromEncoded(titleMatcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher faviconMatcher = FAVICON_PATTERN.matcher(html);
|
||||||
|
if (faviconMatcher.find()) {
|
||||||
|
Matcher faviconHrefMatcher = FAVICON_HREF_PATTERN.matcher(faviconMatcher.group());
|
||||||
|
if (faviconHrefMatcher.find() && faviconHrefMatcher.groupCount() > 0) {
|
||||||
|
faviconUrl = faviconHrefMatcher.group(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OpenGraph(openGraphTags, htmlTitle, faviconUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable String parseTopLevelDomain(@NonNull String domain) {
|
||||||
|
int periodIndex = domain.lastIndexOf(".");
|
||||||
|
|
||||||
|
if (periodIndex >= 0 && periodIndex < domain.length() - 1) {
|
||||||
|
return domain.substring(periodIndex + 1);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static final class OpenGraph {
|
||||||
|
|
||||||
|
private final Map<String, String> values;
|
||||||
|
|
||||||
|
private final @Nullable String htmlTitle;
|
||||||
|
private final @Nullable String faviconUrl;
|
||||||
|
|
||||||
|
private static final String KEY_TITLE = "title";
|
||||||
|
private static final String KEY_DESCRIPTION_URL = "description";
|
||||||
|
private static final String KEY_IMAGE_URL = "image";
|
||||||
|
private static final String KEY_PUBLISHED_TIME_1 = "published_time";
|
||||||
|
private static final String KEY_PUBLISHED_TIME_2 = "article:published_time";
|
||||||
|
private static final String KEY_MODIFIED_TIME_1 = "modified_time";
|
||||||
|
private static final String KEY_MODIFIED_TIME_2 = "article:modified_time";
|
||||||
|
|
||||||
|
public OpenGraph(@NonNull Map<String, String> values, @Nullable String htmlTitle, @Nullable String faviconUrl) {
|
||||||
|
this.values = values;
|
||||||
|
this.htmlTitle = htmlTitle;
|
||||||
|
this.faviconUrl = faviconUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull Optional<String> getTitle() {
|
||||||
|
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_TITLE), htmlTitle));
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull Optional<String> getImageUrl() {
|
||||||
|
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_IMAGE_URL), faviconUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
public long getDate() {
|
||||||
|
return Stream.of(values.get(KEY_PUBLISHED_TIME_1),
|
||||||
|
values.get(KEY_PUBLISHED_TIME_2),
|
||||||
|
values.get(KEY_MODIFIED_TIME_1),
|
||||||
|
values.get(KEY_MODIFIED_TIME_2))
|
||||||
|
.map(DateUtils::parseIso8601)
|
||||||
|
.filter(time -> time > 0)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull
|
||||||
|
Optional<String> getDescription() {
|
||||||
|
return Optional.of(values.get(KEY_DESCRIPTION_URL));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface HtmlDecoder {
|
||||||
|
@NonNull String fromEncoded(@NonNull String html);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,17 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.util;
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
import android.text.format.DateFormat;
|
import android.text.format.DateFormat;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@ -142,4 +149,33 @@ public class DateUtils extends android.text.format.DateUtils {
|
|||||||
private static String getLocalizedPattern(String template, Locale locale) {
|
private static String getLocalizedPattern(String template, Locale locale) {
|
||||||
return DateFormat.getBestDateTimePattern(locale, template);
|
return DateFormat.getBestDateTimePattern(locale, template);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* e.g. 2020-09-04T19:17:51Z
|
||||||
|
* https://www.iso.org/iso-8601-date-and-time-format.html
|
||||||
|
*
|
||||||
|
* Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences.
|
||||||
|
*
|
||||||
|
* @return The timestamp if able to be parsed, otherwise -1.
|
||||||
|
*/
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
public static long parseIso8601(@Nullable String date) {
|
||||||
|
SimpleDateFormat format;
|
||||||
|
if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) {
|
||||||
|
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault());
|
||||||
|
} else {
|
||||||
|
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.isEmpty()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return format.parse(date).getTime();
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.w(TAG, "Failed to parse date.", e);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user