mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-19 19:38:45 +00:00
Added support for link previews.
This commit is contained in:
parent
bef9beff16
commit
c76081d99c
13
build.gradle
13
build.gradle
@ -73,6 +73,7 @@ dependencies {
|
||||
compile "com.android.support:preference-v14:$supportVersion"
|
||||
compile "com.android.support:gridlayout-v7:$supportVersion"
|
||||
compile "com.android.support:exifinterface:$supportVersion"
|
||||
compile 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||
compile 'com.android.support:multidex:1.0.3'
|
||||
compile 'android.arch.lifecycle:extensions:1.1.1'
|
||||
compile 'android.arch.lifecycle:common-java8:1.1.1'
|
||||
@ -86,7 +87,7 @@ dependencies {
|
||||
compile 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||
compile 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||
|
||||
compile 'org.whispersystems:signal-service-android:2.12.5'
|
||||
compile 'org.whispersystems:signal-service-android:2.12.7'
|
||||
compile 'org.whispersystems:webrtc-android:M69'
|
||||
|
||||
compile "me.leolin:ShortcutBadger:1.1.16"
|
||||
@ -172,6 +173,7 @@ dependencyVerification {
|
||||
'com.android.support:cardview-v7:bc9e6b0e06ce1205f1db34f0e6193019613d19cfeb54cdccea722340d1c60f26',
|
||||
'com.android.support:gridlayout-v7:5029529f7db66f8773426bf7318645f0840fc50d74f66355cd60c5e58d2da087',
|
||||
'com.android.support:exifinterface:bbf44e519edd6333a24a3285aa21fd00181b920b81ca8aa89a8899f03ab4d6b0',
|
||||
'com.android.support.constraint:constraint-layout:27b4e5c0b80d3ff8b92f4c93b3b4d3ecf16c01589f4cdf70ca7cf64cb42d8122',
|
||||
'com.android.support:multidex:ecf6098572e23b5155bab3b9a82b2fd1530eda6c6c157745e0f5287c66eec60c',
|
||||
'android.arch.work:work-runtime:810fba0ee8fc58560664b58c6dba532eae05e3d196e9ee5ae78c1f22bdb292bb',
|
||||
'android.arch.lifecycle:extensions:429426b2feec2245ffc5e75b3b5309bedb36159cf06dc71843ae43526ac289b6',
|
||||
@ -182,7 +184,7 @@ dependencyVerification {
|
||||
'com.google.android.gms:play-services-auth:aec9e1c584d442cb9f59481a50b2c66dc191872607c04d97ecb82dd0eb5149ec',
|
||||
'com.google.android.exoplayer:exoplayer-ui:7a942afcc402ff01e9bf48e8d3942850986710f06562d50a1408aaf04a683151',
|
||||
'com.google.android.exoplayer:exoplayer-core:b6ab34abac36bc2bc6934b7a50008162feca2c0fde91aaf1e8c1c22f2c16e2c0',
|
||||
'org.whispersystems:signal-service-android:d48244f9e19a4300b0baf65c2cef8c76082d55f11d331b00d098c686729cde2e',
|
||||
'org.whispersystems:signal-service-android:0afd2cb17ed920611dacc54385f3ed375847c10ecd7839a025d9c61c387f7678',
|
||||
'org.whispersystems:webrtc-android:5493c92141ce884fc5ce8240d783232f4fe14bd17a8d0d7d1bd4944d0bd1682f',
|
||||
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
|
||||
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
|
||||
@ -251,9 +253,10 @@ dependencyVerification {
|
||||
'android.arch.persistence:db-framework:bd665448330acb90a6f551a87b0ba69169da2b8ec168b92f387997339cc14311',
|
||||
'android.arch.persistence:db:504e8c4307bfd53084924776ba3d49fed11b6f76d82dd80d5121c2d907fdfef6',
|
||||
'com.android.support:support-annotations:5d5b9414f02d3fa0ee7526b8d5ddae0da67c8ecc8c4d63ffa6cf91488a93b927',
|
||||
'com.android.support.constraint:constraint-layout-solver:2cafbe356f71c208013d021f32943904798cd6459e5107f9fe27000eb5bc2aef',
|
||||
'com.google.guava:listenablefuture:e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069',
|
||||
'org.signal:signal-metadata-android:d9d798aab7ee7200373ecff8718baf8aaeb632c123604e8a41b7b4c0c97eeee1',
|
||||
'org.whispersystems:signal-service-java:746b0334a2c11e978b50f6474bd67ba1aa7bc76fa96b0f3658411436238d1c79',
|
||||
'org.whispersystems:signal-service-java:9573395fe0b514cff10b8166f44de00a98682e0822a2b8204e9b9e696d53cb90',
|
||||
'com.github.bumptech.glide:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b',
|
||||
'com.github.bumptech.glide:annotations:bede99ef9f71517a4274bac18fd3e483e9f2b6108d7d6fe8f4949be4aa4d9512',
|
||||
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
|
||||
@ -306,8 +309,8 @@ android {
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "GIPHY_PROXY_HOST", "\"giphy-proxy-production.whispersystems.org\""
|
||||
buildConfigField "int", "GIPHY_PROXY_PORT", "80"
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
||||
buildConfigField "boolean", "DEV_BUILD", "false"
|
||||
buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
|
||||
|
BIN
res/drawable-hdpi/link_preview_splash.png
Normal file
BIN
res/drawable-hdpi/link_preview_splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 193 KiB |
BIN
res/drawable-mdpi/link_preview_splash.png
Normal file
BIN
res/drawable-mdpi/link_preview_splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 97 KiB |
BIN
res/drawable-xhdpi/link_preview_splash.png
Normal file
BIN
res/drawable-xhdpi/link_preview_splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 301 KiB |
BIN
res/drawable-xxhdpi/link_preview_splash.png
Normal file
BIN
res/drawable-xxhdpi/link_preview_splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 592 KiB |
BIN
res/drawable-xxxhdpi/link_preview_splash.png
Normal file
BIN
res/drawable-xxxhdpi/link_preview_splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 905 KiB |
@ -45,6 +45,16 @@
|
||||
app:quote_colorSecondary="?attr/conversation_item_sent_text_primary_color"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.LinkPreviewView
|
||||
android:id="@+id/link_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:layout_marginRight="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:visibility="gone"
|
||||
app:linkpreview_type="compose" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -128,6 +128,12 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/conversation_item_received_thumbnail" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/link_preview_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/conversation_item_received_link_preview" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/audio_view_stub"
|
||||
android:layout="@layout/conversation_item_received_audio"
|
||||
|
11
res/layout/conversation_item_received_link_preview.xml
Normal file
11
res/layout/conversation_item_received_link_preview.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.LinkPreviewView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/link_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:linkpreview_type="conversation"
|
||||
tools:visibility="visible" />
|
@ -65,6 +65,12 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/conversation_item_sent_thumbnail" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/link_preview_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/conversation_item_sent_link_preview" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/audio_view_stub"
|
||||
android:layout="@layout/conversation_item_sent_audio"
|
||||
|
11
res/layout/conversation_item_sent_link_preview.xml
Normal file
11
res/layout/conversation_item_sent_link_preview.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.LinkPreviewView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/link_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:linkpreview_type="conversation"
|
||||
tools:visibility="visible" />
|
109
res/layout/experience_upgrade_link_previews_fragment.xml
Normal file
109
res/layout/experience_upgrade_link_previews_fragment.xml
Normal file
@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:background="#FF2090ea">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/blurb"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
android:text="@string/ExperienceUpgradeActivity_introducing_link_previews"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="@android:color/white"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="@dimen/onboarding_title_size"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:src="@drawable/link_preview_splash"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linearLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_max="280dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/blurb"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintWidth_max="280dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toTopOf="@+id/experience_ok_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/ExperienceUpgradeActivity_optional_link_previews_are_now_supported"
|
||||
android:textColor="@color/core_white"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="@dimen/onboarding_subtitle_size" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
android:text="@string/ExperienceUpgradeActivity_you_can_disable_or_enable_this_feature_link_previews"
|
||||
android:textColor="@color/core_white"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<android.support.v7.widget.AppCompatButton
|
||||
android:id="@+id/experience_ok_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/ok"
|
||||
android:textColor="@color/core_blue"
|
||||
app:backgroundTint="@color/core_white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
108
res/layout/link_preview.xml
Normal file
108
res/layout/link_preview.xml
Normal file
@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<android.support.constraint.ConstraintLayout
|
||||
android:id="@+id/linkpreview_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="6dp"
|
||||
android:background="?linkpreview_background_color">
|
||||
|
||||
<org.thoughtcrime.securesms.components.OutlinedThumbnailView
|
||||
android:id="@+id/linkpreview_thumbnail"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linkpreview_divider"
|
||||
app:layout_constraintHeight_min="72dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/linkpreview_title"
|
||||
tools:src="@drawable/ic_contact_picture"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/linkpreview_title"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:maxLines="2"
|
||||
android:textColor="?linkpreview_primary_text_color"
|
||||
app:layout_constraintEnd_toStartOf="@+id/linkpreview_close"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Wall Crawler Strikes Again!" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/linkpreview_site"
|
||||
style="@style/Signal.Text.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?linkpreview_secondary_text_color"
|
||||
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
|
||||
app:layout_constraintTop_toBottomOf="@+id/linkpreview_title"
|
||||
tools:text="dailybugle.com" />
|
||||
|
||||
<View
|
||||
android:id="@+id/linkpreview_divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="?linkpreview_divider_color"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/linkpreview_thumbnail"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
|
||||
app:layout_constraintTop_toBottomOf="@+id/linkpreview_site"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/linkpreview_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:src="@drawable/ic_close_white_18dp"
|
||||
android:tint="@color/gray70"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.pnikosis.materialishprogress.ProgressWheel
|
||||
android:id="@+id/linkpreview_progress_wheel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:indeterminate="true"
|
||||
android:padding="8dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linkpreview_divider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:matProg_barColor="@color/core_blue"
|
||||
app:matProg_progressIndeterminate="true" />
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
|
||||
</merge>
|
@ -179,9 +179,9 @@
|
||||
android:id="@+id/quote_dismiss"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginRight="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:background="@drawable/dismiss_background"
|
||||
android:src="@drawable/ic_close_white_18dp"
|
||||
|
@ -110,6 +110,11 @@
|
||||
|
||||
<attr name="invite_background" format="color"/>
|
||||
|
||||
<attr name="linkpreview_background_color" format="color" />
|
||||
<attr name="linkpreview_primary_text_color" format="color" />
|
||||
<attr name="linkpreview_secondary_text_color" format="color" />
|
||||
<attr name="linkpreview_divider_color" format="color" />
|
||||
|
||||
<attr name="reminder_header_background" format="color"/>
|
||||
|
||||
<attr name="menu_new_conversation_icon" format="reference" />
|
||||
@ -281,6 +286,13 @@
|
||||
<attr name="contact_footerAlpha" format="float" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="LinkPreviewView">
|
||||
<attr name="linkpreview_type" format="enum">
|
||||
<enum name="conversation" value="0" />
|
||||
<enum name="compose" value="1" />
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="DocumentView">
|
||||
<attr name="doc_titleColor" format="color" />
|
||||
<attr name="doc_captionColor" format="color" />
|
||||
|
@ -51,6 +51,8 @@
|
||||
|
||||
<dimen name="mediasend_progress_dialog_size">120dp</dimen>
|
||||
|
||||
<dimen name="thumbnail_default_radius">4dp</dimen>
|
||||
|
||||
<dimen name="conversation_compose_height">40dp</dimen>
|
||||
<dimen name="conversation_individual_right_gutter">16dp</dimen>
|
||||
<dimen name="conversation_individual_left_gutter">16dp</dimen>
|
||||
|
@ -328,6 +328,10 @@
|
||||
<string name="ExperienceUpgradeActivity_turn_on_typing_indicators">Turn on typing indicators</string>
|
||||
<string name="ExperienceUpgradeActivity_no_thanks">No thanks</string>
|
||||
|
||||
<string name="ExperienceUpgradeActivity_introducing_link_previews">Introducing link previews.</string>
|
||||
<string name="ExperienceUpgradeActivity_optional_link_previews_are_now_supported">Optional link previews are now supported for some of the most popular sites on the Internet.</string>
|
||||
<string name="ExperienceUpgradeActivity_you_can_disable_or_enable_this_feature_link_previews">You can disable or enable this feature anytime in your Signal settings (Privacy > Send link previews).</string>
|
||||
|
||||
<!-- GcmBroadcastReceiver -->
|
||||
<string name="GcmBroadcastReceiver_retrieving_a_message">Retrieving a message...</string>
|
||||
|
||||
@ -1149,6 +1153,8 @@
|
||||
<string name="preferences__use_signal_for_viewing_and_storing_all_incoming_multimedia_messages">Use Signal for all incoming multimedia messages</string>
|
||||
<string name="preferences__pref_enter_sends_title">Enter key sends</string>
|
||||
<string name="preferences__pressing_the_enter_key_will_send_text_messages">Pressing the Enter key will send text messages</string>
|
||||
<string name="preferences__send_link_previews">Send link previews</string>
|
||||
<string name="preferences__previews_are_supported_for">Previews are supported for Imgur, Instagram, Reddit, and YouTube links</string>
|
||||
<string name="preferences__choose_identity">Choose identity</string>
|
||||
<string name="preferences__choose_your_contact_entry_from_the_contacts_list">Choose your contact entry from the contacts list.</string>
|
||||
<string name="preferences__change_passphrase">Change passphrase</string>
|
||||
|
@ -188,7 +188,6 @@
|
||||
<item name="emoji_category_emoticons">@drawable/emoji_category_emoticons_light</item>
|
||||
<item name="emoji_variation_selector_background">@drawable/emoji_variation_selector_background_light</item>
|
||||
|
||||
|
||||
<item name="conversation_item_bubble_background">@color/core_grey_05</item>
|
||||
<item name="conversation_item_sent_text_primary_color">@color/core_grey_90</item>
|
||||
<item name="conversation_item_sent_text_secondary_color">@color/core_grey_60</item>
|
||||
@ -223,6 +222,11 @@
|
||||
<item name="import_export_item_background_shadow_color">@color/import_export_item_background_shadow_light</item>
|
||||
<item name="import_export_item_card_background">@drawable/clickable_card_light</item>
|
||||
|
||||
<item name="linkpreview_background_color">@color/core_white</item>
|
||||
<item name="linkpreview_primary_text_color">@color/core_black</item>
|
||||
<item name="linkpreview_secondary_text_color">@color/core_grey_60</item>
|
||||
<item name="linkpreview_divider_color">@color/core_grey_25</item>
|
||||
|
||||
<item name="menu_new_conversation_icon">@drawable/ic_add_white_24dp</item>
|
||||
<item name="menu_group_icon">@drawable/ic_group_white_24dp</item>
|
||||
<item name="menu_search_icon">@drawable/ic_search_white_24dp</item>
|
||||
@ -373,6 +377,11 @@
|
||||
<item name="emoji_category_emoticons">@drawable/emoji_category_emoticons_dark</item>
|
||||
<item name="emoji_variation_selector_background">@drawable/emoji_variation_selector_background_dark</item>
|
||||
|
||||
<item name="linkpreview_background_color">@color/core_grey_95</item>
|
||||
<item name="linkpreview_primary_text_color">@color/core_white</item>
|
||||
<item name="linkpreview_secondary_text_color">@color/core_grey_25</item>
|
||||
<item name="linkpreview_divider_color">@color/core_grey_60</item>
|
||||
|
||||
<item name="quick_camera_icon">@drawable/quick_camera_dark</item>
|
||||
<item name="quick_mic_icon">@drawable/ic_mic_white_24dp</item>
|
||||
|
||||
|
@ -69,6 +69,12 @@
|
||||
android:title="@string/preferences__typing_indicators"
|
||||
android:summary="@string/preferences__if_typing_indicators_are_disabled_you_wont_be_able_to_see_typing_indicators"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="pref_link_previews"
|
||||
android:summary="@string/preferences__previews_are_supported_for"
|
||||
android:title="@string/preferences__send_link_previews"/>
|
||||
|
||||
<Preference android:key="preference_category_blocked"
|
||||
android:title="@string/preferences_app_protection__blocked_contacts" />
|
||||
</PreferenceCategory>
|
||||
|
@ -56,6 +56,7 @@
|
||||
android:key="pref_enter_sends"
|
||||
android:summary="@string/preferences__pressing_the_enter_key_will_send_text_messages"
|
||||
android:title="@string/preferences__pref_enter_sends_title"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:layout="@layout/preference_divider"/>
|
||||
|
@ -31,6 +31,8 @@ import com.google.android.gms.security.ProviderInstaller;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.crypto.PRNGFixes;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
|
||||
|
@ -7,6 +7,7 @@ import android.view.View;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
@ -31,6 +32,7 @@ public interface BindableConversationItem extends Unbindable {
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
||||
void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView);
|
||||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
|
@ -19,6 +19,7 @@ package org.thoughtcrime.securesms;
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
@ -125,6 +126,9 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
||||
@ -270,6 +274,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
protected HidingLinearLayout inlineAttachmentToggle;
|
||||
private QuickAttachmentDrawer quickAttachmentDrawer;
|
||||
private InputPanel inputPanel;
|
||||
private LinkPreviewViewModel linkPreviewViewModel;
|
||||
|
||||
private Recipient recipient;
|
||||
private long threadId;
|
||||
@ -309,6 +314,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
initializeActionBar();
|
||||
initializeViews();
|
||||
initializeResources();
|
||||
initializeLinkPreviewObserver();
|
||||
initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
@ -443,6 +449,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if ((data == null && reqCode != TAKE_PHOTO && reqCode != SMS_DEFAULT) ||
|
||||
(resultCode != RESULT_OK && reqCode != SMS_DEFAULT))
|
||||
{
|
||||
updateLinkPreviewState();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -516,7 +523,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
slideDeck.addSlide(new ImageSlide(this, data.getData(), imageSize, imageWidth, imageHeight));
|
||||
|
||||
sendMediaMessage(transport.isSms(), message, slideDeck, Collections.emptyList(), expiresIn, subscriptionId, initiating);
|
||||
sendMediaMessage(transport.isSms(), message, slideDeck, Collections.emptyList(), Collections.emptyList(), expiresIn, subscriptionId, initiating);
|
||||
break;
|
||||
|
||||
case MEDIA_SENDER:
|
||||
@ -547,7 +554,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
sendMediaMessage(transport.isSms(), message, slideDeck, Collections.emptyList(), expiresIn, subscriptionId, initiating);
|
||||
sendMediaMessage(transport.isSms(), message, slideDeck, Collections.emptyList(), Collections.emptyList(), expiresIn, subscriptionId, initiating);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -1438,6 +1445,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
sendButton.setEnabled(true);
|
||||
sendButton.addOnTransportChangedListener((newTransport, manuallySelected) -> {
|
||||
calculateCharactersRemaining();
|
||||
updateLinkPreviewState();
|
||||
composeText.setTransport(newTransport);
|
||||
buttonToggle.getBackground().setColorFilter(newTransport.getBackgroundColor(), Mode.MULTIPLY);
|
||||
buttonToggle.getBackground().invalidateSelf();
|
||||
@ -1496,6 +1504,31 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
recipient.addListener(this);
|
||||
}
|
||||
|
||||
|
||||
private void initializeLinkPreviewObserver() {
|
||||
linkPreviewViewModel = ViewModelProviders.of(this, new LinkPreviewViewModel.Factory(new LinkPreviewRepository())).get(LinkPreviewViewModel.class);
|
||||
|
||||
if (!TextSecurePreferences.isLinkPreviewsEnabled(this)) {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
linkPreviewViewModel.getLinkPreviewState().observe(this, previewState -> {
|
||||
if (previewState == null) return;
|
||||
|
||||
if (previewState.isLoading()) {
|
||||
Log.d(TAG, "Loading link preview.");
|
||||
inputPanel.setLinkPreviewLoading();
|
||||
} else {
|
||||
Log.d(TAG, "Setting link preview: " + previewState.getLinkPreview().isPresent());
|
||||
inputPanel.setLinkPreview(glideRequests, previewState.getLinkPreview());
|
||||
}
|
||||
|
||||
updateToggleButtonState();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void initializeProfiles() {
|
||||
if (!isSecureText) {
|
||||
Log.i(TAG, "SMS contact, no profile fetch needed.");
|
||||
@ -1546,6 +1579,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
//////// Helper Methods
|
||||
|
||||
private void addAttachment(int type) {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
|
||||
Log.i(TAG, "Selected: " + type);
|
||||
switch (type) {
|
||||
case AttachmentTypeSelector.ADD_GALLERY:
|
||||
@ -1604,7 +1639,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
boolean initiating = threadId == -1;
|
||||
|
||||
sendMediaMessage(isSmsForced(), "", attachmentManager.buildSlideDeck(), contacts, expiresIn, subscriptionId, initiating);
|
||||
sendMediaMessage(isSmsForced(), "", attachmentManager.buildSlideDeck(), contacts, Collections.emptyList(), expiresIn, subscriptionId, initiating);
|
||||
}
|
||||
|
||||
private void selectContactInfo(ContactData contactData) {
|
||||
@ -1843,6 +1878,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
fragment.scrollToBottom();
|
||||
attachmentManager.cleanup();
|
||||
|
||||
updateLinkPreviewState();
|
||||
}
|
||||
|
||||
private void sendMessage() {
|
||||
@ -1857,6 +1894,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
boolean initiating = threadId == -1;
|
||||
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
|
||||
recipient.isGroupRecipient() ||
|
||||
recipient.getAddress().isEmail() ||
|
||||
inputPanel.getQuote().isPresent() ||
|
||||
linkPreviewViewModel.hasLinkPreview();
|
||||
|
||||
Log.i(TAG, "isManual Selection: " + sendButton.isManualSelection());
|
||||
Log.i(TAG, "forceSms: " + forceSms);
|
||||
@ -1867,7 +1909,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
handleUnverifiedRecipients();
|
||||
} else if (!forceSms && identityRecords.isUntrusted()) {
|
||||
handleUntrustedRecipients();
|
||||
} else if (attachmentManager.isAttachmentPresent() || recipient.isGroupRecipient() || recipient.getAddress().isEmail() || inputPanel.getQuote().isPresent()) {
|
||||
} else if (isMediaMessage) {
|
||||
sendMediaMessage(forceSms, expiresIn, subscriptionId, initiating);
|
||||
} else {
|
||||
sendTextMessage(forceSms, expiresIn, subscriptionId, initiating);
|
||||
@ -1888,16 +1930,24 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
throws InvalidMessageException
|
||||
{
|
||||
Log.i(TAG, "Sending media message...");
|
||||
sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), Collections.emptyList(), expiresIn, subscriptionId, initiating);
|
||||
sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), Collections.emptyList(), linkPreviewViewModel.getPersistedLinkPreviews(this), expiresIn, subscriptionId, initiating);
|
||||
}
|
||||
|
||||
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck, List<Contact> contacts, final long expiresIn, final int subscriptionId, final boolean initiating) {
|
||||
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms,
|
||||
String body,
|
||||
SlideDeck slideDeck,
|
||||
List<Contact> contacts,
|
||||
List<LinkPreview> previews,
|
||||
final long expiresIn,
|
||||
final int subscriptionId,
|
||||
final boolean initiating)
|
||||
{
|
||||
if (!isDefaultSms && (!isSecureText || forceSms)) {
|
||||
showDefaultSmsPrompt();
|
||||
return new SettableFuture<>(null);
|
||||
}
|
||||
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, inputPanel.getQuote().orNull(), contacts);
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, inputPanel.getQuote().orNull(), contacts, previews);
|
||||
|
||||
final SettableFuture<Void> future = new SettableFuture<>();
|
||||
final Context context = getApplicationContext();
|
||||
@ -2009,7 +2059,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
buttonToggle.display(sendButton);
|
||||
quickAttachmentToggle.hide();
|
||||
|
||||
if (!attachmentManager.isAttachmentPresent()) {
|
||||
if (!attachmentManager.isAttachmentPresent() && !linkPreviewViewModel.hasLinkPreview()) {
|
||||
inlineAttachmentToggle.show();
|
||||
} else {
|
||||
inlineAttachmentToggle.hide();
|
||||
@ -2017,6 +2067,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLinkPreviewState() {
|
||||
if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent()) {
|
||||
linkPreviewViewModel.onEnabled();
|
||||
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed());
|
||||
} else {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void recordSubscriptionIdPreference(final Optional<Integer> subscriptionId) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
@ -2104,7 +2163,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
slideDeck.addSlide(audioSlide);
|
||||
|
||||
sendMediaMessage(forceSms, "", slideDeck, Collections.emptyList(), expiresIn, subscriptionId, initiating).addListener(new AssertedSuccessListener<Void>() {
|
||||
sendMediaMessage(forceSms, "", slideDeck, Collections.emptyList(), Collections.emptyList(), expiresIn, subscriptionId, initiating).addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void nothing) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@ -2164,6 +2223,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLinkPreviewCanceled() {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaSelected(@NonNull Uri uri, String contentType) {
|
||||
if (!TextUtils.isEmpty(contentType) && contentType.trim().equals("image/gif")) {
|
||||
@ -2193,6 +2257,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height) {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
Media media = new Media(uri, mimeType, dateTaken, width, height, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent());
|
||||
startActivityForResult(MediaSendActivity.getIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
|
||||
}
|
||||
@ -2278,7 +2343,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
public void afterTextChanged(Editable s) {
|
||||
calculateCharactersRemaining();
|
||||
|
||||
if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) {
|
||||
String trimmed = composeText.getTextTrimmed();
|
||||
|
||||
linkPreviewViewModel.onTextChanged(ConversationActivity.this, trimmed);
|
||||
|
||||
if (trimmed.length() == 0 || beforeLength == 0) {
|
||||
composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50);
|
||||
}
|
||||
}
|
||||
@ -2336,6 +2405,21 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
author,
|
||||
body,
|
||||
slideDeck);
|
||||
|
||||
} else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) {
|
||||
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
|
||||
if (linkPreview.getThumbnail().isPresent()) {
|
||||
slideDeck.addSlide(MediaUtil.getSlideForAttachment(this, linkPreview.getThumbnail().get()));
|
||||
}
|
||||
|
||||
inputPanel.setQuote(GlideApp.with(this),
|
||||
messageRecord.getDateSent(),
|
||||
author,
|
||||
messageRecord.getBody(),
|
||||
slideDeck);
|
||||
|
||||
} else {
|
||||
inputPanel.setQuote(GlideApp.with(this),
|
||||
messageRecord.getDateSent(),
|
||||
@ -2349,6 +2433,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
public void onAttachmentChanged() {
|
||||
handleSecurityChange(isSecureText, isDefaultSms);
|
||||
updateToggleButtonState();
|
||||
updateLinkPreviewState();
|
||||
}
|
||||
|
||||
private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener {
|
||||
|
@ -44,6 +44,7 @@ import android.text.TextUtils;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
@ -879,6 +880,13 @@ public class ConversationFragment extends Fragment
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLinkPreviewClicked(@NonNull LinkPreview linkPreview) {
|
||||
if (getContext() != null && getActivity() != null) {
|
||||
CommunicationActions.openBrowserLink(getActivity(), linkPreview.getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView) {
|
||||
if (getContext() != null && getActivity() != null) {
|
||||
|
@ -34,6 +34,10 @@ import android.text.TextUtils;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.LinkPreviewView;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
@ -66,6 +70,7 @@ import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
@ -127,6 +132,7 @@ public class ConversationItem extends LinearLayout
|
||||
private @NonNull Stub<AudioView> audioViewStub;
|
||||
private @NonNull Stub<DocumentView> documentViewStub;
|
||||
private @NonNull Stub<SharedContactView> sharedContactStub;
|
||||
private @NonNull Stub<LinkPreviewView> linkPreviewStub;
|
||||
private @Nullable EventListener eventListener;
|
||||
|
||||
private int defaultBubbleColor;
|
||||
@ -137,6 +143,7 @@ public class ConversationItem extends LinearLayout
|
||||
private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener);
|
||||
private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
|
||||
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
|
||||
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
|
||||
|
||||
private final Context context;
|
||||
|
||||
@ -172,6 +179,7 @@ public class ConversationItem extends LinearLayout
|
||||
this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub));
|
||||
this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub));
|
||||
this.sharedContactStub = new Stub<>(findViewById(R.id.shared_contact_view_stub));
|
||||
this.linkPreviewStub = new Stub<>(findViewById(R.id.link_preview_stub));
|
||||
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
|
||||
this.quoteView = findViewById(R.id.quote_view);
|
||||
this.container = findViewById(R.id.container);
|
||||
@ -383,6 +391,10 @@ public class ConversationItem extends LinearLayout
|
||||
return messageRecord.isMms() && !((MmsMessageRecord)messageRecord).getSharedContacts().isEmpty();
|
||||
}
|
||||
|
||||
private boolean hasLinkPreview(MessageRecord messageRecord) {
|
||||
return messageRecord.isMms() && !((MmsMessageRecord)messageRecord).getLinkPreviews().isEmpty();
|
||||
}
|
||||
|
||||
private void setBodyText(MessageRecord messageRecord) {
|
||||
bodyText.setClickable(false);
|
||||
bodyText.setFocusable(false);
|
||||
@ -409,6 +421,7 @@ public class ConversationItem extends LinearLayout
|
||||
if (audioViewStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
|
||||
|
||||
sharedContactStub.get().setContact(((MediaMmsMessageRecord) messageRecord).getSharedContacts().get(0), glideRequests, locale);
|
||||
sharedContactStub.get().setEventListener(sharedContactEventListener);
|
||||
@ -418,13 +431,51 @@ public class ConversationItem extends LinearLayout
|
||||
setSharedContactCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
footer.setVisibility(GONE);
|
||||
} else if (hasLinkPreview(messageRecord)) {
|
||||
linkPreviewStub.get().setVisibility(View.VISIBLE);
|
||||
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
|
||||
|
||||
if (linkPreview.getThumbnail().isPresent() && shouldPromotePreviewImage(linkPreview.getThumbnail().get())) {
|
||||
mediaThumbnailStub.get().setVisibility(VISIBLE);
|
||||
mediaThumbnailStub.get().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(context, linkPreview.getThumbnail().get())), showControls, false);
|
||||
mediaThumbnailStub.get().setThumbnailClickListener(new LinkPreviewThumbnailClickListener());
|
||||
mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener);
|
||||
mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, false);
|
||||
|
||||
setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
|
||||
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, true);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
} else {
|
||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true);
|
||||
linkPreviewStub.get().setDownloadClickedListener(downloadClickListener);
|
||||
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, false);
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
|
||||
linkPreviewStub.get().setOnClickListener(linkPreviewClickListener);
|
||||
linkPreviewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
|
||||
footer.setVisibility(VISIBLE);
|
||||
} else if (hasAudio(messageRecord)) {
|
||||
audioViewStub.get().setVisibility(View.VISIBLE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
audioViewStub.get().setAudio(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls);
|
||||
@ -439,6 +490,7 @@ public class ConversationItem extends LinearLayout
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
documentViewStub.get().setDocument(((MediaMmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide(), showControls);
|
||||
@ -451,9 +503,10 @@ public class ConversationItem extends LinearLayout
|
||||
footer.setVisibility(VISIBLE);
|
||||
} else if (hasThumbnail(messageRecord)) {
|
||||
mediaThumbnailStub.get().setVisibility(View.VISIBLE);
|
||||
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
List<Slide> thumbnailSlides = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlides();
|
||||
@ -469,7 +522,7 @@ public class ConversationItem extends LinearLayout
|
||||
mediaThumbnailStub.get().setConversationColor(messageRecord.isOutgoing() ? defaultBubbleColor
|
||||
: messageRecord.getRecipient().getColor().toConversationColor(context));
|
||||
|
||||
setThumbnailOutlineCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
|
||||
setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
@ -479,6 +532,7 @@ public class ConversationItem extends LinearLayout
|
||||
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
@ -486,10 +540,10 @@ public class ConversationItem extends LinearLayout
|
||||
}
|
||||
}
|
||||
|
||||
private void setThumbnailOutlineCorners(@NonNull MessageRecord current,
|
||||
@NonNull Optional<MessageRecord> previous,
|
||||
@NonNull Optional<MessageRecord> next,
|
||||
boolean isGroupThread)
|
||||
private void setThumbnailCorners(@NonNull MessageRecord current,
|
||||
@NonNull Optional<MessageRecord> previous,
|
||||
@NonNull Optional<MessageRecord> next,
|
||||
boolean isGroupThread)
|
||||
{
|
||||
int defaultRadius = readDimen(R.dimen.message_corner_radius);
|
||||
int collapseRadius = readDimen(R.dimen.message_corner_collapse_radius);
|
||||
@ -541,18 +595,38 @@ public class ConversationItem extends LinearLayout
|
||||
topRight = 0;
|
||||
}
|
||||
|
||||
mediaThumbnailStub.get().setOutlineCorners(topLeft, topRight, bottomRight, bottomLeft);
|
||||
if (hasLinkPreview(messageRecord)) {
|
||||
bottomLeft = 0;
|
||||
bottomRight = 0;
|
||||
}
|
||||
|
||||
mediaThumbnailStub.get().setCorners(topLeft, topRight, bottomRight, bottomLeft);
|
||||
}
|
||||
|
||||
private void setSharedContactCorners(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
|
||||
if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) {
|
||||
sharedContactStub.get().setSingularStyle();
|
||||
} else if (current.isOutgoing()) {
|
||||
sharedContactStub.get().setClusteredOutgoingStyle();
|
||||
} else {
|
||||
if (current.isOutgoing()) {
|
||||
sharedContactStub.get().setClusteredOutgoingStyle();
|
||||
} else {
|
||||
sharedContactStub.get().setClusteredIncomingStyle();
|
||||
}
|
||||
sharedContactStub.get().setClusteredIncomingStyle();
|
||||
}
|
||||
}
|
||||
|
||||
private void setLinkPreviewCorners(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread, boolean bigImage) {
|
||||
int defaultRadius = readDimen(R.dimen.message_corner_radius);
|
||||
int collapseRadius = readDimen(R.dimen.message_corner_collapse_radius);
|
||||
|
||||
if (bigImage) {
|
||||
linkPreviewStub.get().setCorners(0, 0);
|
||||
} else if (isStartOfMessageCluster(current, previous, isGroupThread) && !current.isOutgoing() && isGroupThread) {
|
||||
linkPreviewStub.get().setCorners(0, 0);
|
||||
} else if (isSingularMessage(current, previous, next, isGroupThread) || isStartOfMessageCluster(current, previous, isGroupThread)) {
|
||||
linkPreviewStub.get().setCorners(defaultRadius, defaultRadius);
|
||||
} else if (current.isOutgoing()) {
|
||||
linkPreviewStub.get().setCorners(defaultRadius, collapseRadius);
|
||||
} else {
|
||||
linkPreviewStub.get().setCorners(collapseRadius, defaultRadius);
|
||||
}
|
||||
}
|
||||
|
||||
@ -561,6 +635,11 @@ public class ConversationItem extends LinearLayout
|
||||
contactPhoto.setAvatar(glideRequests, recipient, true);
|
||||
}
|
||||
|
||||
private boolean shouldPromotePreviewImage(@NonNull Attachment attachment) {
|
||||
int minWidth = getResources().getDimensionPixelSize(R.dimen.media_bubble_min_width);
|
||||
return attachment.getWidth() >= minWidth;
|
||||
}
|
||||
|
||||
private SpannableString linkifyMessageBody(SpannableString messageBody, boolean shouldLinkifyAllLinks) {
|
||||
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
|
||||
boolean hasLinks = Linkify.addLinks(messageBody, shouldLinkifyAllLinks ? linkPattern : 0);
|
||||
@ -847,6 +926,27 @@ public class ConversationItem extends LinearLayout
|
||||
}
|
||||
}
|
||||
|
||||
private class LinkPreviewClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (eventListener != null && batchSelected.isEmpty() && messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) {
|
||||
eventListener.onLinkPreviewClicked(((MmsMessageRecord) messageRecord).getLinkPreviews().get(0));
|
||||
} else {
|
||||
passthroughClickListener.onClick(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LinkPreviewThumbnailClickListener implements SlideClickListener {
|
||||
public void onClick(final View v, final Slide slide) {
|
||||
if (eventListener != null && batchSelected.isEmpty() && messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) {
|
||||
eventListener.onLinkPreviewClicked(((MmsMessageRecord) messageRecord).getLinkPreviews().get(0));
|
||||
} else {
|
||||
performClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AttachmentDownloadClickListener implements SlidesClickedListener {
|
||||
@Override
|
||||
public void onClick(View v, final List<Slide> slides) {
|
||||
|
@ -48,7 +48,6 @@ import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
@ -60,7 +59,7 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.LifecycleBoundTask;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
@ -112,7 +111,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
|
||||
LifecycleBoundTask.run(getLifecycle(), () -> {
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
return Recipient.from(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), false);
|
||||
}, this::initializeProfileIcon);
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ExperienceUpgradeActivity extends BaseActionBarActivity implements TypingIndicatorIntroFragment.Controller {
|
||||
public class ExperienceUpgradeActivity extends BaseActionBarActivity implements TypingIndicatorIntroFragment.Controller, LinkPreviewsIntroFragment.Controller {
|
||||
private static final String TAG = ExperienceUpgradeActivity.class.getSimpleName();
|
||||
private static final String DISMISS_ACTION = "org.thoughtcrime.securesms.ExperienceUpgradeActivity.DISMISS_ACTION";
|
||||
private static final int NOTIFICATION_ID = 1339;
|
||||
@ -80,7 +80,14 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity implements
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
|
||||
null,
|
||||
true);
|
||||
true),
|
||||
LINK_PREVIEWS(449,
|
||||
new IntroPage(0xFF2090EA, LinkPreviewsIntroFragment.newInstance()),
|
||||
R.string.ExperienceUpgradeActivity_introducing_link_previews,
|
||||
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
|
||||
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
|
||||
null,
|
||||
true);
|
||||
|
||||
private int version;
|
||||
private List<IntroPage> pages;
|
||||
@ -215,10 +222,15 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished() {
|
||||
public void onTypingIndicatorsFinished() {
|
||||
onContinue(Optional.of(ExperienceUpgrade.TYPING_INDICATORS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLinkPreviewsFinished() {
|
||||
onContinue(Optional.of(ExperienceUpgrade.LINK_PREVIEWS));
|
||||
}
|
||||
|
||||
private final class OnPageChangeListener implements ViewPager.OnPageChangeListener {
|
||||
private final ArgbEvaluator evaluator = new ArgbEvaluator();
|
||||
private final ExperienceUpgrade upgrade;
|
||||
|
@ -0,0 +1,65 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class LinkPreviewsIntroFragment extends Fragment {
|
||||
|
||||
private Controller controller;
|
||||
|
||||
public static LinkPreviewsIntroFragment newInstance() {
|
||||
LinkPreviewsIntroFragment fragment = new LinkPreviewsIntroFragment();
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public LinkPreviewsIntroFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
||||
}
|
||||
|
||||
controller = (Controller) getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.experience_upgrade_link_previews_fragment, container, false);
|
||||
|
||||
view.findViewById(R.id.experience_ok_button).setOnClickListener(v -> {
|
||||
ApplicationContext.getInstance(requireContext())
|
||||
.getJobManager()
|
||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
||||
TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(requireContext())));
|
||||
controller.onLinkPreviewsFinished();
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onLinkPreviewsFinished();
|
||||
}
|
||||
}
|
@ -41,7 +41,8 @@ public class ReadReceiptsIntroFragment extends Fragment {
|
||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
||||
isChecked,
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext())));
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
});
|
||||
|
||||
return v;
|
||||
|
@ -62,6 +62,10 @@ public class TransportOptions {
|
||||
}
|
||||
|
||||
public void setDefaultSubscriptionId(Optional<Integer> subscriptionId) {
|
||||
if (defaultSubscriptionId.equals(subscriptionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.defaultSubscriptionId = subscriptionId;
|
||||
|
||||
if (!selectedOption.isPresent()) {
|
||||
|
@ -4,7 +4,6 @@ package org.thoughtcrime.securesms;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.widget.SwitchCompat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -12,7 +11,6 @@ import android.view.ViewGroup;
|
||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class TypingIndicatorIntroFragment extends Fragment {
|
||||
|
||||
@ -64,12 +62,13 @@ public class TypingIndicatorIntroFragment extends Fragment {
|
||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
||||
TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
typingEnabled,
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext())));
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
|
||||
controller.onFinished();
|
||||
controller.onTypingIndicatorsFinished();
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onFinished();
|
||||
void onTypingIndicatorsFinished();
|
||||
}
|
||||
}
|
||||
|
@ -26,24 +26,12 @@ import java.util.List;
|
||||
|
||||
public class ConversationItemThumbnail extends FrameLayout {
|
||||
|
||||
private static final String TAG = ConversationItemThumbnail.class.getSimpleName();
|
||||
|
||||
private final float[] radii = new float[8];
|
||||
private final RectF bounds = new RectF();
|
||||
private final Path corners = new Path();
|
||||
|
||||
private ThumbnailView thumbnail;
|
||||
private AlbumThumbnailView album;
|
||||
private ImageView shade;
|
||||
private ConversationItemFooter footer;
|
||||
private CornerMask cornerMask;
|
||||
|
||||
private final Paint outlinePaint = new Paint();
|
||||
{
|
||||
outlinePaint.setStyle(Paint.Style.STROKE);
|
||||
outlinePaint.setStrokeWidth(1f);
|
||||
outlinePaint.setAntiAlias(true);
|
||||
}
|
||||
private Outliner outliner;
|
||||
|
||||
public ConversationItemThumbnail(Context context) {
|
||||
super(context);
|
||||
@ -63,13 +51,14 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
inflate(getContext(), R.layout.conversation_item_thumbnail, this);
|
||||
|
||||
outlinePaint.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
||||
|
||||
this.thumbnail = findViewById(R.id.conversation_thumbnail_image);
|
||||
this.album = findViewById(R.id.conversation_thumbnail_album);
|
||||
this.shade = findViewById(R.id.conversation_thumbnail_shade);
|
||||
this.footer = findViewById(R.id.conversation_thumbnail_footer);
|
||||
this.cornerMask = new CornerMask(this);
|
||||
this.outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
|
||||
@ -95,17 +84,7 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
}
|
||||
|
||||
if (album.getVisibility() != VISIBLE) {
|
||||
final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2;
|
||||
|
||||
bounds.left = halfStrokeWidth;
|
||||
bounds.top = halfStrokeWidth;
|
||||
bounds.right = canvas.getWidth() - halfStrokeWidth;
|
||||
bounds.bottom = canvas.getHeight() - halfStrokeWidth;
|
||||
|
||||
corners.reset();
|
||||
corners.addRoundRect(bounds, radii, Path.Direction.CW);
|
||||
|
||||
canvas.drawPath(corners, outlinePaint);
|
||||
outliner.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,13 +111,9 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
forceLayout();
|
||||
}
|
||||
|
||||
public void setOutlineCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
||||
radii[0] = radii[1] = topLeft;
|
||||
radii[2] = radii[3] = topRight;
|
||||
radii[4] = radii[5] = bottomRight;
|
||||
radii[6] = radii[7] = bottomLeft;
|
||||
|
||||
public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
||||
cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
||||
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
||||
}
|
||||
|
||||
public ConversationItemFooter getFooter() {
|
||||
|
@ -4,6 +4,7 @@ import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.DimenRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
@ -22,6 +23,7 @@ import android.widget.Toast;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
@ -48,13 +50,14 @@ public class InputPanel extends LinearLayout
|
||||
|
||||
private static final int FADE_TIME = 150;
|
||||
|
||||
private QuoteView quoteView;
|
||||
private EmojiToggle emojiToggle;
|
||||
private ComposeText composeText;
|
||||
private View quickCameraToggle;
|
||||
private View quickAudioToggle;
|
||||
private View buttonToggle;
|
||||
private View recordingContainer;
|
||||
private QuoteView quoteView;
|
||||
private LinkPreviewView linkPreview;
|
||||
private EmojiToggle emojiToggle;
|
||||
private ComposeText composeText;
|
||||
private View quickCameraToggle;
|
||||
private View quickAudioToggle;
|
||||
private View buttonToggle;
|
||||
private View recordingContainer;
|
||||
|
||||
private MicrophoneRecorderView microphoneRecorderView;
|
||||
private SlideToCancel slideToCancel;
|
||||
@ -83,6 +86,7 @@ public class InputPanel extends LinearLayout
|
||||
View quoteDismiss = findViewById(R.id.quote_dismiss);
|
||||
|
||||
this.quoteView = findViewById(R.id.quote_view);
|
||||
this.linkPreview = findViewById(R.id.link_preview);
|
||||
this.emojiToggle = findViewById(R.id.emoji_toggle);
|
||||
this.composeText = findViewById(R.id.embedded_text_editor);
|
||||
this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
|
||||
@ -108,6 +112,12 @@ public class InputPanel extends LinearLayout
|
||||
}
|
||||
|
||||
quoteDismiss.setOnClickListener(v -> clearQuote());
|
||||
|
||||
linkPreview.setCloseClickedListener(() -> {
|
||||
if (listener != null) {
|
||||
listener.onLinkPreviewCanceled();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setListener(final @NonNull Listener listener) {
|
||||
@ -123,10 +133,20 @@ public class InputPanel extends LinearLayout
|
||||
public void setQuote(@NonNull GlideRequests glideRequests, long id, @NonNull Recipient author, @NonNull String body, @NonNull SlideDeck attachments) {
|
||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
|
||||
this.quoteView.setVisibility(View.VISIBLE);
|
||||
|
||||
if (this.linkPreview.getVisibility() == View.VISIBLE) {
|
||||
int cornerRadius = readDimen(R.dimen.message_corner_collapse_radius);
|
||||
this.linkPreview.setCorners(cornerRadius, cornerRadius);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearQuote() {
|
||||
this.quoteView.dismiss();
|
||||
|
||||
if (this.linkPreview.getVisibility() == View.VISIBLE) {
|
||||
int cornerRadius = readDimen(R.dimen.message_corner_radius);
|
||||
this.linkPreview.setCorners(cornerRadius, cornerRadius);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<QuoteModel> getQuote() {
|
||||
@ -137,6 +157,25 @@ public class InputPanel extends LinearLayout
|
||||
}
|
||||
}
|
||||
|
||||
public void setLinkPreviewLoading() {
|
||||
this.linkPreview.setVisibility(View.VISIBLE);
|
||||
this.linkPreview.setLoading();
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull Optional<LinkPreview> preview) {
|
||||
if (preview.isPresent()) {
|
||||
this.linkPreview.setVisibility(View.VISIBLE);
|
||||
this.linkPreview.setLinkPreview(glideRequests, preview.get(), true);
|
||||
} else {
|
||||
this.linkPreview.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
int cornerRadius = quoteView.getVisibility() == VISIBLE ? readDimen(R.dimen.message_corner_collapse_radius)
|
||||
: readDimen(R.dimen.message_corner_radius);
|
||||
|
||||
this.linkPreview.setCorners(cornerRadius, cornerRadius);
|
||||
}
|
||||
|
||||
public void setEmojiDrawer(@NonNull EmojiDrawer emojiDrawer) {
|
||||
emojiToggle.attach(emojiDrawer);
|
||||
}
|
||||
@ -238,6 +277,10 @@ public class InputPanel extends LinearLayout
|
||||
composeText.insertEmoji(emoji);
|
||||
}
|
||||
|
||||
private int readDimen(@DimenRes int dimenRes) {
|
||||
return getResources().getDimensionPixelSize(dimenRes);
|
||||
}
|
||||
|
||||
|
||||
public interface Listener {
|
||||
void onRecorderStarted();
|
||||
@ -245,6 +288,7 @@ public class InputPanel extends LinearLayout
|
||||
void onRecorderCanceled();
|
||||
void onRecorderPermissionRequired();
|
||||
void onEmojiToggle();
|
||||
void onLinkPreviewCanceled();
|
||||
}
|
||||
|
||||
private static class SlideToCancel {
|
||||
|
160
src/org/thoughtcrime/securesms/components/LinkPreviewView.java
Normal file
160
src/org/thoughtcrime/securesms/components/LinkPreviewView.java
Normal file
@ -0,0 +1,160 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class LinkPreviewView extends FrameLayout {
|
||||
|
||||
private static final int TYPE_CONVERSATION = 0;
|
||||
private static final int TYPE_COMPOSE = 1;
|
||||
|
||||
private ViewGroup container;
|
||||
private OutlinedThumbnailView thumbnail;
|
||||
private TextView title;
|
||||
private TextView site;
|
||||
private View divider;
|
||||
private View closeButton;
|
||||
private View spinner;
|
||||
|
||||
private int type;
|
||||
private int defaultRadius;
|
||||
private CornerMask cornerMask;
|
||||
private Outliner outliner;
|
||||
private CloseClickedListener closeClickedListener;
|
||||
|
||||
public LinkPreviewView(Context context) {
|
||||
super(context);
|
||||
init(null);
|
||||
}
|
||||
|
||||
public LinkPreviewView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
inflate(getContext(), R.layout.link_preview, this);
|
||||
|
||||
container = findViewById(R.id.linkpreview_container);
|
||||
thumbnail = findViewById(R.id.linkpreview_thumbnail);
|
||||
title = findViewById(R.id.linkpreview_title);
|
||||
site = findViewById(R.id.linkpreview_site);
|
||||
divider = findViewById(R.id.linkpreview_divider);
|
||||
spinner = findViewById(R.id.linkpreview_progress_wheel);
|
||||
closeButton = findViewById(R.id.linkpreview_close);
|
||||
defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
|
||||
cornerMask = new CornerMask(this);
|
||||
outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.LinkPreviewView, 0, 0);
|
||||
type = typedArray.getInt(R.styleable.LinkPreviewView_linkpreview_type, 0);
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
if (type == TYPE_COMPOSE) {
|
||||
container.setBackgroundColor(Color.TRANSPARENT);
|
||||
container.setPadding(0, 0, 0, 0);
|
||||
divider.setVisibility(VISIBLE);
|
||||
closeButton.setVisibility(VISIBLE);
|
||||
|
||||
closeButton.setOnClickListener(v -> {
|
||||
if (closeClickedListener != null) {
|
||||
closeClickedListener.onCloseClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
if (type == TYPE_COMPOSE) return;
|
||||
|
||||
if (cornerMask.isLegacy()) {
|
||||
cornerMask.mask(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchDraw(Canvas canvas) {
|
||||
super.dispatchDraw(canvas);
|
||||
if (type == TYPE_COMPOSE) return;
|
||||
|
||||
if (!cornerMask.isLegacy()) {
|
||||
cornerMask.mask(canvas);
|
||||
}
|
||||
|
||||
outliner.draw(canvas);
|
||||
}
|
||||
|
||||
public void setLoading() {
|
||||
title.setVisibility(GONE);
|
||||
site.setVisibility(GONE);
|
||||
thumbnail.setVisibility(GONE);
|
||||
spinner.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
||||
title.setVisibility(VISIBLE);
|
||||
site.setVisibility(VISIBLE);
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
spinner.setVisibility(GONE);
|
||||
|
||||
title.setText(linkPreview.getTitle());
|
||||
|
||||
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
|
||||
if (url != null) {
|
||||
site.setText(url.topPrivateDomain());
|
||||
}
|
||||
|
||||
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
thumbnail.setImageResource(glideRequests, new ImageSlide(getContext(), linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
|
||||
thumbnail.showDownloadText(false);
|
||||
} else {
|
||||
thumbnail.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setCorners(int topLeft, int topRight) {
|
||||
cornerMask.setRadii(topLeft, topRight, 0, 0);
|
||||
outliner.setRadii(topLeft, topRight, 0, 0);
|
||||
thumbnail.setCorners(topLeft, defaultRadius, defaultRadius, defaultRadius);
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setCloseClickedListener(@Nullable CloseClickedListener closeClickedListener) {
|
||||
this.closeClickedListener = closeClickedListener;
|
||||
}
|
||||
|
||||
public void setDownloadClickedListener(SlidesClickedListener listener) {
|
||||
thumbnail.setDownloadClickListener(listener);
|
||||
}
|
||||
|
||||
public interface CloseClickedListener {
|
||||
void onCloseClicked();
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter;
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
||||
|
||||
public class OutlinedThumbnailView extends ThumbnailView {
|
||||
|
||||
private CornerMask cornerMask;
|
||||
private Outliner outliner;
|
||||
|
||||
public OutlinedThumbnailView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public OutlinedThumbnailView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
cornerMask = new CornerMask(this);
|
||||
outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
||||
setRadius(0);
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (cornerMask.isLegacy()) {
|
||||
cornerMask.mask(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchDraw(Canvas canvas) {
|
||||
super.dispatchDraw(canvas);
|
||||
|
||||
if (!cornerMask.isLegacy()) {
|
||||
cornerMask.mask(canvas);
|
||||
}
|
||||
|
||||
outliner.draw(canvas);
|
||||
}
|
||||
|
||||
public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
||||
cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
||||
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
||||
postInvalidate();
|
||||
}
|
||||
}
|
55
src/org/thoughtcrime/securesms/components/Outliner.java
Normal file
55
src/org/thoughtcrime/securesms/components/Outliner.java
Normal file
@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
|
||||
public class Outliner {
|
||||
|
||||
private final float[] radii = new float[8];
|
||||
private final Path corners = new Path();
|
||||
private final RectF bounds = new RectF();
|
||||
private final Paint outlinePaint = new Paint();
|
||||
{
|
||||
outlinePaint.setStyle(Paint.Style.STROKE);
|
||||
outlinePaint.setStrokeWidth(1f);
|
||||
outlinePaint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
public void setColor(@ColorInt int color) {
|
||||
outlinePaint.setColor(color);
|
||||
}
|
||||
|
||||
public void draw(Canvas canvas) {
|
||||
final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2;
|
||||
|
||||
bounds.left = halfStrokeWidth;
|
||||
bounds.top = halfStrokeWidth;
|
||||
bounds.right = canvas.getWidth() - halfStrokeWidth;
|
||||
bounds.bottom = canvas.getHeight() - halfStrokeWidth;
|
||||
|
||||
corners.reset();
|
||||
corners.addRoundRect(bounds, radii, Path.Direction.CW);
|
||||
|
||||
canvas.drawPath(corners, outlinePaint);
|
||||
}
|
||||
|
||||
public void setRadius(int radius) {
|
||||
setRadii(radius, radius, radius, radius);
|
||||
}
|
||||
|
||||
public void setRadii(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
||||
radii[0] = radii[1] = topLeft;
|
||||
radii[2] = radii[3] = topRight;
|
||||
radii[4] = radii[5] = bottomRight;
|
||||
radii[6] = radii[7] = bottomLeft;
|
||||
}
|
||||
}
|
@ -89,12 +89,11 @@ public class ThumbnailView extends FrameLayout {
|
||||
bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0);
|
||||
bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0);
|
||||
bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0);
|
||||
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, getResources().getDimensionPixelSize(R.dimen.message_corner_collapse_radius));
|
||||
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius));
|
||||
typedArray.recycle();
|
||||
} else {
|
||||
radius = getResources().getDimensionPixelSize(R.dimen.message_corner_collapse_radius);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -329,10 +328,18 @@ public class ThumbnailView extends FrameLayout {
|
||||
slide = null;
|
||||
}
|
||||
|
||||
public void showDownloadText(boolean showDownloadText) {
|
||||
getTransferControls().setShowDownloadText(showDownloadText);
|
||||
}
|
||||
|
||||
public void showProgressSpinner() {
|
||||
getTransferControls().showProgressSpinner();
|
||||
}
|
||||
|
||||
protected void setRadius(int radius) {
|
||||
this.radius = radius;
|
||||
}
|
||||
|
||||
private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
|
@ -170,6 +170,7 @@ public class TransferControlView extends FrameLayout {
|
||||
|
||||
public void setShowDownloadText(boolean showDownloadText) {
|
||||
downloadDetailsText.setVisibility(showDownloadText ? VISIBLE : GONE);
|
||||
forceLayout();
|
||||
}
|
||||
|
||||
private boolean isUpdateToExistingSet(@NonNull List<Slide> slides) {
|
||||
|
@ -144,6 +144,10 @@ public class DatabaseFactory {
|
||||
getInstance(context).databaseHelper.markCurrent(database);
|
||||
}
|
||||
|
||||
public void doThing(Context context) {
|
||||
getInstance(context).databaseHelper.getReadableDatabase().execSQL("ALTER TABLE mms ADD COLUMN previews TEXT");
|
||||
}
|
||||
|
||||
private DatabaseFactory(@NonNull Context context) {
|
||||
SQLiteDatabase.loadLibs(context);
|
||||
|
||||
|
@ -25,6 +25,7 @@ import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.mms.pdu_alt.NotificationInd;
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
@ -51,6 +52,7 @@ import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Quote;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
@ -80,7 +82,6 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.Avatar;
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.deserialize;
|
||||
|
||||
public class MmsDatabase extends MessagingDatabase {
|
||||
|
||||
@ -105,7 +106,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
static final String QUOTE_ATTACHMENT = "quote_attachment";
|
||||
static final String QUOTE_MISSING = "quote_missing";
|
||||
|
||||
static final String SHARED_CONTACTS = "shared_contacts";
|
||||
static final String SHARED_CONTACTS = "shared_contacts";
|
||||
static final String LINK_PREVIEWS = "previews";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
THREAD_ID + " INTEGER, " + DATE_SENT + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + MESSAGE_BOX + " INTEGER, " +
|
||||
@ -125,7 +127,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
EXPIRE_STARTED + " INTEGER DEFAULT 0, " + NOTIFIED + " INTEGER DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + QUOTE_ID + " INTEGER DEFAULT 0, " +
|
||||
QUOTE_AUTHOR + " TEXT, " + QUOTE_BODY + " TEXT, " + QUOTE_ATTACHMENT + " INTEGER DEFAULT -1, " +
|
||||
QUOTE_MISSING + " INTEGER DEFAULT 0, " + SHARED_CONTACTS + " TEXT, " + UNIDENTIFIED + " INTEGER DEFAULT 0);";
|
||||
QUOTE_MISSING + " INTEGER DEFAULT 0, " + SHARED_CONTACTS + " TEXT, " + UNIDENTIFIED + " INTEGER DEFAULT 0, " +
|
||||
LINK_PREVIEWS + " TEXT);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
||||
@ -145,7 +148,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
MESSAGE_SIZE, STATUS, TRANSACTION_ID,
|
||||
BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
|
||||
DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
|
||||
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING, SHARED_CONTACTS, UNIDENTIFIED,
|
||||
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING,
|
||||
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED,
|
||||
"json_group_array(json_object(" +
|
||||
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
|
||||
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
|
||||
@ -588,14 +592,19 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.MISMATCHED_IDENTITIES));
|
||||
String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.NETWORK_FAILURE));
|
||||
|
||||
long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID));
|
||||
String quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR));
|
||||
String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY));
|
||||
boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_MISSING)) == 1;
|
||||
List<Attachment> quoteAttachments = Stream.of(associatedAttachments).filter(Attachment::isQuote).map(a -> (Attachment)a).toList();
|
||||
List<Contact> contacts = getSharedContacts(cursor, associatedAttachments);
|
||||
Set<Attachment> contactAttachments = new HashSet<>(Stream.of(contacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList());
|
||||
List<Attachment> attachments = Stream.of(associatedAttachments).filterNot(Attachment::isQuote).filterNot(contactAttachments::contains).map(a -> (Attachment)a).toList();
|
||||
long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID));
|
||||
String quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR));
|
||||
String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY));
|
||||
boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_MISSING)) == 1;
|
||||
List<Attachment> quoteAttachments = Stream.of(associatedAttachments).filter(Attachment::isQuote).map(a -> (Attachment)a).toList();
|
||||
List<Contact> contacts = getSharedContacts(cursor, associatedAttachments);
|
||||
Set<Attachment> contactAttachments = new HashSet<>(Stream.of(contacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList());
|
||||
List<LinkPreview> previews = getLinkPreviews(cursor, associatedAttachments);
|
||||
Set<Attachment> previewAttachments = Stream.of(previews).filter(lp -> lp.getThumbnail().isPresent()).map(lp -> lp.getThumbnail().get()).collect(Collectors.toSet());
|
||||
List<Attachment> attachments = Stream.of(associatedAttachments).filterNot(Attachment::isQuote)
|
||||
.filterNot(contactAttachments::contains)
|
||||
.filterNot(previewAttachments::contains)
|
||||
.map(a -> (Attachment)a).toList();
|
||||
|
||||
Recipient recipient = Recipient.from(context, Address.fromSerialized(address), false);
|
||||
List<NetworkFailure> networkFailures = new LinkedList<>();
|
||||
@ -623,12 +632,12 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
|
||||
if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) {
|
||||
return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, quote, contacts);
|
||||
return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, quote, contacts, previews);
|
||||
} else if (Types.isExpirationTimerUpdate(outboxType)) {
|
||||
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
|
||||
}
|
||||
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, distributionType, quote, contacts, networkFailures, mismatches);
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, distributionType, quote, contacts, previews, networkFailures, mismatches);
|
||||
|
||||
if (Types.isSecureType(outboxType)) {
|
||||
return new OutgoingSecureMediaMessage(message);
|
||||
@ -663,7 +672,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
JSONArray jsonContacts = new JSONArray(serializedContacts);
|
||||
|
||||
for (int i = 0; i < jsonContacts.length(); i++) {
|
||||
Contact contact = deserialize(jsonContacts.getJSONObject(i).toString());
|
||||
Contact contact = Contact.deserialize(jsonContacts.getJSONObject(i).toString());
|
||||
|
||||
if (contact.getAvatar() != null && contact.getAvatar().getAttachmentId() != null) {
|
||||
DatabaseAttachment attachment = attachmentIdMap.get(contact.getAvatar().getAttachmentId());
|
||||
@ -684,6 +693,43 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private List<LinkPreview> getLinkPreviews(@NonNull Cursor cursor, @NonNull List<DatabaseAttachment> attachments) {
|
||||
String serializedPreviews = cursor.getString(cursor.getColumnIndexOrThrow(LINK_PREVIEWS));
|
||||
|
||||
if (TextUtils.isEmpty(serializedPreviews)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Map<AttachmentId, DatabaseAttachment> attachmentIdMap = new HashMap<>();
|
||||
for (DatabaseAttachment attachment : attachments) {
|
||||
attachmentIdMap.put(attachment.getAttachmentId(), attachment);
|
||||
}
|
||||
|
||||
try {
|
||||
List<LinkPreview> previews = new LinkedList<>();
|
||||
JSONArray jsonPreviews = new JSONArray(serializedPreviews);
|
||||
|
||||
for (int i = 0; i < jsonPreviews.length(); i++) {
|
||||
LinkPreview preview = LinkPreview.deserialize(jsonPreviews.getJSONObject(i).toString());
|
||||
|
||||
if (preview.getAttachmentId() != null) {
|
||||
DatabaseAttachment attachment = attachmentIdMap.get(preview.getAttachmentId());
|
||||
if (attachment != null) {
|
||||
previews.add(new LinkPreview(preview.getUrl(), preview.getTitle(), attachment));
|
||||
}
|
||||
} else {
|
||||
previews.add(preview);
|
||||
}
|
||||
}
|
||||
|
||||
return previews;
|
||||
} catch (JSONException | IOException e) {
|
||||
Log.w(TAG, "Failed to parse shared contacts.", e);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public long copyMessageInbox(long messageId) throws MmsException {
|
||||
try {
|
||||
OutgoingMediaMessage request = getOutgoingMessage(messageId);
|
||||
@ -724,6 +770,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
attachments,
|
||||
new LinkedList<>(),
|
||||
request.getSharedContacts(),
|
||||
request.getLinkPreviews(),
|
||||
contentValues,
|
||||
null);
|
||||
} catch (NoSuchMessageException e) {
|
||||
@ -783,7 +830,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
long messageId = insertMediaMessage(retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), contentValues, null);
|
||||
long messageId = insertMediaMessage(retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), retrieved.getLinkPreviews(), contentValues, null);
|
||||
|
||||
if (!Types.isExpirationTimerUpdate(mailbox)) {
|
||||
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
|
||||
@ -922,7 +969,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
quoteAttachments.addAll(message.getOutgoingQuote().getAttachments());
|
||||
}
|
||||
|
||||
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), contentValues, insertListener);
|
||||
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), contentValues, insertListener);
|
||||
|
||||
if (message.getRecipient().getAddress().isGroup()) {
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().getAddress().toGroupString(), false);
|
||||
@ -946,6 +993,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
@NonNull List<Attachment> attachments,
|
||||
@NonNull List<Attachment> quoteAttachments,
|
||||
@NonNull List<Contact> sharedContacts,
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
@NonNull ContentValues contentValues,
|
||||
@Nullable SmsDatabase.InsertListener insertListener)
|
||||
throws MmsException
|
||||
@ -955,9 +1003,11 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
|
||||
List<Attachment> allAttachments = new LinkedList<>();
|
||||
List<Attachment> contactAttachments = Stream.of(sharedContacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList();
|
||||
List<Attachment> previewAttachments = Stream.of(linkPreviews).filter(lp -> lp.getThumbnail().isPresent()).map(lp -> lp.getThumbnail().get()).toList();
|
||||
|
||||
allAttachments.addAll(attachments);
|
||||
allAttachments.addAll(contactAttachments);
|
||||
allAttachments.addAll(previewAttachments);
|
||||
|
||||
contentValues.put(BODY, body);
|
||||
contentValues.put(PART_COUNT, allAttachments.size());
|
||||
@ -967,7 +1017,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
long messageId = db.insert(TABLE_NAME, null, contentValues);
|
||||
|
||||
Map<Attachment, AttachmentId> insertedAttachments = partsDatabase.insertAttachmentsForMessage(messageId, allAttachments, quoteAttachments);
|
||||
String serializedContacts = getSerializedSharedContacts(messageId, insertedAttachments, sharedContacts);
|
||||
String serializedContacts = getSerializedSharedContacts(insertedAttachments, sharedContacts);
|
||||
String serializedPreviews = getSerializedLinkPreviews(insertedAttachments, linkPreviews);
|
||||
|
||||
if (!TextUtils.isEmpty(serializedContacts)) {
|
||||
ContentValues contactValues = new ContentValues();
|
||||
@ -981,6 +1032,18 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(serializedPreviews)) {
|
||||
ContentValues contactValues = new ContentValues();
|
||||
contactValues.put(LINK_PREVIEWS, serializedPreviews);
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
int rows = database.update(TABLE_NAME, contactValues, ID + " = ?", new String[]{ String.valueOf(messageId) });
|
||||
|
||||
if (rows <= 0) {
|
||||
Log.w(TAG, "Failed to update message with link preview data.");
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
return messageId;
|
||||
} finally {
|
||||
@ -1016,7 +1079,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
deleteThreads(singleThreadSet);
|
||||
}
|
||||
|
||||
private @Nullable String getSerializedSharedContacts(long mmsId, @NonNull Map<Attachment, AttachmentId> insertedAttachmentIds, @NonNull List<Contact> contacts) {
|
||||
private @Nullable String getSerializedSharedContacts(@NonNull Map<Attachment, AttachmentId> insertedAttachmentIds, @NonNull List<Contact> contacts) {
|
||||
if (contacts.isEmpty()) return null;
|
||||
|
||||
JSONArray sharedContactJson = new JSONArray();
|
||||
@ -1042,6 +1105,28 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
return sharedContactJson.toString();
|
||||
}
|
||||
|
||||
private @Nullable String getSerializedLinkPreviews(@NonNull Map<Attachment, AttachmentId> insertedAttachmentIds, @NonNull List<LinkPreview> previews) {
|
||||
if (previews.isEmpty()) return null;
|
||||
|
||||
JSONArray linkPreviewJson = new JSONArray();
|
||||
|
||||
for (LinkPreview preview : previews) {
|
||||
try {
|
||||
AttachmentId attachmentId = null;
|
||||
|
||||
if (preview.getThumbnail().isPresent()) {
|
||||
attachmentId = insertedAttachmentIds.get(preview.getThumbnail().get());
|
||||
}
|
||||
|
||||
LinkPreview updatedPreview = new LinkPreview(preview.getUrl(), preview.getTitle(), attachmentId);
|
||||
linkPreviewJson.put(new JSONObject(updatedPreview.serialize()));
|
||||
} catch (JSONException | IOException e) {
|
||||
Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e);
|
||||
}
|
||||
}
|
||||
return linkPreviewJson.toString();
|
||||
}
|
||||
|
||||
private boolean isDuplicate(IncomingMediaMessage message, long threadId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",
|
||||
@ -1223,7 +1308,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
message.getOutgoingQuote().isOriginalMissing(),
|
||||
new SlideDeck(context, message.getOutgoingQuote().getAttachments())) :
|
||||
null,
|
||||
message.getSharedContacts(), false);
|
||||
message.getSharedContacts(), message.getLinkPreviews(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1322,15 +1407,17 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
List<NetworkFailure> networkFailures = getFailures(networkDocument);
|
||||
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor);
|
||||
List<Contact> contacts = getSharedContacts(cursor, attachments);
|
||||
Set<Attachment> contactAttachments = new HashSet<>(Stream.of(contacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList());
|
||||
SlideDeck slideDeck = getSlideDeck(Stream.of(attachments).filterNot(contactAttachments::contains).toList());
|
||||
Set<Attachment> contactAttachments = Stream.of(contacts).map(Contact::getAvatarAttachment).filter(a -> a != null).collect(Collectors.toSet());
|
||||
List<LinkPreview> previews = getLinkPreviews(cursor, attachments);
|
||||
Set<Attachment> previewAttachments = Stream.of(previews).filter(lp -> lp.getThumbnail().isPresent()).map(lp -> lp.getThumbnail().get()).collect(Collectors.toSet());
|
||||
SlideDeck slideDeck = getSlideDeck(Stream.of(attachments).filterNot(contactAttachments::contains).filterNot(previewAttachments::contains).toList());
|
||||
Quote quote = getQuote(cursor);
|
||||
|
||||
return new MediaMmsMessageRecord(context, id, recipient, recipient,
|
||||
addressDeviceId, dateSent, dateReceived, deliveryReceiptCount,
|
||||
threadId, body, slideDeck, partCount, box, mismatches,
|
||||
networkFailures, subscriptionId, expiresIn, expireStarted,
|
||||
readReceiptCount, quote, contacts, unidentified);
|
||||
readReceiptCount, quote, contacts, previews, unidentified);
|
||||
}
|
||||
|
||||
private Recipient getRecipientFor(String serialized) {
|
||||
|
@ -20,7 +20,6 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteQueryBuilder;
|
||||
@ -70,7 +69,8 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.QUOTE_BODY,
|
||||
MmsDatabase.QUOTE_MISSING,
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.SHARED_CONTACTS};
|
||||
MmsDatabase.SHARED_CONTACTS,
|
||||
MmsDatabase.LINK_PREVIEWS};
|
||||
|
||||
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
@ -246,7 +246,8 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.QUOTE_BODY,
|
||||
MmsDatabase.QUOTE_MISSING,
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.SHARED_CONTACTS};
|
||||
MmsDatabase.SHARED_CONTACTS,
|
||||
MmsDatabase.LINK_PREVIEWS};
|
||||
|
||||
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
|
||||
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
|
||||
@ -271,7 +272,8 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.QUOTE_BODY,
|
||||
MmsDatabase.QUOTE_MISSING,
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.SHARED_CONTACTS};
|
||||
MmsDatabase.SHARED_CONTACTS,
|
||||
MmsDatabase.LINK_PREVIEWS};
|
||||
|
||||
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
|
||||
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
|
||||
@ -338,6 +340,7 @@ public class MmsSmsDatabase extends Database {
|
||||
mmsColumnsPresent.add(MmsDatabase.QUOTE_MISSING);
|
||||
mmsColumnsPresent.add(MmsDatabase.QUOTE_ATTACHMENT);
|
||||
mmsColumnsPresent.add(MmsDatabase.SHARED_CONTACTS);
|
||||
mmsColumnsPresent.add(MmsDatabase.LINK_PREVIEWS);
|
||||
|
||||
Set<String> smsColumnsPresent = new HashSet<>();
|
||||
smsColumnsPresent.add(MmsSmsColumns.ID);
|
||||
|
@ -59,8 +59,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int SECRET_SENDER = 13;
|
||||
private static final int ATTACHMENT_CAPTIONS = 14;
|
||||
private static final int ATTACHMENT_CAPTIONS_FIX = 15;
|
||||
private static final int PREVIEWS = 16;
|
||||
|
||||
private static final int DATABASE_VERSION = 15;
|
||||
private static final int DATABASE_VERSION = 16;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@ -308,6 +309,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < PREVIEWS) {
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN previews TEXT");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
@ -56,11 +57,12 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
List<NetworkFailure> failures, int subscriptionId,
|
||||
long expiresIn, long expireStarted, int readReceiptCount,
|
||||
@Nullable Quote quote, @Nullable List<Contact> contacts,
|
||||
boolean unidentified)
|
||||
@Nullable List<LinkPreview> linkPreviews, boolean unidentified)
|
||||
{
|
||||
super(context, id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
|
||||
dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
|
||||
subscriptionId, expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts, unidentified);
|
||||
subscriptionId, expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts,
|
||||
linkPreviews, unidentified);
|
||||
|
||||
this.context = context.getApplicationContext();
|
||||
this.partCount = partCount;
|
||||
|
@ -8,6 +8,7 @@ import android.support.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@ -17,9 +18,10 @@ import java.util.List;
|
||||
|
||||
public abstract class MmsMessageRecord extends MessageRecord {
|
||||
|
||||
private final @NonNull SlideDeck slideDeck;
|
||||
private final @Nullable Quote quote;
|
||||
private final @NonNull List<Contact> contacts = new LinkedList<>();
|
||||
private final @NonNull SlideDeck slideDeck;
|
||||
private final @Nullable Quote quote;
|
||||
private final @NonNull List<Contact> contacts = new LinkedList<>();
|
||||
private final @NonNull List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
|
||||
MmsMessageRecord(Context context, long id, String body, Recipient conversationRecipient,
|
||||
Recipient individualRecipient, int recipientDeviceId, long dateSent,
|
||||
@ -27,7 +29,8 @@ public abstract class MmsMessageRecord extends MessageRecord {
|
||||
long type, List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> networkFailures, int subscriptionId, long expiresIn,
|
||||
long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount,
|
||||
@Nullable Quote quote, @NonNull List<Contact> contacts, boolean unidentified)
|
||||
@Nullable Quote quote, @NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews, boolean unidentified)
|
||||
{
|
||||
super(context, id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified);
|
||||
|
||||
@ -35,6 +38,7 @@ public abstract class MmsMessageRecord extends MessageRecord {
|
||||
this.quote = quote;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
this.linkPreviews.addAll(linkPreviews);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -69,4 +73,8 @@ public abstract class MmsMessageRecord extends MessageRecord {
|
||||
public @NonNull List<Contact> getSharedContacts() {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public @NonNull List<LinkPreview> getLinkPreviews() {
|
||||
return linkPreviews;
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
|
||||
super(context, id, "", conversationRecipient, individualRecipient, recipientDeviceId,
|
||||
dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
|
||||
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>(), subscriptionId,
|
||||
0, 0, slideDeck, readReceiptCount, null, Collections.emptyList(), false);
|
||||
0, 0, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false);
|
||||
|
||||
this.contentLocation = contentLocation;
|
||||
this.messageSize = messageSize;
|
||||
|
@ -0,0 +1,56 @@
|
||||
package org.thoughtcrime.securesms.giph.model;
|
||||
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.Key;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public class ChunkedImageUrl implements Key {
|
||||
|
||||
public static final long SIZE_UNKNOWN = -1;
|
||||
|
||||
private final String url;
|
||||
private final long size;
|
||||
|
||||
public ChunkedImageUrl(@NonNull String url) {
|
||||
this(url, SIZE_UNKNOWN);
|
||||
}
|
||||
|
||||
public ChunkedImageUrl(@NonNull String url, long size) {
|
||||
this.url = url;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDiskCacheKey(MessageDigest messageDigest) {
|
||||
messageDigest.update(url.getBytes());
|
||||
messageDigest.update(Conversions.longToByteArray(size));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof ChunkedImageUrl)) return false;
|
||||
|
||||
ChunkedImageUrl that = (ChunkedImageUrl)other;
|
||||
|
||||
return this.url.equals(that.url) && this.size == that.size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return url.hashCode() ^ (int)size;
|
||||
}
|
||||
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package org.thoughtcrime.securesms.giph.model;
|
||||
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.Key;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public class GiphyPaddedUrl implements Key {
|
||||
|
||||
private final String target;
|
||||
private final long size;
|
||||
|
||||
public GiphyPaddedUrl(@NonNull String target, long size) {
|
||||
this.target = target;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDiskCacheKey(MessageDigest messageDigest) {
|
||||
messageDigest.update(target.getBytes());
|
||||
messageDigest.update(Conversions.longToByteArray(size));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof GiphyPaddedUrl)) return false;
|
||||
|
||||
GiphyPaddedUrl that = (GiphyPaddedUrl)other;
|
||||
|
||||
return this.target.equals(that.target) && this.size == that.size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return target.hashCode() ^ (int)size;
|
||||
}
|
||||
|
||||
}
|
@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyImage;
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyResponse;
|
||||
import org.thoughtcrime.securesms.net.ContentProxySelector;
|
||||
import org.thoughtcrime.securesms.util.AsyncLoader;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
|
||||
@ -35,7 +36,7 @@ public abstract class GiphyLoader extends AsyncLoader<List<GiphyImage>> {
|
||||
protected GiphyLoader(@NonNull Context context, @Nullable String searchString) {
|
||||
super(context);
|
||||
this.searchString = searchString;
|
||||
this.client = new OkHttpClient.Builder().proxySelector(new GiphyProxySelector()).build();
|
||||
this.client = new OkHttpClient.Builder().proxySelector(new ContentProxySelector()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,73 +0,0 @@
|
||||
package org.thoughtcrime.securesms.giph.net;
|
||||
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GiphyProxySelector extends ProxySelector {
|
||||
|
||||
private static final String TAG = GiphyProxySelector.class.getSimpleName();
|
||||
|
||||
private final List<Proxy> EMPTY = new ArrayList<>(1);
|
||||
private volatile List<Proxy> GIPHY = null;
|
||||
|
||||
public GiphyProxySelector() {
|
||||
EMPTY.add(Proxy.NO_PROXY);
|
||||
|
||||
if (Util.isMainThread()) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
synchronized (GiphyProxySelector.this) {
|
||||
initializeGiphyProxy();
|
||||
GiphyProxySelector.this.notifyAll();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
} else {
|
||||
initializeGiphyProxy();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
if (uri.getHost().endsWith("giphy.com")) return getOrCreateGiphyProxy();
|
||||
else return EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress address, IOException failure) {
|
||||
Log.w(TAG, failure);
|
||||
}
|
||||
|
||||
private void initializeGiphyProxy() {
|
||||
GIPHY = new ArrayList<Proxy>(1) {{
|
||||
add(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(BuildConfig.GIPHY_PROXY_HOST,
|
||||
BuildConfig.GIPHY_PROXY_PORT)));
|
||||
}};
|
||||
}
|
||||
|
||||
private List<Proxy> getOrCreateGiphyProxy() {
|
||||
if (GIPHY == null) {
|
||||
synchronized (this) {
|
||||
while (GIPHY == null) Util.wait(this, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return GIPHY;
|
||||
}
|
||||
|
||||
}
|
@ -17,6 +17,7 @@ import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
@ -25,7 +26,7 @@ import com.bumptech.glide.util.ByteBufferUtil;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyImage;
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyPaddedUrl;
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@ -70,7 +71,7 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
||||
Log.w(TAG, e);
|
||||
|
||||
synchronized (this) {
|
||||
if (new GiphyPaddedUrl(image.getGifUrl(), image.getGifSize()).equals(model)) {
|
||||
if (new ChunkedImageUrl(image.getGifUrl(), image.getGifSize()).equals(model)) {
|
||||
this.modelReady = true;
|
||||
notifyAll();
|
||||
}
|
||||
@ -82,7 +83,7 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||
synchronized (this) {
|
||||
if (new GiphyPaddedUrl(image.getGifUrl(), image.getGifSize()).equals(model)) {
|
||||
if (new ChunkedImageUrl(image.getGifUrl(), image.getGifSize()).equals(model)) {
|
||||
this.modelReady = true;
|
||||
notifyAll();
|
||||
}
|
||||
@ -100,8 +101,8 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
||||
}
|
||||
|
||||
GifDrawable drawable = glideRequests.asGif()
|
||||
.load(forMms ? new GiphyPaddedUrl(image.getGifMmsUrl(), image.getMmsGifSize()) :
|
||||
new GiphyPaddedUrl(image.getGifUrl(), image.getGifSize()))
|
||||
.load(forMms ? new ChunkedImageUrl(image.getGifMmsUrl(), image.getMmsGifSize()) :
|
||||
new ChunkedImageUrl(image.getGifUrl(), image.getGifSize()))
|
||||
.submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
|
||||
.get();
|
||||
|
||||
@ -148,22 +149,24 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
||||
holder.gifProgress.setVisibility(View.GONE);
|
||||
|
||||
RequestBuilder<Drawable> thumbnailRequest = GlideApp.with(context)
|
||||
.load(new GiphyPaddedUrl(image.getStillUrl(), image.getStillSize()))
|
||||
.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL);
|
||||
|
||||
if (Util.isLowMemory(context)) {
|
||||
glideRequests.load(new GiphyPaddedUrl(image.getStillUrl(), image.getStillSize()))
|
||||
glideRequests.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
|
||||
.placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.listener(holder)
|
||||
.into(holder.thumbnail);
|
||||
|
||||
holder.setModelReady();
|
||||
} else {
|
||||
glideRequests.load(new GiphyPaddedUrl(image.getGifUrl(), image.getGifSize()))
|
||||
glideRequests.load(new ChunkedImageUrl(image.getGifUrl(), image.getGifSize()))
|
||||
.thumbnail(thumbnailRequest)
|
||||
.placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.listener(holder)
|
||||
.into(holder.thumbnail);
|
||||
}
|
||||
|
@ -0,0 +1,75 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.net.ChunkedDataFetcher;
|
||||
import org.thoughtcrime.securesms.net.RequestController;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
class ChunkedImageUrlFetcher implements DataFetcher<InputStream> {
|
||||
|
||||
private static final String TAG = ChunkedImageUrlFetcher.class.getSimpleName();
|
||||
|
||||
private final OkHttpClient client;
|
||||
private final ChunkedImageUrl url;
|
||||
|
||||
private RequestController requestController;
|
||||
|
||||
ChunkedImageUrlFetcher(@NonNull OkHttpClient client, @NonNull ChunkedImageUrl url) {
|
||||
this.client = client;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||
ChunkedDataFetcher fetcher = new ChunkedDataFetcher(client);
|
||||
requestController = fetcher.fetch(url.getUrl(), url.getSize(), new ChunkedDataFetcher.Callback() {
|
||||
@Override
|
||||
public void onSuccess(InputStream stream) {
|
||||
callback.onDataReady(stream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception e) {
|
||||
callback.onLoadFailed(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
if (requestController != null) {
|
||||
requestController.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
Log.d(TAG, "Canceled.");
|
||||
if (requestController != null) {
|
||||
requestController.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<InputStream> getDataClass() {
|
||||
return InputStream.class;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DataSource getDataSource() {
|
||||
return DataSource.REMOTE;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.net.ContentProxySelector;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class ChunkedImageUrlLoader implements ModelLoader<ChunkedImageUrl, InputStream> {
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
private ChunkedImageUrlLoader(OkHttpClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public LoadData<InputStream> buildLoadData(ChunkedImageUrl url, int width, int height, Options options) {
|
||||
return new LoadData<>(url, new ChunkedImageUrlFetcher(client, url));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(ChunkedImageUrl url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<ChunkedImageUrl, InputStream> {
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
public Factory() {
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.proxySelector(new ContentProxySelector())
|
||||
.cache(null)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelLoader<ChunkedImageUrl, InputStream> build(MultiModelLoaderFactory multiFactory) {
|
||||
return new ChunkedImageUrlLoader(client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {}
|
||||
}
|
||||
}
|
@ -1,285 +0,0 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
import com.bumptech.glide.util.ContentLengthInputStream;
|
||||
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyPaddedUrl;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
class GiphyPaddedUrlFetcher implements DataFetcher<InputStream> {
|
||||
|
||||
private static final String TAG = GiphyPaddedUrlFetcher.class.getSimpleName();
|
||||
|
||||
private static final long MB = 1024 * 1024;
|
||||
private static final long KB = 1024;
|
||||
|
||||
private final OkHttpClient client;
|
||||
private final GiphyPaddedUrl url;
|
||||
|
||||
private List<ResponseBody> bodies;
|
||||
private List<InputStream> rangeStreams;
|
||||
private InputStream stream;
|
||||
|
||||
GiphyPaddedUrlFetcher(@NonNull OkHttpClient client,
|
||||
@NonNull GiphyPaddedUrl url)
|
||||
{
|
||||
this.client = client;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||
bodies = new LinkedList<>();
|
||||
rangeStreams = new LinkedList<>();
|
||||
stream = null;
|
||||
|
||||
try {
|
||||
List<ByteRange> requestPattern = getRequestPattern(url.getSize());
|
||||
|
||||
for (ByteRange range : requestPattern) {
|
||||
Request request = new Request.Builder()
|
||||
.addHeader("Range", "bytes=" + range.start + "-" + range.end)
|
||||
.addHeader("Accept-Encoding", "identity")
|
||||
.url(url.getTarget())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
Response response = client.newCall(request).execute();
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("Bad response: " + response.code() + " - " + response.message());
|
||||
}
|
||||
|
||||
ResponseBody responseBody = response.body();
|
||||
|
||||
if (responseBody == null) throw new IOException("Response body was null");
|
||||
else bodies.add(responseBody);
|
||||
|
||||
rangeStreams.add(new SkippingInputStream(ContentLengthInputStream.obtain(responseBody.byteStream(), responseBody.contentLength()), range.ignoreFirst));
|
||||
}
|
||||
|
||||
stream = new InputStreamList(rangeStreams);
|
||||
callback.onDataReady(stream);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
callback.onLoadFailed(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
if (rangeStreams != null) {
|
||||
for (InputStream rangeStream : rangeStreams) {
|
||||
try {
|
||||
if (rangeStream != null) rangeStream.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (bodies != null) {
|
||||
for (ResponseBody body : bodies) {
|
||||
if (body != null) body.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (stream != null) {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<InputStream> getDataClass() {
|
||||
return InputStream.class;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DataSource getDataSource() {
|
||||
return DataSource.REMOTE;
|
||||
}
|
||||
|
||||
private List<ByteRange> getRequestPattern(long size) throws IOException {
|
||||
if (size > MB) return getRequestPattern(size, MB);
|
||||
else if (size > 500 * KB) return getRequestPattern(size, 500 * KB);
|
||||
else if (size > 100 * KB) return getRequestPattern(size, 100 * KB);
|
||||
else if (size > 50 * KB) return getRequestPattern(size, 50 * KB);
|
||||
else if (size > KB) return getRequestPattern(size, KB);
|
||||
|
||||
throw new IOException("Unsupported size: " + size);
|
||||
}
|
||||
|
||||
private List<ByteRange> getRequestPattern(long size, long increment) {
|
||||
List<ByteRange> results = new LinkedList<>();
|
||||
|
||||
long offset = 0;
|
||||
|
||||
while (size - offset > increment) {
|
||||
results.add(new ByteRange(offset, offset + increment - 1, 0));
|
||||
offset += increment;
|
||||
}
|
||||
|
||||
if (size - offset > 0) {
|
||||
results.add(new ByteRange(size - increment, size-1, increment - (size - offset)));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static class ByteRange {
|
||||
private final long start;
|
||||
private final long end;
|
||||
private final long ignoreFirst;
|
||||
|
||||
private ByteRange(long start, long end, long ignoreFirst) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.ignoreFirst = ignoreFirst;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SkippingInputStream extends FilterInputStream {
|
||||
|
||||
private long skip;
|
||||
|
||||
SkippingInputStream(InputStream in, long skip) {
|
||||
super(in);
|
||||
this.skip = skip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (skip != 0) {
|
||||
skipFully(skip);
|
||||
skip = 0;
|
||||
}
|
||||
|
||||
return super.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] buffer) throws IOException {
|
||||
if (skip != 0) {
|
||||
skipFully(skip);
|
||||
skip = 0;
|
||||
}
|
||||
|
||||
return super.read(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {
|
||||
if (skip != 0) {
|
||||
skipFully(skip);
|
||||
skip = 0;
|
||||
}
|
||||
|
||||
return super.read(buffer, offset, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return Util.toIntExact(super.available() - skip);
|
||||
}
|
||||
|
||||
private void skipFully(long amount) throws IOException {
|
||||
byte[] buffer = new byte[4096];
|
||||
|
||||
while (amount > 0) {
|
||||
int read = super.read(buffer, 0, Math.min(buffer.length, Util.toIntExact(amount)));
|
||||
|
||||
if (read != -1) amount -= read;
|
||||
else return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class InputStreamList extends InputStream {
|
||||
|
||||
private final List<InputStream> inputStreams;
|
||||
|
||||
private int currentStreamIndex = 0;
|
||||
|
||||
InputStreamList(List<InputStream> inputStreams) {
|
||||
this.inputStreams = inputStreams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
while (currentStreamIndex < inputStreams.size()) {
|
||||
int result = inputStreams.get(currentStreamIndex).read();
|
||||
|
||||
if (result == -1) currentStreamIndex++;
|
||||
else return result;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {
|
||||
while (currentStreamIndex < inputStreams.size()) {
|
||||
int result = inputStreams.get(currentStreamIndex).read(buffer, offset, length);
|
||||
|
||||
if (result == -1) currentStreamIndex++;
|
||||
else return result;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] buffer) throws IOException {
|
||||
return read(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
for (InputStream stream : inputStreams) {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
int total = 0;
|
||||
|
||||
for (int i=currentStreamIndex;i<inputStreams.size();i++) {
|
||||
try {
|
||||
int available = inputStreams.get(i).available();
|
||||
|
||||
if (available != -1) total += available;
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyPaddedUrl;
|
||||
import org.thoughtcrime.securesms.giph.net.GiphyProxySelector;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class GiphyPaddedUrlLoader implements ModelLoader<GiphyPaddedUrl, InputStream> {
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
private GiphyPaddedUrlLoader(OkHttpClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public LoadData<InputStream> buildLoadData(GiphyPaddedUrl url, int width, int height, Options options) {
|
||||
return new LoadData<>(url, new GiphyPaddedUrlFetcher(client, url));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(GiphyPaddedUrl url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<GiphyPaddedUrl, InputStream> {
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
public Factory() {
|
||||
this.client = new OkHttpClient.Builder().proxySelector(new GiphyProxySelector()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelLoader<GiphyPaddedUrl, InputStream> build(MultiModelLoaderFactory multiFactory) {
|
||||
return new GiphyPaddedUrlLoader(client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.giph.net.GiphyProxySelector;
|
||||
import org.thoughtcrime.securesms.net.ContentProxySelector;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
@ -45,7 +45,7 @@ public class OkHttpUrlLoader implements ModelLoader<GlideUrl, InputStream> {
|
||||
synchronized (Factory.class) {
|
||||
if (internalClient == null) {
|
||||
internalClient = new OkHttpClient.Builder()
|
||||
.proxySelector(new GiphyProxySelector())
|
||||
.proxySelector(new ContentProxySelector())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ public class GroupManager {
|
||||
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null);
|
||||
}
|
||||
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null, Collections.emptyList());
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
|
||||
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
|
@ -212,7 +212,7 @@ public class GroupMessageProcessor {
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
Address addres = Address.fromExternal(context, GroupUtil.getEncodedId(group.getGroupId(), false));
|
||||
Recipient recipient = Recipient.from(context, addres, false);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList());
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||
|
||||
|
@ -101,7 +101,7 @@ public class AttachmentUploadJob extends ContextJob implements InjectableType {
|
||||
exception instanceof ConnectException;
|
||||
}
|
||||
|
||||
protected SignalServiceAttachment getAttachmentFor(Attachment attachment) {
|
||||
private SignalServiceAttachment getAttachmentFor(Attachment attachment) {
|
||||
try {
|
||||
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
|
||||
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
|
||||
|
@ -33,18 +33,25 @@ public class MultiDeviceConfigurationUpdateJob extends ContextJob implements Inj
|
||||
private static final String KEY_READ_RECEIPTS_ENABLED = "read_receipts_enabled";
|
||||
private static final String KEY_TYPING_INDICATORS_ENABLED = "typing_indicators_enabled";
|
||||
private static final String KEY_UNIDENTIFIED_DELIVERY_INDICATORS_ENABLED = "unidentified_delivery_indicators_enabled";
|
||||
private static final String KEY_LINK_PREVIEWS_ENABLED = "link_previews_enabled";
|
||||
|
||||
@Inject transient SignalServiceMessageSender messageSender;
|
||||
|
||||
private boolean readReceiptsEnabled;
|
||||
private boolean typingIndicatorsEnabled;
|
||||
private boolean unidentifiedDeliveryIndicatorsEnabled;
|
||||
private boolean linkPreviewsEnabled;
|
||||
|
||||
public MultiDeviceConfigurationUpdateJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
||||
super(context, workerParameters);
|
||||
}
|
||||
|
||||
public MultiDeviceConfigurationUpdateJob(Context context, boolean readReceiptsEnabled, boolean typingIndicatorsEnabled, boolean unidentifiedDeliveryIndicatorsEnabled) {
|
||||
public MultiDeviceConfigurationUpdateJob(Context context,
|
||||
boolean readReceiptsEnabled,
|
||||
boolean typingIndicatorsEnabled,
|
||||
boolean unidentifiedDeliveryIndicatorsEnabled,
|
||||
boolean linkPreviewsEnabled)
|
||||
{
|
||||
super(context, JobParameters.newBuilder()
|
||||
.withGroupId("__MULTI_DEVICE_CONFIGURATION_UPDATE_JOB__")
|
||||
.withNetworkRequirement()
|
||||
@ -53,6 +60,7 @@ public class MultiDeviceConfigurationUpdateJob extends ContextJob implements Inj
|
||||
this.readReceiptsEnabled = readReceiptsEnabled;
|
||||
this.typingIndicatorsEnabled = typingIndicatorsEnabled;
|
||||
this.unidentifiedDeliveryIndicatorsEnabled = unidentifiedDeliveryIndicatorsEnabled;
|
||||
this.linkPreviewsEnabled = linkPreviewsEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -60,6 +68,7 @@ public class MultiDeviceConfigurationUpdateJob extends ContextJob implements Inj
|
||||
readReceiptsEnabled = data.getBoolean(KEY_READ_RECEIPTS_ENABLED);
|
||||
typingIndicatorsEnabled = data.getBoolean(KEY_TYPING_INDICATORS_ENABLED);
|
||||
unidentifiedDeliveryIndicatorsEnabled = data.getBoolean(KEY_UNIDENTIFIED_DELIVERY_INDICATORS_ENABLED);
|
||||
linkPreviewsEnabled = data.getBoolean(KEY_LINK_PREVIEWS_ENABLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -67,6 +76,7 @@ public class MultiDeviceConfigurationUpdateJob extends ContextJob implements Inj
|
||||
return dataBuilder.putBoolean(KEY_READ_RECEIPTS_ENABLED, readReceiptsEnabled)
|
||||
.putBoolean(KEY_TYPING_INDICATORS_ENABLED, typingIndicatorsEnabled)
|
||||
.putBoolean(KEY_UNIDENTIFIED_DELIVERY_INDICATORS_ENABLED, unidentifiedDeliveryIndicatorsEnabled)
|
||||
.putBoolean(KEY_LINK_PREVIEWS_ENABLED, linkPreviewsEnabled)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -79,7 +89,8 @@ public class MultiDeviceConfigurationUpdateJob extends ContextJob implements Inj
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.of(readReceiptsEnabled),
|
||||
Optional.of(unidentifiedDeliveryIndicatorsEnabled),
|
||||
Optional.of(typingIndicatorsEnabled))),
|
||||
Optional.of(typingIndicatorsEnabled),
|
||||
Optional.of(linkPreviewsEnabled))),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,7 @@ public class MultiDeviceReadReceiptUpdateJob extends ContextJob implements Injec
|
||||
return;
|
||||
}
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.of(enabled), Optional.absent(), Optional.absent())),
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.of(enabled), Optional.absent(), Optional.absent(), Optional.absent())),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
|
@ -9,8 +9,11 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
|
||||
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
|
||||
import org.signal.libsignal.metadata.ProtocolDuplicateMessageException;
|
||||
@ -53,6 +56,8 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
@ -80,6 +85,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||
@ -227,7 +233,7 @@ public class PushDecryptJob extends ContextJob {
|
||||
|
||||
if (content.getDataMessage().isPresent()) {
|
||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent();
|
||||
|
||||
if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
|
||||
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
|
||||
@ -484,6 +490,7 @@ public class PushDecryptJob extends ContextJob {
|
||||
message.getGroupInfo(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
|
||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
@ -518,7 +525,7 @@ public class PushDecryptJob extends ContextJob {
|
||||
threadId = GroupMessageProcessor.process(context, content, message.getMessage(), true);
|
||||
} else if (message.getMessage().isExpirationUpdate()) {
|
||||
threadId = handleSynchronizeSentExpirationUpdate(message);
|
||||
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent()) {
|
||||
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent()) {
|
||||
threadId = handleSynchronizeSentMediaMessage(message);
|
||||
} else {
|
||||
threadId = handleSynchronizeSentTextMessage(message);
|
||||
@ -581,7 +588,8 @@ public class PushDecryptJob extends ContextJob {
|
||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
||||
TextSecurePreferences.isReadReceiptsEnabled(getContext()),
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext())));
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
}
|
||||
}
|
||||
|
||||
@ -617,18 +625,20 @@ public class PushDecryptJob extends ContextJob {
|
||||
notifyTypingStoppedFromIncomingMessage(getMessageDestination(content, message), content.getSender(), content.getSenderDevice());
|
||||
|
||||
try {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Address.fromExternal(context, content.getSender()),
|
||||
message.getTimestamp(), -1,
|
||||
message.getExpiresInSeconds() * 1000L, false,
|
||||
content.isNeedsReceipt(),
|
||||
message.getBody(),
|
||||
message.getGroupInfo(),
|
||||
message.getAttachments(),
|
||||
quote,
|
||||
sharedContacts);
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
|
||||
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Address.fromExternal(context, content.getSender()),
|
||||
message.getTimestamp(), -1,
|
||||
message.getExpiresInSeconds() * 1000L, false,
|
||||
content.isNeedsReceipt(),
|
||||
message.getBody(),
|
||||
message.getGroupInfo(),
|
||||
message.getAttachments(),
|
||||
quote,
|
||||
sharedContacts,
|
||||
linkPreviews);
|
||||
|
||||
Optional<InsertResult> insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
|
||||
@ -673,17 +683,19 @@ public class PushDecryptJob extends ContextJob {
|
||||
private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message)
|
||||
throws MmsException
|
||||
{
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Recipient recipients = getSyncMessageDestination(message);
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
|
||||
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(),
|
||||
PointerAttachment.forPointers(message.getMessage().getAttachments()),
|
||||
message.getTimestamp(), -1,
|
||||
message.getMessage().getExpiresInSeconds() * 1000,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(),
|
||||
sharedContacts.or(Collections.emptyList()),
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Recipient recipients = getSyncMessageDestination(message);
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
|
||||
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().or(""));
|
||||
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(),
|
||||
PointerAttachment.forPointers(message.getMessage().getAttachments()),
|
||||
message.getTimestamp(), -1,
|
||||
message.getMessage().getExpiresInSeconds() * 1000,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(),
|
||||
sharedContacts.or(Collections.emptyList()),
|
||||
previews.or(Collections.emptyList()),
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
||||
|
||||
@ -784,7 +796,7 @@ public class PushDecryptJob extends ContextJob {
|
||||
long messageId;
|
||||
|
||||
if (isGroup) {
|
||||
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, ThreadDatabase.DistributionTypes.DEFAULT, null, Collections.emptyList());
|
||||
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, ThreadDatabase.DistributionTypes.DEFAULT, null, Collections.emptyList(), Collections.emptyList());
|
||||
outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage);
|
||||
|
||||
messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, null);
|
||||
@ -1003,7 +1015,14 @@ public class PushDecryptJob extends ContextJob {
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
|
||||
if (message.isMms()) {
|
||||
attachments = ((MmsMessageRecord) message).getSlideDeck().asAttachments();
|
||||
MmsMessageRecord mmsMessage = (MmsMessageRecord) message;
|
||||
attachments = mmsMessage.getSlideDeck().asAttachments();
|
||||
if (attachments.isEmpty()) {
|
||||
attachments.addAll(Stream.of(mmsMessage.getLinkPreviews())
|
||||
.filter(lp -> lp.getThumbnail().isPresent())
|
||||
.map(lp -> lp.getThumbnail().get())
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), false, attachments));
|
||||
@ -1029,6 +1048,30 @@ public class PushDecryptJob extends ContextJob {
|
||||
return Optional.of(contacts);
|
||||
}
|
||||
|
||||
private Optional<List<LinkPreview>> getLinkPreviews(Optional<List<Preview>> previews, @NonNull String message) {
|
||||
if (!previews.isPresent()) return Optional.absent();
|
||||
|
||||
List<LinkPreview> linkPreviews = new ArrayList<>(previews.get().size());
|
||||
|
||||
for (Preview preview : previews.get()) {
|
||||
Optional<Attachment> thumbnail = PointerAttachment.forPointer(preview.getImage());
|
||||
Optional<String> url = Optional.fromNullable(preview.getUrl());
|
||||
Optional<String> title = Optional.fromNullable(preview.getTitle());
|
||||
boolean hasContent = !TextUtils.isEmpty(title.or("")) || thumbnail.isPresent();
|
||||
boolean presentInBody = url.isPresent() && LinkPreviewUtil.findWhitelistedUrls(message).contains(url.get());
|
||||
boolean validDomain = url.isPresent() && LinkPreviewUtil.isWhitelistedLinkUrl(url.get());
|
||||
|
||||
if (hasContent && presentInBody && validDomain) {
|
||||
LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), thumbnail);
|
||||
linkPreviews.add(linkPreview);
|
||||
} else {
|
||||
Log.w(TAG, String.format("Discarding an invalid link preview. hasContent: %b presentInBody: %b validDomain: %b", hasContent, presentInBody, validDomain));
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.of(linkPreviews);
|
||||
}
|
||||
|
||||
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp) {
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
IncomingTextMessage textMessage = new IncomingTextMessage(Address.fromExternal(context, sender),
|
||||
|
@ -41,6 +41,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Quote;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
@ -50,6 +51,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupC
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -93,16 +95,15 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination, @Nullable Address filterAddress) {
|
||||
try {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
|
||||
if (message.isGroup()) {
|
||||
Log.i(TAG, "Group update message. Using legacy attachment upload path.");
|
||||
jobManager.add(new PushGroupSendJob(context, messageId, destination, filterAddress));
|
||||
return;
|
||||
}
|
||||
attachments.addAll(message.getAttachments());
|
||||
attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList());
|
||||
attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList());
|
||||
|
||||
List<AttachmentUploadJob> attachmentJobs = Stream.of(message.getAttachments()).map(a -> new AttachmentUploadJob(context, ((DatabaseAttachment) a).getAttachmentId())).toList();
|
||||
List<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(context, ((DatabaseAttachment) a).getAttachmentId())).toList();
|
||||
ChainParameters chainParams = new ChainParameters.Builder().setGroupId(destination.serialize()).build();
|
||||
|
||||
if (attachmentJobs.isEmpty()) {
|
||||
@ -237,7 +238,9 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
Optional<Quote> quote = getQuoteFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
List<Preview> previews = getPreviewsFor(message);
|
||||
List<SignalServiceAddress> addresses = Stream.of(destinations).map(this::getPushAddress).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(message.getAttachments());
|
||||
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(addresses)
|
||||
.map(address -> Address.fromSerialized(address.getNumber()))
|
||||
@ -246,13 +249,9 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
.toList();
|
||||
|
||||
if (message.isGroup()) {
|
||||
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
|
||||
List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments());
|
||||
List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(scaledAttachments);
|
||||
|
||||
OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
|
||||
GroupContext groupContext = groupMessage.getGroupContext();
|
||||
SignalServiceAttachment avatar = attachmentStreams.isEmpty() ? null : attachmentStreams.get(0);
|
||||
SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0);
|
||||
SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE;
|
||||
SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupContext.getName(), groupContext.getMembersList(), avatar);
|
||||
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
|
||||
@ -263,8 +262,6 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
return messageSender.sendMessage(addresses, unidentifiedAccess, groupDataMessage);
|
||||
} else {
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(message.getAttachments());
|
||||
|
||||
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedId(groupId));
|
||||
SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
@ -276,6 +273,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.withPreviews(previews)
|
||||
.build();
|
||||
|
||||
return messageSender.sendMessage(addresses, unidentifiedAccess, groupMessage);
|
||||
|
@ -7,9 +7,11 @@ import android.support.annotation.WorkerThread;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
@ -18,6 +20,7 @@ import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.ChainParameters;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
@ -32,12 +35,14 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@ -69,9 +74,15 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) {
|
||||
try {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
List<AttachmentUploadJob> attachmentJobs = Stream.of(message.getAttachments()).map(a -> new AttachmentUploadJob(context, ((DatabaseAttachment) a).getAttachmentId())).toList();
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
|
||||
attachments.addAll(message.getAttachments());
|
||||
attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList());
|
||||
attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList());
|
||||
|
||||
List<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(context, ((DatabaseAttachment) a).getAttachmentId())).toList();
|
||||
ChainParameters chainParams = new ChainParameters.Builder().setGroupId(destination.serialize()).build();
|
||||
|
||||
if (attachmentJobs.isEmpty()) {
|
||||
@ -191,6 +202,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
Optional<SignalServiceDataMessage.Quote> quote = getQuoteFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
List<Preview> previews = getPreviewsFor(message);
|
||||
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withBody(message.getBody())
|
||||
.withAttachments(serviceAttachments)
|
||||
@ -199,6 +211,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.withPreviews(previews)
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.build();
|
||||
|
||||
|
@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
@ -36,6 +37,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
@ -54,7 +56,7 @@ public abstract class PushSendJob extends SendJob {
|
||||
private static final String TAG = PushSendJob.class.getSimpleName();
|
||||
private static final long CERTIFICATE_EXPIRATION_BUFFER = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
protected PushSendJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
||||
public PushSendJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
||||
super(context, workerParameters);
|
||||
}
|
||||
|
||||
@ -247,6 +249,13 @@ public abstract class PushSendJob extends SendJob {
|
||||
return sharedContacts;
|
||||
}
|
||||
|
||||
List<Preview> getPreviewsFor(OutgoingMediaMessage mediaMessage) {
|
||||
return Stream.of(mediaMessage.getLinkPreviews()).map(lp -> {
|
||||
SignalServiceAttachment attachment = lp.getThumbnail().isPresent() ? getAttachmentPointerFor(lp.getThumbnail().get()) : null;
|
||||
return new Preview(lp.getUrl(), lp.getTitle(), Optional.fromNullable(attachment));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
protected void rotateSenderCertificateIfNecessary() throws IOException {
|
||||
try {
|
||||
byte[] certificateBytes = TextSecurePreferences.getUnidentifiedAccessCertificate(context);
|
||||
|
78
src/org/thoughtcrime/securesms/linkpreview/LinkPreview.java
Normal file
78
src/org/thoughtcrime/securesms/linkpreview/LinkPreview.java
Normal file
@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.linkpreview;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class LinkPreview {
|
||||
|
||||
@JsonProperty
|
||||
private final String url;
|
||||
|
||||
@JsonProperty
|
||||
private final String title;
|
||||
|
||||
@JsonProperty
|
||||
private final AttachmentId attachmentId;
|
||||
|
||||
@JsonIgnore
|
||||
private final Optional<Attachment> thumbnail;
|
||||
|
||||
public LinkPreview(@NonNull String url, @NonNull String title, @NonNull DatabaseAttachment thumbnail) {
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.thumbnail = Optional.of(thumbnail);
|
||||
this.attachmentId = thumbnail.getAttachmentId();
|
||||
}
|
||||
|
||||
public LinkPreview(@NonNull String url, @NonNull String title, @NonNull Optional<Attachment> thumbnail) {
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.thumbnail = thumbnail;
|
||||
this.attachmentId = null;
|
||||
}
|
||||
|
||||
public LinkPreview(@JsonProperty("url") @NonNull String url,
|
||||
@JsonProperty("title") @NonNull String title,
|
||||
@JsonProperty("attachmentId") @Nullable AttachmentId attachmentId)
|
||||
{
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.attachmentId = attachmentId;
|
||||
this.thumbnail = Optional.absent();
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public Optional<Attachment> getThumbnail() {
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
public @Nullable AttachmentId getAttachmentId() {
|
||||
return attachmentId;
|
||||
}
|
||||
|
||||
public String serialize() throws IOException {
|
||||
return JsonUtils.toJson(this);
|
||||
}
|
||||
|
||||
public static LinkPreview deserialize(@NonNull String serialized) throws IOException {
|
||||
return JsonUtils.fromJson(serialized, LinkPreview.class);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package org.thoughtcrime.securesms.linkpreview;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class LinkPreviewDomains {
|
||||
public static final Set<String> LINKS = new HashSet<>(Arrays.asList(
|
||||
"youtube.com",
|
||||
"www.youtube.com",
|
||||
"m.youtube.com",
|
||||
"youtu.be",
|
||||
"reddit.com",
|
||||
"www.reddit.com",
|
||||
"m.reddit.com",
|
||||
"imgur.com",
|
||||
"www.imgur.com",
|
||||
"m.imgur.com",
|
||||
"instagram.com",
|
||||
"www.instagram.com",
|
||||
"m.instagram.com"
|
||||
));
|
||||
|
||||
public static final Set<String> IMAGES = new HashSet<>(Arrays.asList(
|
||||
"ytimg.com",
|
||||
"cdninstagram.com",
|
||||
"redd.it",
|
||||
"imgur.com"
|
||||
));
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
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.load.resource.bitmap.DownsampleStrategy;
|
||||
import com.bumptech.glide.request.FutureTarget;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
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;
|
||||
import org.thoughtcrime.securesms.net.ContentProxySelector;
|
||||
import org.thoughtcrime.securesms.net.RequestController;
|
||||
import org.thoughtcrime.securesms.providers.MemoryBlobProvider;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
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;
|
||||
|
||||
import okhttp3.CacheControl;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class LinkPreviewRepository {
|
||||
|
||||
private static final String TAG = LinkPreviewRepository.class.getSimpleName();
|
||||
|
||||
private static final CacheControl NO_CACHE = new CacheControl.Builder().noCache().build();
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
public LinkPreviewRepository() {
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.proxySelector(new ContentProxySelector())
|
||||
.cache(null)
|
||||
.build();
|
||||
}
|
||||
|
||||
RequestController getLinkPreview(@NonNull Context context, @NonNull String url, @NonNull Callback<Optional<LinkPreview>> callback) {
|
||||
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;
|
||||
}
|
||||
|
||||
RequestController 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);
|
||||
});
|
||||
|
||||
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
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.w(TAG, "Request failed.", e);
|
||||
callback.onComplete(Metadata.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
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)
|
||||
.downsample(DownsampleStrategy.AT_MOST)
|
||||
.submit(1024, 1024);
|
||||
|
||||
RequestController controller = () -> bitmapFuture.cancel(false);
|
||||
|
||||
SignalExecutors.IO.execute(() -> {
|
||||
try {
|
||||
Bitmap bitmap = bitmapFuture.get();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
|
||||
|
||||
byte[] bytes = baos.toByteArray();
|
||||
Uri uri = MemoryBlobProvider.getInstance().createUri(bytes);
|
||||
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,
|
||||
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) {
|
||||
Pattern pattern = Pattern.compile("<\\s*meta\\s+property\\s*=\\s*\"\\s*og:" + property + "\\s*\"\\s+content\\s*=\\s*\"(.*?)\"\\s*/?\\s*>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback<T> {
|
||||
void onComplete(@NonNull T result);
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package org.thoughtcrime.securesms.linkpreview;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public final class LinkPreviewUtil {
|
||||
|
||||
/**
|
||||
* @return All whitelisted URLs in the source text.
|
||||
*/
|
||||
public static @NonNull List<String> findWhitelistedUrls(@NonNull String text) {
|
||||
SpannableString spannable = new SpannableString(text);
|
||||
boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS);
|
||||
|
||||
if (!found) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
|
||||
.map(URLSpan::getURL)
|
||||
.filter(LinkPreviewUtil::isWhitelistedLinkUrl)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the host is present in the link whitelist.
|
||||
*/
|
||||
public static boolean isWhitelistedLinkUrl(@NonNull String linkUrl) {
|
||||
HttpUrl url = HttpUrl.parse(linkUrl);
|
||||
return url != null &&
|
||||
!TextUtils.isEmpty(url.scheme()) &&
|
||||
"https".equals(url.scheme()) &&
|
||||
LinkPreviewDomains.LINKS.contains(url.host());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the top-level domain is present in the media whitelist.
|
||||
*/
|
||||
public static boolean isWhitelistedMediaUrl(@NonNull String mediaUrl) {
|
||||
HttpUrl url = HttpUrl.parse(mediaUrl);
|
||||
return url != null &&
|
||||
!TextUtils.isEmpty(url.scheme()) &&
|
||||
"https".equals(url.scheme()) &&
|
||||
LinkPreviewDomains.IMAGES.contains(url.topPrivateDomain());
|
||||
}
|
||||
}
|
@ -0,0 +1,191 @@
|
||||
package org.thoughtcrime.securesms.linkpreview;
|
||||
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.arch.lifecycle.ViewModelProvider;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.net.RequestController;
|
||||
import org.thoughtcrime.securesms.providers.MemoryBlobProvider;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class LinkPreviewViewModel extends ViewModel {
|
||||
|
||||
private final LinkPreviewRepository repository;
|
||||
private final MutableLiveData<LinkPreviewState> linkPreviewState;
|
||||
|
||||
private String activeUrl;
|
||||
private RequestController activeRequest;
|
||||
private boolean userCanceled;
|
||||
private Debouncer debouncer;
|
||||
|
||||
private LinkPreviewViewModel(@NonNull LinkPreviewRepository repository) {
|
||||
this.repository = repository;
|
||||
this.linkPreviewState = new MutableLiveData<>();
|
||||
this.debouncer = new Debouncer(250);
|
||||
}
|
||||
|
||||
public LiveData<LinkPreviewState> getLinkPreviewState() {
|
||||
return linkPreviewState;
|
||||
}
|
||||
|
||||
public boolean hasLinkPreview() {
|
||||
return linkPreviewState.getValue() != null && linkPreviewState.getValue().getLinkPreview().isPresent();
|
||||
}
|
||||
|
||||
public @NonNull List<LinkPreview> getPersistedLinkPreviews(@NonNull Context context) {
|
||||
final LinkPreviewState state = linkPreviewState.getValue();
|
||||
if (state == null || !state.getLinkPreview().isPresent()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (!state.getLinkPreview().get().getThumbnail().isPresent() || state.getLinkPreview().get().getThumbnail().get().getDataUri() == null) {
|
||||
return Collections.singletonList(state.getLinkPreview().get());
|
||||
}
|
||||
|
||||
LinkPreview originalPreview = state.getLinkPreview().get();
|
||||
Attachment originalAttachment = originalPreview.getThumbnail().get();
|
||||
Uri memoryUri = originalAttachment.getDataUri();
|
||||
byte[] imageBlob = MemoryBlobProvider.getInstance().getBlob(memoryUri);
|
||||
Uri diskUri = PersistentBlobProvider.getInstance(context).create(context, imageBlob, MediaUtil.IMAGE_JPEG, null);
|
||||
Attachment newAttachment = new UriAttachment(diskUri,
|
||||
diskUri,
|
||||
originalAttachment.getContentType(),
|
||||
originalAttachment.getTransferState(),
|
||||
originalAttachment.getSize(),
|
||||
originalAttachment.getWidth(),
|
||||
originalAttachment.getHeight(),
|
||||
originalAttachment.getFileName(),
|
||||
originalAttachment.getFastPreflightId(),
|
||||
originalAttachment.isVoiceNote(),
|
||||
originalAttachment.isQuote(),
|
||||
originalAttachment.getCaption());
|
||||
|
||||
MemoryBlobProvider.getInstance().delete(memoryUri);
|
||||
|
||||
return Collections.singletonList(new LinkPreview(originalPreview.getUrl(), originalPreview.getTitle(), Optional.of(newAttachment)));
|
||||
}
|
||||
|
||||
public void onTextChanged(@NonNull Context context, @NonNull String text) {
|
||||
debouncer.publish(() -> {
|
||||
if (userCanceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> urls = LinkPreviewUtil.findWhitelistedUrls(text);
|
||||
Optional<String> url = urls.isEmpty() ? Optional.absent() : Optional.of(urls.get(0));
|
||||
|
||||
if (url.isPresent() && url.get().equals(activeUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeRequest != null) {
|
||||
activeRequest.cancel();
|
||||
activeRequest = null;
|
||||
}
|
||||
|
||||
if (!url.isPresent()) {
|
||||
activeUrl = null;
|
||||
linkPreviewState.setValue(LinkPreviewState.forEmpty());
|
||||
return;
|
||||
}
|
||||
|
||||
linkPreviewState.setValue(LinkPreviewState.forLoading());
|
||||
|
||||
activeUrl = url.get();
|
||||
activeRequest = repository.getLinkPreview(context, url.get(), lp -> {
|
||||
Util.runOnMain(() -> {
|
||||
if (!userCanceled) {
|
||||
linkPreviewState.setValue(LinkPreviewState.forPreview(lp));
|
||||
}
|
||||
activeRequest = null;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public void onUserCancel() {
|
||||
if (activeRequest != null) {
|
||||
activeRequest.cancel();
|
||||
activeRequest = null;
|
||||
}
|
||||
|
||||
userCanceled = true;
|
||||
activeUrl = null;
|
||||
|
||||
debouncer.clear();
|
||||
linkPreviewState.setValue(LinkPreviewState.forEmpty());
|
||||
}
|
||||
|
||||
public void onEnabled() {
|
||||
userCanceled = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
if (activeRequest != null) {
|
||||
activeRequest.cancel();
|
||||
}
|
||||
|
||||
debouncer.clear();
|
||||
}
|
||||
|
||||
public static class LinkPreviewState {
|
||||
private final boolean isLoading;
|
||||
private final Optional<LinkPreview> linkPreview;
|
||||
|
||||
private LinkPreviewState(boolean isLoading, Optional<LinkPreview> linkPreview) {
|
||||
this.isLoading = isLoading;
|
||||
this.linkPreview = linkPreview;
|
||||
}
|
||||
|
||||
private static LinkPreviewState forLoading() {
|
||||
return new LinkPreviewState(true, Optional.absent());
|
||||
}
|
||||
|
||||
private static LinkPreviewState forPreview(@NonNull Optional<LinkPreview> linkPreview) {
|
||||
return new LinkPreviewState(false, linkPreview);
|
||||
}
|
||||
|
||||
private static LinkPreviewState forEmpty() {
|
||||
return new LinkPreviewState(false, Optional.absent());
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return isLoading;
|
||||
}
|
||||
|
||||
public Optional<LinkPreview> getLinkPreview() {
|
||||
return linkPreview;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
private final LinkPreviewRepository repository;
|
||||
|
||||
public Factory(@NonNull LinkPreviewRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return modelClass.cast(new LinkPreviewViewModel(repository));
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
@ -26,8 +27,9 @@ public class IncomingMediaMessage {
|
||||
private final QuoteModel quote;
|
||||
private final boolean unidentified;
|
||||
|
||||
private final List<Attachment> attachments = new LinkedList<>();
|
||||
private final List<Contact> sharedContacts = new LinkedList<>();
|
||||
private final List<Attachment> attachments = new LinkedList<>();
|
||||
private final List<Contact> sharedContacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
|
||||
public IncomingMediaMessage(Address from,
|
||||
Optional<Address> groupId,
|
||||
@ -63,7 +65,8 @@ public class IncomingMediaMessage {
|
||||
Optional<SignalServiceGroup> group,
|
||||
Optional<List<SignalServiceAttachment>> attachments,
|
||||
Optional<QuoteModel> quote,
|
||||
Optional<List<Contact>> sharedContacts)
|
||||
Optional<List<Contact>> sharedContacts,
|
||||
Optional<List<LinkPreview>> linkPreviews)
|
||||
{
|
||||
this.push = true;
|
||||
this.from = from;
|
||||
@ -80,6 +83,7 @@ public class IncomingMediaMessage {
|
||||
|
||||
this.attachments.addAll(PointerAttachment.forPointers(attachments));
|
||||
this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
|
||||
this.linkPreviews.addAll(linkPreviews.or(Collections.emptyList()));
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
@ -130,6 +134,10 @@ public class IncomingMediaMessage {
|
||||
return sharedContacts;
|
||||
}
|
||||
|
||||
public List<LinkPreview> getLinkPreviews() {
|
||||
return linkPreviews;
|
||||
}
|
||||
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
|
@ -11,7 +11,8 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage
|
||||
|
||||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
|
||||
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList());
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -6,6 +6,7 @@ import android.support.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||
@ -24,11 +25,12 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
long sentTimeMillis,
|
||||
long expiresIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts)
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
throws IOException
|
||||
{
|
||||
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, quote, contacts);
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, quote, contacts, previews);
|
||||
|
||||
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
|
||||
}
|
||||
@ -39,12 +41,13 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
long sentTimeMillis,
|
||||
long expireIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts)
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
System.currentTimeMillis(),
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts);
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
||||
|
||||
this.group = group;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.LinkedList;
|
||||
@ -27,6 +28,7 @@ public class OutgoingMediaMessage {
|
||||
private final List<NetworkFailure> networkFailures = new LinkedList<>();
|
||||
private final List<IdentityKeyMismatch> identityKeyMismatches = new LinkedList<>();
|
||||
private final List<Contact> contacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, String message,
|
||||
List<Attachment> attachments, long sentTimeMillis,
|
||||
@ -34,6 +36,7 @@ public class OutgoingMediaMessage {
|
||||
int distributionType,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
@NonNull List<NetworkFailure> networkFailures,
|
||||
@NonNull List<IdentityKeyMismatch> identityKeyMismatches)
|
||||
{
|
||||
@ -47,18 +50,22 @@ public class OutgoingMediaMessage {
|
||||
this.outgoingQuote = outgoingQuote;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
this.linkPreviews.addAll(linkPreviews);
|
||||
this.networkFailures.addAll(networkFailures);
|
||||
this.identityKeyMismatches.addAll(identityKeyMismatches);
|
||||
}
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, SlideDeck slideDeck, String message, long sentTimeMillis, int subscriptionId, long expiresIn, int distributionType, @Nullable QuoteModel outgoingQuote, @NonNull List<Contact> contacts)
|
||||
public OutgoingMediaMessage(Recipient recipient, SlideDeck slideDeck, String message,
|
||||
long sentTimeMillis, int subscriptionId, long expiresIn,
|
||||
int distributionType, @Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts, @NonNull List<LinkPreview> linkPreviews)
|
||||
{
|
||||
this(recipient,
|
||||
buildMessage(slideDeck, message),
|
||||
slideDeck.asAttachments(),
|
||||
sentTimeMillis, subscriptionId,
|
||||
expiresIn, distributionType, outgoingQuote,
|
||||
contacts, new LinkedList<>(), new LinkedList<>());
|
||||
contacts, linkPreviews, new LinkedList<>(), new LinkedList<>());
|
||||
}
|
||||
|
||||
public OutgoingMediaMessage(OutgoingMediaMessage that) {
|
||||
@ -74,6 +81,7 @@ public class OutgoingMediaMessage {
|
||||
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
|
||||
this.networkFailures.addAll(that.networkFailures);
|
||||
this.contacts.addAll(that.contacts);
|
||||
this.linkPreviews.addAll(that.linkPreviews);
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
@ -124,6 +132,10 @@ public class OutgoingMediaMessage {
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public @NonNull List<LinkPreview> getLinkPreviews() {
|
||||
return linkPreviews;
|
||||
}
|
||||
|
||||
public @NonNull List<NetworkFailure> getNetworkFailures() {
|
||||
return networkFailures;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
@ -18,9 +19,10 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
||||
int distributionType,
|
||||
long expiresIn,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts)
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts, Collections.emptyList(), Collections.emptyList());
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
||||
|
@ -23,14 +23,14 @@ import com.bumptech.glide.module.AppGlideModule;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyPaddedUrl;
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.glide.ContactPhotoLoader;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapCacheDecoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedGifCacheDecoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder;
|
||||
import org.thoughtcrime.securesms.glide.GiphyPaddedUrlLoader;
|
||||
import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader;
|
||||
import org.thoughtcrime.securesms.glide.OkHttpUrlLoader;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
@ -68,7 +68,7 @@ public class SignalGlideModule extends AppGlideModule {
|
||||
registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context));
|
||||
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
||||
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
|
||||
registry.append(GiphyPaddedUrl.class, InputStream.class, new GiphyPaddedUrlLoader.Factory());
|
||||
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
|
||||
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms.net;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import okhttp3.Call;
|
||||
|
||||
public class CallRequestController implements RequestController {
|
||||
|
||||
private final Call call;
|
||||
|
||||
private InputStream stream;
|
||||
private boolean canceled;
|
||||
|
||||
public CallRequestController(@NonNull Call call) {
|
||||
this.call = call;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
synchronized (CallRequestController.this) {
|
||||
call.cancel();
|
||||
|
||||
if (stream != null) {
|
||||
Util.close(stream);
|
||||
}
|
||||
|
||||
canceled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public synchronized void setStream(@NonNull InputStream stream) {
|
||||
if (canceled) {
|
||||
Util.close(stream);
|
||||
} else {
|
||||
this.stream = stream;
|
||||
}
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks until the stream is available or until the request is canceled.
|
||||
*/
|
||||
@WorkerThread
|
||||
public synchronized Optional<InputStream> getStream() {
|
||||
while(stream == null && !canceled) {
|
||||
Util.wait(this, 0);
|
||||
}
|
||||
|
||||
return Optional.fromNullable(this.stream);
|
||||
}
|
||||
}
|
350
src/org/thoughtcrime/securesms/net/ChunkedDataFetcher.java
Normal file
350
src/org/thoughtcrime/securesms/net/ChunkedDataFetcher.java
Normal file
@ -0,0 +1,350 @@
|
||||
package org.thoughtcrime.securesms.net;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.util.ContentLengthInputStream;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.CacheControl;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ChunkedDataFetcher {
|
||||
|
||||
private static final String TAG = ChunkedDataFetcher.class.getSimpleName();
|
||||
|
||||
private static final CacheControl NO_CACHE = new CacheControl.Builder().noCache().build();
|
||||
|
||||
private static final long MB = 1024 * 1024;
|
||||
private static final long KB = 1024;
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
public ChunkedDataFetcher(@NonNull OkHttpClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public RequestController fetch(@NonNull String url, long contentLength, @NonNull Callback callback) {
|
||||
if (contentLength <= 0) {
|
||||
return fetch(url, callback);
|
||||
}
|
||||
|
||||
CompositeRequestController compositeController = new CompositeRequestController();
|
||||
fetchChunks(url, contentLength, compositeController, callback);
|
||||
return compositeController;
|
||||
}
|
||||
|
||||
public RequestController fetch(@NonNull String url, @NonNull Callback callback) {
|
||||
CompositeRequestController compositeController = new CompositeRequestController();
|
||||
|
||||
Call headCall = client.newCall(new Request.Builder().url(url).head().cacheControl(NO_CACHE).build());
|
||||
compositeController.addController(new CallRequestController(headCall));
|
||||
|
||||
headCall.enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
if (!compositeController.isCanceled()) {
|
||||
callback.onFailure(e);
|
||||
compositeController.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
String contentLength = response.header("Content-Length");
|
||||
String acceptRanges = response.header("Accept-Ranges");
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
Log.w(TAG, "Non-successful response code: " + response.code());
|
||||
callback.onFailure(new IOException("Non-successful response code: " + response.code()));
|
||||
compositeController.cancel();
|
||||
if (response.body() != null) response.body().close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(contentLength)) {
|
||||
Log.w(TAG, "Missing Content-Length header.");
|
||||
callback.onFailure(new IOException("Missing Content-Length header."));
|
||||
compositeController.cancel();
|
||||
if (response.body() != null) response.body().close();
|
||||
return;
|
||||
}
|
||||
|
||||
long parsedContentLength;
|
||||
try {
|
||||
parsedContentLength = Long.parseLong(contentLength);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Invalid Content-Length value.");
|
||||
callback.onFailure(new IOException("Invalid Content-Length value."));
|
||||
compositeController.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.body() != null) {
|
||||
response.body().close();
|
||||
}
|
||||
|
||||
fetchChunks(url, parsedContentLength, compositeController, callback);
|
||||
}
|
||||
});
|
||||
|
||||
return compositeController;
|
||||
}
|
||||
|
||||
private void fetchChunks(@NonNull String url, long contentLength, CompositeRequestController compositeController, Callback callback) {
|
||||
List<ByteRange> requestPattern;
|
||||
try {
|
||||
requestPattern = getRequestPattern(contentLength);
|
||||
} catch (IOException e) {
|
||||
callback.onFailure(e);
|
||||
compositeController.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
SignalExecutors.IO.execute(() -> {
|
||||
List<CallRequestController> controllers = Stream.of(requestPattern).map(range -> makeChunkRequest(client, url, range)).toList();
|
||||
List<InputStream> streams = new ArrayList<>(controllers.size());
|
||||
|
||||
Stream.of(controllers).forEach(compositeController::addController);
|
||||
|
||||
for (CallRequestController controller : controllers) {
|
||||
Optional<InputStream> stream = controller.getStream();
|
||||
|
||||
if (!stream.isPresent()) {
|
||||
Log.w(TAG, "Stream was canceled.");
|
||||
callback.onFailure(new IOException("Failure"));
|
||||
compositeController.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
streams.add(stream.get());
|
||||
}
|
||||
|
||||
try {
|
||||
callback.onSuccess(new InputStreamList(streams));
|
||||
} catch (IOException e) {
|
||||
callback.onFailure(e);
|
||||
compositeController.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CallRequestController makeChunkRequest(@NonNull OkHttpClient client, @NonNull String url, @NonNull ByteRange range) {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.cacheControl(NO_CACHE)
|
||||
.addHeader("Range", "bytes=" + range.start + "-" + range.end)
|
||||
.addHeader("Accept-Encoding", "identity")
|
||||
.build();
|
||||
|
||||
Call call = client.newCall(request);
|
||||
CallRequestController callController = new CallRequestController(call);
|
||||
|
||||
call.enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
callController.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
if (!response.isSuccessful()) {
|
||||
callController.cancel();
|
||||
if (response.body() != null) response.body().close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.body() == null) {
|
||||
callController.cancel();
|
||||
if (response.body() != null) response.body().close();
|
||||
return;
|
||||
}
|
||||
|
||||
InputStream stream = new SkippingInputStream(ContentLengthInputStream.obtain(response.body().byteStream(), response.body().contentLength()), range.ignoreFirst);
|
||||
callController.setStream(stream);
|
||||
}
|
||||
});
|
||||
|
||||
return callController;
|
||||
}
|
||||
|
||||
private List<ByteRange> getRequestPattern(long size) throws IOException {
|
||||
if (size > MB) return getRequestPattern(size, MB);
|
||||
else if (size > 500 * KB) return getRequestPattern(size, 500 * KB);
|
||||
else if (size > 100 * KB) return getRequestPattern(size, 100 * KB);
|
||||
else if (size > 50 * KB) return getRequestPattern(size, 50 * KB);
|
||||
else if (size > 10 * KB) return getRequestPattern(size, 10 * KB);
|
||||
else if (size > KB) return getRequestPattern(size, KB);
|
||||
|
||||
throw new IOException("Unsupported size: " + size);
|
||||
}
|
||||
|
||||
private List<ByteRange> getRequestPattern(long size, long increment) {
|
||||
List<ByteRange> results = new LinkedList<>();
|
||||
|
||||
long offset = 0;
|
||||
|
||||
while (size - offset > increment) {
|
||||
results.add(new ByteRange(offset, offset + increment - 1, 0));
|
||||
offset += increment;
|
||||
}
|
||||
|
||||
if (size - offset > 0) {
|
||||
results.add(new ByteRange(size - increment, size-1, increment - (size - offset)));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static class ByteRange {
|
||||
private final long start;
|
||||
private final long end;
|
||||
private final long ignoreFirst;
|
||||
|
||||
private ByteRange(long start, long end, long ignoreFirst) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.ignoreFirst = ignoreFirst;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SkippingInputStream extends FilterInputStream {
|
||||
|
||||
private long skip;
|
||||
|
||||
SkippingInputStream(InputStream in, long skip) {
|
||||
super(in);
|
||||
this.skip = skip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (skip != 0) {
|
||||
skipFully(skip);
|
||||
skip = 0;
|
||||
}
|
||||
|
||||
return super.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] buffer) throws IOException {
|
||||
if (skip != 0) {
|
||||
skipFully(skip);
|
||||
skip = 0;
|
||||
}
|
||||
|
||||
return super.read(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {
|
||||
if (skip != 0) {
|
||||
skipFully(skip);
|
||||
skip = 0;
|
||||
}
|
||||
|
||||
return super.read(buffer, offset, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return Util.toIntExact(super.available() - skip);
|
||||
}
|
||||
|
||||
private void skipFully(long amount) throws IOException {
|
||||
byte[] buffer = new byte[4096];
|
||||
|
||||
while (amount > 0) {
|
||||
int read = super.read(buffer, 0, Math.min(buffer.length, Util.toIntExact(amount)));
|
||||
|
||||
if (read != -1) amount -= read;
|
||||
else return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class InputStreamList extends InputStream {
|
||||
|
||||
private final List<InputStream> inputStreams;
|
||||
|
||||
private int currentStreamIndex = 0;
|
||||
|
||||
InputStreamList(List<InputStream> inputStreams) {
|
||||
this.inputStreams = inputStreams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
while (currentStreamIndex < inputStreams.size()) {
|
||||
int result = inputStreams.get(currentStreamIndex).read();
|
||||
|
||||
if (result == -1) currentStreamIndex++;
|
||||
else return result;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] buffer, int offset, int length) throws IOException {
|
||||
while (currentStreamIndex < inputStreams.size()) {
|
||||
int result = inputStreams.get(currentStreamIndex).read(buffer, offset, length);
|
||||
|
||||
if (result == -1) currentStreamIndex++;
|
||||
else return result;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] buffer) throws IOException {
|
||||
return read(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
for (InputStream stream : inputStreams) {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
int total = 0;
|
||||
|
||||
for (int i=currentStreamIndex;i<inputStreams.size();i++) {
|
||||
try {
|
||||
int available = inputStreams.get(i).available();
|
||||
|
||||
if (available != -1) total += available;
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onSuccess(InputStream stream) throws IOException;
|
||||
void onFailure(Exception e);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.net;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CompositeRequestController implements RequestController {
|
||||
|
||||
private final List<RequestController> controllers = new ArrayList<>();
|
||||
private boolean canceled = false;
|
||||
|
||||
public synchronized void addController(@NonNull RequestController controller) {
|
||||
if (canceled) {
|
||||
controller.cancel();
|
||||
} else {
|
||||
controllers.add(controller);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void cancel() {
|
||||
canceled = true;
|
||||
Stream.of(controllers).forEach(RequestController::cancel);
|
||||
}
|
||||
|
||||
public synchronized boolean isCanceled() {
|
||||
return canceled;
|
||||
}
|
||||
}
|
84
src/org/thoughtcrime/securesms/net/ContentProxySelector.java
Normal file
84
src/org/thoughtcrime/securesms/net/ContentProxySelector.java
Normal file
@ -0,0 +1,84 @@
|
||||
package org.thoughtcrime.securesms.net;
|
||||
|
||||
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewDomains;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class ContentProxySelector extends ProxySelector {
|
||||
|
||||
private static final String TAG = ContentProxySelector.class.getSimpleName();
|
||||
|
||||
public static final Set<String> WHITELISTED_DOMAINS = new HashSet<>();
|
||||
static {
|
||||
WHITELISTED_DOMAINS.addAll(LinkPreviewDomains.LINKS);
|
||||
WHITELISTED_DOMAINS.addAll(LinkPreviewDomains.IMAGES);
|
||||
WHITELISTED_DOMAINS.add("giphy.com");
|
||||
}
|
||||
|
||||
private final List<Proxy> EMPTY = new ArrayList<>(1);
|
||||
private volatile List<Proxy> CONTENT = null;
|
||||
|
||||
public ContentProxySelector() {
|
||||
EMPTY.add(Proxy.NO_PROXY);
|
||||
|
||||
if (Util.isMainThread()) {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
synchronized (ContentProxySelector.this) {
|
||||
initializeContentProxy();
|
||||
ContentProxySelector.this.notifyAll();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
initializeContentProxy();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
for (String domain : WHITELISTED_DOMAINS) {
|
||||
if (uri.getHost().endsWith(domain)) {
|
||||
return getOrCreateContentProxy();
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Tried to proxy a non-whitelisted domain.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress address, IOException failure) {
|
||||
Log.w(TAG, failure);
|
||||
}
|
||||
|
||||
private void initializeContentProxy() {
|
||||
CONTENT = new ArrayList<Proxy>(1) {{
|
||||
add(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(BuildConfig.CONTENT_PROXY_HOST,
|
||||
BuildConfig.CONTENT_PROXY_PORT)));
|
||||
}};
|
||||
}
|
||||
|
||||
private List<Proxy> getOrCreateContentProxy() {
|
||||
if (CONTENT == null) {
|
||||
synchronized (this) {
|
||||
while (CONTENT == null) Util.wait(this, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return CONTENT;
|
||||
}
|
||||
|
||||
}
|
10
src/org/thoughtcrime/securesms/net/RequestController.java
Normal file
10
src/org/thoughtcrime/securesms/net/RequestController.java
Normal file
@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.net;
|
||||
|
||||
public interface RequestController {
|
||||
|
||||
/**
|
||||
* Best-effort cancellation of any outstanding requests. Will also release any resources held by
|
||||
* the underlying request.
|
||||
*/
|
||||
void cancel();
|
||||
}
|
@ -76,7 +76,7 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver {
|
||||
|
||||
if (recipient.isGroupRecipient()) {
|
||||
Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message");
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
|
||||
} else {
|
||||
Log.w("AndroidAutoReplyReceiver", "Sending regular message ");
|
||||
|
@ -72,7 +72,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
|
||||
if (recipient.isGroupRecipient()) {
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
threadId = MessageSender.send(context, reply, -1, false, null);
|
||||
} else if (TextSecurePreferences.isPushRegistered(context) && recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
|
||||
OutgoingEncryptedMessage reply = new OutgoingEncryptedMessage(recipient, responseText.toString(), expiresIn);
|
||||
|
@ -66,6 +66,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
this.findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF).setOnPreferenceClickListener(new PassphraseIntervalClickListener());
|
||||
this.findPreference(TextSecurePreferences.READ_RECEIPTS_PREF).setOnPreferenceChangeListener(new ReadReceiptToggleListener());
|
||||
this.findPreference(TextSecurePreferences.TYPING_INDICATORS).setOnPreferenceChangeListener(new TypingIndicatorsToggleListener());
|
||||
this.findPreference(TextSecurePreferences.LINK_PREVIEWS).setOnPreferenceChangeListener(new LinkPreviewToggleListener());
|
||||
this.findPreference(PREFERENCE_CATEGORY_BLOCKED).setOnPreferenceClickListener(new BlockedContactsClickListener());
|
||||
this.findPreference(TextSecurePreferences.SHOW_UNIDENTIFIED_DELIVERY_INDICATORS).setOnPreferenceChangeListener(new ShowUnidentifiedDeliveryIndicatorsChangedListener());
|
||||
this.findPreference(TextSecurePreferences.UNIVERSAL_UNIDENTIFIED_ACCESS).setOnPreferenceChangeListener(new UniversalUnidentifiedAccessChangedListener());
|
||||
@ -189,7 +190,8 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
||||
enabled,
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext())));
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -200,11 +202,12 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
boolean enabled = (boolean)newValue;
|
||||
ApplicationContext.getInstance(getContext())
|
||||
.getJobManager()
|
||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
||||
TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
enabled,
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext())));
|
||||
.getJobManager()
|
||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
||||
TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
enabled,
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
|
||||
if (!enabled) {
|
||||
ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().clear();
|
||||
@ -214,6 +217,22 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
}
|
||||
}
|
||||
|
||||
private class LinkPreviewToggleListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
boolean enabled = (boolean)newValue;
|
||||
ApplicationContext.getInstance(requireContext())
|
||||
.getJobManager()
|
||||
.add(new MultiDeviceConfigurationUpdateJob(requireContext(),
|
||||
TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()),
|
||||
enabled));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static CharSequence getSummary(Context context) {
|
||||
final int privacySummaryResId = R.string.ApplicationPreferencesActivity_privacy_summary;
|
||||
final String onRes = context.getString(R.string.ApplicationPreferencesActivity_on);
|
||||
@ -307,11 +326,12 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
boolean enabled = (boolean) newValue;
|
||||
ApplicationContext.getInstance(getContext())
|
||||
.getJobManager()
|
||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
||||
TextSecurePreferences.isReadReceiptsEnabled(getContext()),
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(getContext()),
|
||||
enabled));
|
||||
.getJobManager()
|
||||
.add(new MultiDeviceConfigurationUpdateJob(getContext(),
|
||||
TextSecurePreferences.isReadReceiptsEnabled(getContext()),
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(getContext()),
|
||||
enabled,
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -43,6 +43,21 @@ public class MemoryBlobProvider {
|
||||
cache.remove(ContentUris.parseId(uri));
|
||||
}
|
||||
|
||||
public synchronized @NonNull byte[] getBlob(@NonNull Uri uri) {
|
||||
long id = ContentUris.parseId(uri);
|
||||
Entry entry = cache.get(ContentUris.parseId(uri));
|
||||
|
||||
if (entry == null) {
|
||||
throw new IllegalArgumentException("ID not found: " + id);
|
||||
}
|
||||
|
||||
if (entry.isSingleUse()) {
|
||||
cache.remove(id);
|
||||
}
|
||||
|
||||
return entry.getBlob();
|
||||
}
|
||||
|
||||
public synchronized @NonNull InputStream getStream(long id) throws IOException {
|
||||
Entry entry = cache.get(id);
|
||||
|
||||
|
@ -35,7 +35,7 @@ import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity;
|
||||
import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.LifecycleBoundTask;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
@ -230,7 +230,7 @@ public class ScribbleFragment extends Fragment implements ScribbleHud.EventListe
|
||||
if (resultCode == RESULT_OK && requestCode == SELECT_STICKER_REQUEST_CODE && data != null) {
|
||||
final String stickerFile = data.getStringExtra(StickerSelectActivity.EXTRA_STICKER_FILE);
|
||||
|
||||
LifecycleBoundTask.run(getLifecycle(), () -> {
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
try {
|
||||
return BitmapFactory.decodeStream(getContext().getAssets().open(stickerFile));
|
||||
} catch (IOException e) {
|
||||
|
@ -74,7 +74,7 @@ public class GroupUtil {
|
||||
.setType(GroupContext.Type.QUIT)
|
||||
.build();
|
||||
|
||||
return Optional.of(new OutgoingGroupMediaMessage(groupRecipient, groupContext, null, System.currentTimeMillis(), 0, null, Collections.emptyList()));
|
||||
return Optional.of(new OutgoingGroupMediaMessage(groupRecipient, groupContext, null, System.currentTimeMillis(), 0, null, Collections.emptyList(), Collections.emptyList()));
|
||||
}
|
||||
|
||||
|
||||
|
@ -173,6 +173,8 @@ public class TextSecurePreferences {
|
||||
|
||||
public static final String TYPING_INDICATORS = "pref_typing_indicators";
|
||||
|
||||
public static final String LINK_PREVIEWS = "pref_link_previews";
|
||||
|
||||
public static boolean isScreenLockEnabled(@NonNull Context context) {
|
||||
return getBooleanPreference(context, SCREEN_LOCK, false);
|
||||
}
|
||||
@ -346,6 +348,10 @@ public class TextSecurePreferences {
|
||||
setBooleanPreference(context, TYPING_INDICATORS, enabled);
|
||||
}
|
||||
|
||||
public static boolean isLinkPreviewsEnabled(Context context) {
|
||||
return getBooleanPreference(context, LINK_PREVIEWS, true);
|
||||
}
|
||||
|
||||
public static @Nullable String getProfileKey(Context context) {
|
||||
return getStringPreference(context, PROFILE_KEY_PREF, null);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class LifecycleBoundTask {
|
||||
public class SimpleTask {
|
||||
|
||||
/**
|
||||
* Runs a task in the background and passes the result of the computation to a task that is run
|
||||
@ -35,6 +35,17 @@ public class LifecycleBoundTask {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a task in the background and passes the result of the computation to a task that is run on
|
||||
* the main thread. Essentially {@link AsyncTask}, but lambda-compatible.
|
||||
*/
|
||||
public static <E> void run(@NonNull BackgroundTask<E> backgroundTask, @NonNull ForegroundTask<E> foregroundTask) {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
final E result = backgroundTask.run();
|
||||
Util.runOnMain(() -> foregroundTask.run(result));
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean isValid(@NonNull Lifecycle lifecycle) {
|
||||
return lifecycle.getCurrentState().isAtLeast(Lifecycle.State.CREATED);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user