Compare commits

..

35 Commits

Author SHA1 Message Date
Greyson Parrelli
9d21c36ddf Bump verstion to 4.34.8
Again, no changes. Needed to resubmit to the Play Store.
2019-02-21 18:04:44 -08:00
Greyson Parrelli
983290aa5b Bump version to 4.34.7
No changes. Necessary to resubmit to Google Play.
2019-02-21 12:57:55 -08:00
Greyson Parrelli
88b9fc25d2 Bump version to 4.34.6 2019-02-20 17:23:54 -08:00
Greyson Parrelli
60c7fb0056 Fix possible NPE. 2019-02-20 17:20:12 -08:00
Greyson Parrelli
fa6da1902f Fix button spinning after failed CAPTCHA. 2019-02-19 13:13:32 -08:00
Greyson Parrelli
5cc3ac00c7 Bump version to 4.34.5 2019-02-19 09:37:30 -08:00
Greyson Parrelli
33daa21ad9 Guard against devices not supporting mandatory ContentProvider columns.
The docs specify that this column is supposed to be present, but a
crash says it wasn't, so alas, here in goes the check.
2019-02-19 09:34:24 -08:00
Greyson Parrelli
c4d1bdc44d Bump version to 4.34.4
No changes. Necessary to resubmit to the Play Store.
2019-02-18 16:03:15 -08:00
Greyson Parrelli
ca99c732f8 Bump version to 4.34.3 2019-02-18 11:47:40 -08:00
Greyson Parrelli
1f79808cf0 Remove unneccesary FCM manifest attribute.
`firebase_analytics_collection_enabled` is used for temporarily
enabling/disabling analytics.

We already use `firebase_analytics_collection_deactivated`, which is
used for permanently disabling analytics.
2019-02-18 11:46:53 -08:00
Greyson Parrelli
5c0e1100ed Fix possible NPE in conversation menu.
Would occur if someone had previously muted a conversation with
themselves.
2019-02-16 11:45:09 -08:00
Greyson Parrelli
d0b763c16e Bump version to 4.34.2 2019-02-15 19:34:19 -08:00
Greyson Parrelli
b962751c96 Fix possible IllegalArgumentException during a database migration. 2019-02-15 19:33:16 -08:00
Greyson Parrelli
94e8553b73 Fix possible NPE during conversation load. 2019-02-15 19:24:23 -08:00
Greyson Parrelli
351b625975 Bump version to 4.34.1 2019-02-15 14:21:27 -08:00
Greyson Parrelli
a2b6dbda14 Correctly sync Note to Self conversation color. 2019-02-15 14:21:27 -08:00
Greyson Parrelli
a6564f8f84 FCM improvements. 2019-02-15 14:21:23 -08:00
Greyson Parrelli
4dbe165c18 Bump version to 4.34.0 2019-02-14 21:04:01 -08:00
Greyson Parrelli
f29a42411e Update WorkManager to beta05. 2019-02-14 20:19:07 -08:00
Greyson Parrelli
02b0800b22 Support requesting a CAPTCHA during registration. 2019-02-14 20:19:07 -08:00
Greyson Parrelli
2cfa431cad Supply a reason for CDS error reporting. 2019-02-14 20:19:07 -08:00
Greyson Parrelli
fe4068afce Don't preview links if your cursor is touching them. 2019-02-14 20:19:07 -08:00
Greyson Parrelli
1c23603c25 Add the Redmi Note 5 to the hardware AEC blacklist. 2019-02-14 20:19:07 -08:00
Greyson Parrelli
c2a86fcc74 Sync self-sends to desktop.
Updated UI to show self-conversations as "Note to Self".
2019-02-14 20:19:07 -08:00
Greyson Parrelli
d42c9b5dbc Ensure the group shortstring in the action bar is up-to-date.
There were situations where adding/removing members from a group
would update the group member list, but the short string (the little
text listing the first couple members of the group) wouldn't be updated
until you left the screen and came back.
2019-02-14 20:19:06 -08:00
Greyson Parrelli
3b6429c163 Don't unnecessarily stop the ShareActivity in onPause.
1. Due to ShareActivity having noHistory=true, it will already be
ditched when you leave the activity.
2. We only need to truly finish() here if we've dropped the underlying
media.

Fixes #8591
2019-02-14 20:19:06 -08:00
Greyson Parrelli
6896f8ea15 Properly check attachment size during media send.
Prevent users from trying to send videos that exceed the size limit.

Also, this commit properly populates height/width on media shared into
the app.

Fixes #8573
2019-02-14 20:19:06 -08:00
Greyson Parrelli
a3768c7d74 Fix StickyHeader measuring.
It didn't re-measure when pulling an item from the cache, screwing stuff
up after a phone rotation. Had a workaround for it for specific screens,
but this fixes the problem at the source.

Fixes #8583
2019-02-14 20:19:06 -08:00
Greyson Parrelli
c9a0a66f18 Migrate backup passphrase to be keystore-encrypted when available. 2019-02-14 20:19:06 -08:00
Greyson Parrelli
db1ad39c6b Fix issues with bundled notifications. 2019-02-14 20:19:06 -08:00
Greyson Parrelli
9f04c28bfd Implemented conversation search.
You can now search for messages within a specific conversation.
2019-02-14 20:19:01 -08:00
Greyson Parrelli
10631d7e71 Add a gradle.properties with increased memory size. 2019-02-14 14:29:12 -08:00
Greyson Parrelli
cfff10622a Move conversation classes to their own package. 2019-02-14 14:29:12 -08:00
Greyson Parrelli
b769c7d9b6 Properly batch contact inserts.
Fixes #8580
2019-02-14 14:29:12 -08:00
Greyson Parrelli
1e0f691a56 Updated to WebRTC M72. 2019-02-14 14:28:57 -08:00
97 changed files with 2161 additions and 555 deletions

View File

@@ -109,7 +109,6 @@
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
<meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />
<activity android:name="org.thoughtcrime.securesms.WebRtcCallActivity"
android:excludeFromRecents="true"
@@ -209,7 +208,7 @@
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
</activity>
<activity android:name=".ConversationActivity"
<activity android:name=".conversation.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -219,7 +218,7 @@
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
</activity>
<activity android:name=".ConversationPopupActivity"
<activity android:name=".conversation.ConversationPopupActivity"
android:windowSoftInputMode="stateVisible"
android:launchMode="singleTask"
android:taskAffinity=""
@@ -305,6 +304,12 @@
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".registration.CaptchaActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightNoActionBar"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DeviceActivity"
android:label="@string/AndroidManifest__linked_devices"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>

View File

@@ -15,7 +15,6 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.google.gms:google-services:4.0.2'
classpath files('libs/gradle-witness.jar')
}
}
@@ -78,10 +77,14 @@ dependencies {
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'
compile 'android.arch.work:work-runtime:1.0.0-beta03'
compile 'android.arch.work:work-runtime:1.0.0-beta05'
compile('com.google.firebase:firebase-messaging:17.3.4') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
compile 'com.google.firebase:firebase-messaging:17.3.4'
compile 'com.google.firebase:firebase-core:16.0.6'
compile 'com.google.android.gms:play-services-maps:16.0.0'
compile 'com.google.android.gms:play-services-places:16.0.0'
compile 'com.google.android.gms:play-services-auth:16.0.1'
@@ -89,8 +92,8 @@ 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.7'
compile 'org.whispersystems:webrtc-android:M71'
compile 'org.whispersystems:signal-service-android:2.12.8'
compile 'org.whispersystems:webrtc-android:M72-S2'
compile "me.leolin:ShortcutBadger:1.1.16"
compile 'se.emilsjolander:stickylistheaders:2.7.0'
@@ -177,18 +180,17 @@ dependencyVerification {
'com.android.support:exifinterface:bbf44e519edd6333a24a3285aa21fd00181b920b81ca8aa89a8899f03ab4d6b0',
'com.android.support.constraint:constraint-layout:27b4e5c0b80d3ff8b92f4c93b3b4d3ecf16c01589f4cdf70ca7cf64cb42d8122',
'com.android.support:multidex:ecf6098572e23b5155bab3b9a82b2fd1530eda6c6c157745e0f5287c66eec60c',
'android.arch.work:work-runtime:f428464342adeb412fd350a0c268134e92a13cf3d71c5d38180386c2b23fa694',
'android.arch.work:work-runtime:a84a016b20a82fb67c59a4081d383a185b0f2affcadde2f435df7565d6843816',
'android.arch.lifecycle:extensions:429426b2feec2245ffc5e75b3b5309bedb36159cf06dc71843ae43526ac289b6',
'android.arch.lifecycle:common-java8:7078b5c8ccb94203df9cc2a463c69cf0021596e6cf966d78fbfd697aaafe0630',
'com.google.firebase:firebase-messaging:e42288e7950d7d3b033d3395a5ac9365d230da3e439a2794ec13e2ef0fbaf078',
'com.google.firebase:firebase-core:07d1544aeed9690843858982ea5a69506038f94e93b5d031b741ba9164f6258a',
'com.google.android.gms:play-services-places:2d5c4e4ac3ee5be21b4ec544411bc51d11457b5ae2fa2a5d4539019f87c233c6',
'com.google.android.gms:play-services-maps:07f59c5955b759ce7b80ceaeb8261643c5b79acc9f180df2b7c3987658eed2e8',
'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:0afd2cb17ed920611dacc54385f3ed375847c10ecd7839a025d9c61c387f7678',
'org.whispersystems:webrtc-android:0620880760976d78ef429dc8b383136981b9a72178e5d70f5affa681deffed69',
'org.whispersystems:signal-service-android:68a349a9e05089f33ab5a9b9fc330526f59d31e8385ff9f5b70bc4a88bd0e297',
'org.whispersystems:webrtc-android:6b0a7e11c8d63e9a7ea523cd219247cf23e2919ce3411e7cd51e0f4446031597',
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
'com.jpardogo.materialtabstrip:library:c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa',
@@ -211,10 +213,6 @@ dependencyVerification {
'com.github.dmytrodanylyk.circular-progress-button:library:8dc6a29a5a8db7b2ad5a9a7fda1dc9ae0893f4c8f0545732b2c63854ea693e8e',
'org.signal:android-database-sqlcipher:33d4063336893af00b9d68b418e7b290cace74c20ce8aacffddc0911010d3d73',
'com.googlecode.ez-vcard:ez-vcard:7e24ad50b222d2f70ac91bdccfa3c0f6200b078d797cb784837f75e77bb4210f',
'com.google.firebase:firebase-measurement-connector-impl:eacaa68ed2c5c390b517267d7dae34268084d43b006db12682db88d17bbdc0ee',
'com.google.firebase:firebase-analytics:91a6b814b556779c223c80f52d0ca8ed48edbd4645b0d9104ac7b22639d5acf1',
'com.google.android.gms:play-services-measurement-api:a026fc70e777bcda3f7e51e68e331a03ed7a1143a7b3e3f67b99c21177a5b4d5',
'com.google.firebase:firebase-analytics-impl:dff7c79fe2dc3bef441057ae36678b51e27301f9b03377657170820bbe3c7441',
'com.google.firebase:firebase-iid:bb42774e309d5eac1aa493d19711032bee4f677a409639b6a5cfa93089af93eb',
'com.google.firebase:firebase-common:3db6bfd4c6f758551e5f9acdeada2050577277e6da1aefb2412de23829759bcf',
'com.google.android.gms:play-services-auth-api-phone:19365818b9ceb048ef48db12b5ffadd5eb86dbeb2c7c7b823bfdd89c665f42e5',
@@ -222,11 +220,8 @@ dependencyVerification {
'com.google.firebase:firebase-iid-interop:2a86322b9346fd4836219206d249e85803311655e96036a8e4b714ce7e79693b',
'com.google.android.gms:play-services-base:aca10c780c3219bc50f3db06734f4ab88badd3113c564c0a3156ff8ff674655b',
'com.google.android.gms:play-services-tasks:b31c18d8d1cc8d9814f295ee7435471333f370ba5bd904ca14f8f2bec4f35c35',
'com.google.firebase:firebase-measurement-connector:bc318110486ed738e1cc84d4b280e156b35a9a3964d678ee64930d846150d0c3',
'com.google.android.gms:play-services-places-placereport:04f8baeb1f8f8a734c7d4b1701a3974281b45591affa7e963b59dd019b8abc6e',
'com.google.android.gms:play-services-stats:5b2d8281adbfd6e74d2295c94bab9ea80fc9a84dfbb397995673f5af4d4c6368',
'com.google.android.gms:play-services-measurement-base:887ddc8b384108a35ff7a41c8bc5c653dcedb44d9d6e46110569f586898d3c1d',
'com.google.android.gms:play-services-ads-identifier:380b09bfc5389fff93b5719c04e57c99678c9c3af0402a91e26d89734babcc49',
'com.google.android.gms:play-services-basement:e08bfd1e87c4e50ef76161d7ac76b873aeb975367eeb3afa4abe62ea1887c7c6',
'com.android.support:support-v4:8b9031381c678d628c9e47b566ae1d161e1c9710f7855c759beeac7596cecf30',
'com.android.support:support-fragment:3772fc738ada86824ba1a4b3f197c3dbd67b7ddcfe2c9db1de95ef2e3487a915',
@@ -268,7 +263,7 @@ dependencyVerification {
'com.android.support.constraint:constraint-layout-solver:2cafbe356f71c208013d021f32943904798cd6459e5107f9fe27000eb5bc2aef',
'com.google.guava:listenablefuture:e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069',
'org.signal:signal-metadata-android:d9d798aab7ee7200373ecff8718baf8aaeb632c123604e8a41b7b4c0c97eeee1',
'org.whispersystems:signal-service-java:9573395fe0b514cff10b8166f44de00a98682e0822a2b8204e9b9e696d53cb90',
'org.whispersystems:signal-service-java:fde1a008fe42ebbf1cd35018b363135cd8fec9e690304f8917b5ffb7080fa2a5',
'com.github.bumptech.glide:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b',
'com.github.bumptech.glide:annotations:bede99ef9f71517a4274bac18fd3e483e9f2b6108d7d6fe8f4949be4aa4d9512',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
@@ -306,8 +301,8 @@ android {
}
defaultConfig {
versionCode 454
versionName "4.33.5"
versionCode 463
versionName "4.34.8"
minSdkVersion 14
targetSdkVersion 26
@@ -352,6 +347,7 @@ android {
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-firebase-messaging.pro',
'proguard-google-play-services.pro',
'proguard-dagger.pro',
'proguard-jackson.pro',
@@ -506,4 +502,3 @@ def getLastCommitTimestamp() {
return os.toString() + "000"
}
}
apply plugin: 'com.google.gms.google-services'

View File

@@ -1,42 +0,0 @@
{
"project_info": {
"project_number": "312334754206",
"firebase_url": "https://api-project-312334754206.firebaseio.com",
"project_id": "api-project-312334754206",
"storage_bucket": "api-project-312334754206.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:312334754206:android:a9297b152879f266",
"android_client_info": {
"package_name": "org.thoughtcrime.securesms"
}
},
"oauth_client": [
{
"client_id": "312334754206-dg1p1mtekis8ivja3ica50vonmrlunh4.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDrfzNAPBPzX6key51hqo3p5LZXF5Y-yxU"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}

1
gradle.properties Normal file
View File

@@ -0,0 +1 @@
org.gradle.jvmargs=-Xmx2048m

View File

@@ -0,0 +1 @@
-dontwarn com.google.firebase.analytics.connector.AnalyticsConnector

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

View File

@@ -0,0 +1,41 @@
<?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">
<TextView
android:id="@+id/registration_captcha_title"
style="@style/Signal.Text.Headline"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:text="@string/RegistrationActivity_we_need_to_verify_that_youre_human"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<WebView
android:id="@+id/registration_captcha_web_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/registration_captcha_title" />
</android.support.constraint.ConstraintLayout>

View File

@@ -54,7 +54,15 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<include layout="@layout/conversation_input_panel"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/conversation_input_panel"/>
<include layout="@layout/conversation_search_nav" />
</FrameLayout>
<Button android:id="@+id/register_button"
android:layout_width="fill_parent"

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationItem
<org.thoughtcrime.securesms.conversation.ConversationItem
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"
@@ -193,4 +193,4 @@
android:gravity="center_vertical"/>
</RelativeLayout>
</org.thoughtcrime.securesms.ConversationItem>
</org.thoughtcrime.securesms.conversation.ConversationItem>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationItem
<org.thoughtcrime.securesms.conversation.ConversationItem
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"
@@ -152,4 +152,4 @@
</RelativeLayout>
</org.thoughtcrime.securesms.ConversationItem>
</org.thoughtcrime.securesms.conversation.ConversationItem>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationUpdateItem
<org.thoughtcrime.securesms.conversation.ConversationUpdateItem
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/conversation_update_item"
@@ -77,4 +77,4 @@
</LinearLayout>
</org.thoughtcrime.securesms.ConversationUpdateItem>
</org.thoughtcrime.securesms.conversation.ConversationUpdateItem>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.ConversationSearchBottomBar
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:id="@+id/conversation_search_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?conversation_background"
android:visibility="gone"
tools:visibility="visible"
tools:parentTag="android.support.constraint.ConstraintLayout">
<TextView
android:id="@+id/conversation_search_position"
style="@style/Signal.Text.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/conversation_search_up"
app:layout_constraintTop_toTopOf="parent"
tools:text="37 of 73" />
<ImageView
android:id="@+id/conversation_search_up"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:background="?selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_keyboard_arrow_up_white_36dp"
android:tint="@color/signal_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/conversation_search_down"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/conversation_search_down"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:background="?selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_keyboard_arrow_down_white_24dp"
android:tint="@color/signal_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/conversation_search_progress_wheel"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_marginTop="0dp"
android:layout_marginBottom="0dp"
android:indeterminate="true"
android:padding="8dp"
android:background="?conversation_background"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:matProg_barColor="@color/core_grey_25"
app:matProg_progressIndeterminate="true" />
</org.thoughtcrime.securesms.components.ConversationSearchBottomBar>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationTitleView
<org.thoughtcrime.securesms.conversation.ConversationTitleView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@@ -20,6 +20,7 @@
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:background="?selectableItemBackgroundBorderless"
android:visibility="visible"/>
<org.thoughtcrime.securesms.components.AvatarImageView
@@ -39,44 +40,47 @@
tools:src="@drawable/ic_contact_picture"
android:contentDescription="@string/conversation_list_item_view__contact_photo_image"/>
<RelativeLayout android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/contact_photo_image"
android:layout_toEndOf="@id/contact_photo_image"
android:layout_centerVertical="true">
<LinearLayout
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_toRightOf="@id/contact_photo_image"
android:layout_toEndOf="@id/contact_photo_image"
android:layout_centerVertical="true">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/title"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:textSize="18dp"
android:transitionName="recipient_name"
android:drawablePadding="5dp"
android:gravity="center_vertical"
android:layout_gravity="center_vertical"
style="@style/TextSecure.TitleTextStyle"
tools:ignore="UnusedAttribute"/>
<LinearLayout
android:id="@+id/subtitle_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/verified_indicator"
android:src="@drawable/ic_check_circle_white_18dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:textSize="18dp"
android:transitionName="recipient_name"
android:drawablePadding="5dp"
android:gravity="center_vertical"
android:layout_gravity="center_vertical"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
style="@style/TextSecure.TitleTextStyle"
tools:ignore="UnusedAttribute"/>
android:layout_marginRight="3dp"
android:layout_marginEnd="3dp"
android:layout_gravity="bottom"
android:alpha="0.7"
android:visibility="gone"/>
<ImageView android:id="@+id/verified_indicator"
android:src="@drawable/ic_check_circle_white_18dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="3dp"
android:layout_marginEnd="3dp"
android:layout_gravity="bottom"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@id/title"
android:alpha="0.7"
android:visibility="gone"/>
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -84,14 +88,13 @@
android:ellipsize="end"
android:layout_gravity="center_vertical|start"
android:gravity="center_vertical"
android:layout_toRightOf="@id/verified_indicator"
android:layout_toEndOf="@id/verified_indicator"
android:layout_below="@id/title"
android:textDirection="ltr"
android:textSize="13dp"
tools:text="(123) 123-1234"
style="@style/TextSecure.SubtitleTextStyle"/>
</RelativeLayout>
</LinearLayout>
</org.thoughtcrime.securesms.ConversationTitleView>
</LinearLayout>
</org.thoughtcrime.securesms.conversation.ConversationTitleView>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/conversation__menu_view_all_media"
android:id="@+id/menu_view_media" />
@@ -7,6 +8,12 @@
<item android:title="@string/conversation__menu_conversation_settings"
android:id="@+id/menu_conversation_settings"/>
<item android:title="@string/SearchToolbar_search"
android:id="@+id/menu_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="collapseActionView"/>
<item android:title="@string/conversation__menu_add_shortcut"
android:id="@+id/menu_add_shortcut"/>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="google_app_id" translatable="false">1:312334754206:android:a9297b152879f266</string>
<string name="gcm_defaultSenderId" translatable="false">312334754206</string>
<string name="default_web_client_id" translatable="false">312334754206-dg1p1mtekis8ivja3ica50vonmrlunh4.apps.googleusercontent.com</string>
<string name="firebase_database_url" translatable="false">https://api-project-312334754206.firebaseio.com</string>
<string name="google_api_key" translatable="false">AIzaSyDrfzNAPBPzX6key51hqo3p5LZXF5Y-yxU</string>
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyDrfzNAPBPzX6key51hqo3p5LZXF5Y-yxU</string>
<string name="project_id" translatable="false">api-project-312334754206</string>
</resources>

View File

@@ -6,6 +6,7 @@
<string name="delete">Delete</string>
<string name="please_wait">Please wait...</string>
<string name="save">Save</string>
<string name="note_to_self">Note to Self</string>
<!-- AbstractNotificationBuilder -->
<string name="AbstractNotificationBuilder_new_message">New message</string>
@@ -180,6 +181,8 @@
<string name="ConversationActivity_signal_cannot_sent_sms_mms_messages_because_it_is_not_your_default_sms_app">Signal cannot send SMS/MMS messages because it is not your default SMS app. Would you like to change this in your Android settings?</string>
<string name="ConversationActivity_yes">Yes</string>
<string name="ConversationActivity_no">No</string>
<string name="ConversationActivity_search_position">%1$d of %2$d</string>
<string name="ConversationActivity_no_results">No results</string>
<!-- ConversationAdapter -->
<plurals name="ConversationAdapter_n_unread_messages">
@@ -452,6 +455,7 @@
<!-- MediaSendActivity -->
<string name="MediaSendActivity_add_a_caption">Add a caption...</string>
<string name="MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit">An item was removed because it exceeded the size limit</string>
<!-- MediaRepository -->
<string name="MediaRepository_all_media">All media</string>
@@ -593,6 +597,8 @@
<item quantity="one">You are now %d step away from submitting a debug log.</item>
<item quantity="other">You are now %d steps away from submitting a debug log.</item>
</plurals>
<string name="RegistrationActivity_we_need_to_verify_that_youre_human">We need to verify that you\'re human.</string>
<string name="RegistrationActivity_failed_to_verify_the_captcha">Failed to verify the CAPTCHA</string>
<!-- ScribbleActivity -->
<string name="ScribbleActivity_save_failure">Failed to save image changes</string>

View File

@@ -252,6 +252,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
add("TA-1053");
add("Mi A1");
add("E5823"); // Sony z5 compact
add("Redmi Note 5");
}};
Set<String> OPEN_SL_ES_WHITELIST = new HashSet<String>() {{

View File

@@ -24,6 +24,7 @@ public interface BindableConversationItem extends Unbindable {
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient recipients,
@Nullable String searchQuery,
boolean pulseHighlight);
MessageRecord getMessageRecord();

View File

@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;

View File

@@ -4,6 +4,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;

View File

@@ -18,27 +18,18 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.RippleDrawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.components.AlertView;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
@@ -51,17 +42,15 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import static org.thoughtcrime.securesms.util.SpanUtil.color;
public class ConversationListItem extends RelativeLayout
implements RecipientModifiedListener,
BindableConversationListItem, Unbindable
@@ -150,7 +139,9 @@ public class ConversationListItem extends RelativeLayout
this.recipient.addListener(this);
if (highlightSubstring != null) {
this.fromView.setText(getHighlightedSpan(locale, recipient.getName(), highlightSubstring));
String name = recipient.isLocalNumber() ? getContext().getString(R.string.note_to_self) : recipient.getName();
this.fromView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), name, highlightSubstring));
} else {
this.fromView.setText(recipient, unreadCount == 0);
}
@@ -204,8 +195,10 @@ public class ConversationListItem extends RelativeLayout
this.recipient.addListener(this);
fromView.setText(getHighlightedSpan(locale, recipient.getName(), highlightSubstring));
subjectView.setText(getHighlightedSpan(locale, contact.getAddress().toPhoneString(), highlightSubstring));
String name = recipient.isLocalNumber() ? getContext().getString(R.string.note_to_self) : recipient.getName();
fromView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), name, highlightSubstring));
subjectView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), contact.getAddress().toPhoneString(), highlightSubstring));
dateView.setText("");
archivedView.setVisibility(GONE);
unreadIndicator.setVisibility(GONE);
@@ -224,13 +217,13 @@ public class ConversationListItem extends RelativeLayout
@Nullable String highlightSubstring)
{
this.selectedThreads = Collections.emptySet();
this.recipient = messageResult.recipient;
this.recipient = messageResult.conversationRecipient;
this.glideRequests = glideRequests;
this.recipient.addListener(this);
fromView.setText(recipient, true);
subjectView.setText(getHighlightedSpan(locale, messageResult.bodySnippet, highlightSubstring));
subjectView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), messageResult.bodySnippet, highlightSubstring));
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.receivedTimestampMs));
archivedView.setVisibility(GONE);
unreadIndicator.setVisibility(GONE);
@@ -333,44 +326,6 @@ public class ConversationListItem extends RelativeLayout
unreadIndicator.setVisibility(View.VISIBLE);
}
private Spanned getHighlightedSpan(@NonNull Locale locale,
@Nullable String value,
@Nullable String highlight)
{
if (TextUtils.isEmpty(value)) {
return new SpannableString("");
}
value = value.replaceAll("\n", " ");
if (TextUtils.isEmpty(highlight)) {
return new SpannableString(value);
}
String normalizedValue = value.toLowerCase(locale);
String normalizedTest = highlight.toLowerCase(locale);
List<String> testTokens = Stream.of(normalizedTest.split(" ")).filter(s -> s.trim().length() > 0).toList();
Spannable spanned = new SpannableString(value);
int searchStartIndex = 0;
for (String token : testTokens) {
if (searchStartIndex >= spanned.length()) {
break;
}
int start = normalizedValue.indexOf(token, searchStartIndex);
if (start >= 0) {
int end = Math.min(start + token.length(), spanned.length());
spanned.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
searchStartIndex = end;
}
}
return spanned;
}
@Override
public void onModified(final Recipient recipient) {
Util.runOnMain(() -> {

View File

@@ -96,6 +96,7 @@ public class DatabaseUpgradeActivity extends BaseActivity {
public static final int COLOR_MIGRATION = 412;
public static final int UNIDENTIFIED_DELIVERY = 422;
public static final int SIGNALING_KEY_DEPRECATION = 447;
public static final int CONVERSATION_SEARCH = 455;
private static final SortedSet<Integer> UPGRADE_VERSIONS = new TreeSet<Integer>() {{
add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION);
@@ -123,6 +124,7 @@ public class DatabaseUpgradeActivity extends BaseActivity {
add(COLOR_MIGRATION);
add(UNIDENTIFIED_DELIVERY);
add(SIGNALING_KEY_DEPRECATION);
add(CONVERSATION_SEARCH);
}};
private MasterSecret masterSecret;

View File

@@ -26,6 +26,8 @@ import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.logging.Log;
import android.view.Menu;
import android.view.MenuInflater;

View File

@@ -29,6 +29,8 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import org.thoughtcrime.securesms.conversation.ConversationItem;
import org.thoughtcrime.securesms.logging.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
@@ -269,7 +271,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
toFromRes = R.string.message_details_header__from;
}
toFrom.setText(toFromRes);
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, false);
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, null, false);
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
}

View File

@@ -23,6 +23,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;

View File

@@ -29,6 +29,10 @@ import android.support.v7.widget.Toolbar;
import android.telephony.PhoneNumberUtils;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
import org.thoughtcrime.securesms.logging.Log;
@@ -202,14 +206,19 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
}
private void setHeader(@NonNull Recipient recipient) {
glideRequests.load(recipient.getContactPhoto())
.fallback(recipient.getFallbackContactPhoto().asCallCard(this))
.error(recipient.getFallbackContactPhoto().asCallCard(this))
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient.getAddress(), String.valueOf(TextSecurePreferences.getProfileAvatarId(this)))
: recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_default, R.drawable.ic_person_large)
: recipient.getFallbackContactPhoto();
glideRequests.load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(this))
.error(fallbackPhoto.asCallCard(this))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(this.avatar);
if (recipient.getContactPhoto() == null) this.avatar.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
else this.avatar.setScaleType(ImageView.ScaleType.CENTER_CROP);
if (contactPhoto == null) this.avatar.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
else this.avatar.setScaleType(ImageView.ScaleType.CENTER_CROP);
this.avatar.setBackgroundColor(recipient.getColor().toActionBarColor(this));
this.toolbarLayout.setTitle(recipient.toShortString());
@@ -356,6 +365,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private void setSummaries(Recipient recipient) {
CheckBoxPreference mutePreference = (CheckBoxPreference) this.findPreference(PREFERENCE_MUTED);
Preference customPreference = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
Preference ringtoneMessagePreference = this.findPreference(PREFERENCE_MESSAGE_TONE);
Preference ringtoneCallPreference = this.findPreference(PREFERENCE_CALL_TONE);
ListPreference vibrateMessagePreference = (ListPreference) this.findPreference(PREFERENCE_MESSAGE_VIBRATE);
@@ -384,7 +394,19 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
vibrateCallPreference.setSummary(vibrateCallSummary.first);
vibrateCallPreference.setValueIndex(vibrateCallSummary.second);
if (recipient.isGroupRecipient()) {
if (recipient.isLocalNumber()) {
mutePreference.setVisible(false);
customPreference.setVisible(false);
ringtoneMessagePreference.setVisible(false);
vibrateMessagePreference.setVisible(false);
if (identityPreference != null) identityPreference.setVisible(false);
if (aboutCategory != null) aboutCategory.setVisible(false);
if (aboutDivider != null) aboutDivider.setVisible(false);
if (privacyCategory != null) privacyCategory.setVisible(false);
if (divider != null) divider.setVisible(false);
if (callCategory != null) callCategory.setVisible(false);
} if (recipient.isGroupRecipient()) {
if (colorPreference != null) colorPreference.setVisible(false);
if (identityPreference != null) identityPreference.setVisible(false);
if (callCategory != null) callCategory.setVisible(false);

View File

@@ -78,6 +78,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.registration.CaptchaActivity;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.VerificationCodeParser;
@@ -96,6 +97,7 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.push.LockedException;
@@ -117,6 +119,7 @@ import java.util.concurrent.TimeUnit;
public class RegistrationActivity extends BaseActionBarActivity implements VerificationCodeView.OnCodeEnteredListener {
private static final int PICK_COUNTRY = 1;
private static final int CAPTCHA = 24601;
private static final int SCENE_TRANSITION_DURATION = 250;
private static final int DEBUG_TAP_TARGET = 8;
private static final int DEBUG_TAP_ANNOUNCE = 4;
@@ -185,6 +188,18 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
this.countryCode.setText(String.valueOf(data.getIntExtra("country_code", 1)));
setCountryDisplay(data.getStringExtra("country_name"));
setCountryFormatter(data.getIntExtra("country_code", 1));
} else if (requestCode == CAPTCHA && resultCode == RESULT_OK && data != null) {
registrationState = new RegistrationState(Optional.fromNullable(data.getStringExtra(CaptchaActivity.KEY_TOKEN)), registrationState);
if (data.getBooleanExtra(CaptchaActivity.KEY_IS_SMS, true)) {
handleRegister();
} else {
handlePhoneCallRequest();
}
} else if (requestCode == CAPTCHA) {
Toast.makeText(this, R.string.RegistrationActivity_failed_to_verify_the_captcha, Toast.LENGTH_LONG).show();
createButton.setIndeterminateProgressMode(false);
createButton.setProgress(0);
}
}
@@ -226,7 +241,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
this.pinForgotButton = findViewById(R.id.forgot_button);
this.pinClarificationContainer = findViewById(R.id.pin_clarification_container);
this.registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, null);
this.registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, Optional.absent(), Optional.absent());
this.countryCode.addTextChangedListener(new CountryCodeChangedListener());
this.number.addTextChangedListener(new NumberChangedListener());
@@ -388,13 +403,14 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
restoreButton.setIndeterminateProgressMode(true);
restoreButton.setProgress(50);
final String passphrase = prompt.getText().toString();
new AsyncTask<Void, Void, BackupImportResult>() {
@Override
protected BackupImportResult doInBackground(Void... voids) {
try {
Context context = RegistrationActivity.this;
String passphrase = prompt.getText().toString();
SQLiteDatabase database = DatabaseFactory.getBackupDatabase(context);
Context context = RegistrationActivity.this;
SQLiteDatabase database = DatabaseFactory.getBackupDatabase(context);
FullBackupImporter.importFile(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
@@ -498,9 +514,9 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
@SuppressLint("StaticFieldLeak")
private void requestVerificationCode(@NonNull String e164number, boolean gcmSupported, boolean smsRetrieverSupported) {
new AsyncTask<Void, Void, Pair<String, Optional<String>>> () {
new AsyncTask<Void, Void, VerificationRequestResult> () {
@Override
protected @Nullable Pair<String, Optional<String>> doInBackground(Void... voids) {
protected @NonNull VerificationRequestResult doInBackground(Void... voids) {
try {
markAsVerifying(true);
@@ -515,29 +531,34 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
}
accountManager = AccountManagerFactory.createManager(RegistrationActivity.this, e164number, password);
accountManager.requestSmsVerificationCode(smsRetrieverSupported);
accountManager.requestSmsVerificationCode(smsRetrieverSupported, registrationState.captchaToken);
return new Pair<>(password, fcmToken);
return new VerificationRequestResult(password, fcmToken, Optional.absent());
} catch (IOException e) {
Log.w(TAG, "Error during account registration", e);
return null;
return new VerificationRequestResult(null, Optional.absent(), Optional.of(e));
}
}
protected void onPostExecute(@Nullable Pair<String, Optional<String>> result) {
if (result == null) {
protected void onPostExecute(@NonNull VerificationRequestResult result) {
if (result.exception.isPresent() && result.exception.get() instanceof CaptchaRequiredException) {
requestCaptcha(true);
} else if (result.exception.isPresent()) {
Toast.makeText(RegistrationActivity.this, R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
createButton.setIndeterminateProgressMode(false);
createButton.setProgress(0);
return;
} else {
registrationState = new RegistrationState(RegistrationState.State.VERIFYING, e164number, result.password, result.fcmToken, Optional.absent());
displayVerificationView(e164number, 64);
}
registrationState = new RegistrationState(RegistrationState.State.VERIFYING, e164number, result.first, result.second);
displayVerificationView(e164number, 64);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void requestCaptcha(boolean isSms) {
startActivityForResult(CaptchaActivity.getIntent(this, isSms), CAPTCHA);
}
private void handleVerificationCodeReceived(@Nullable String code) {
List<Integer> parsedCode = convertVerificationCodeToDigits(code);
@@ -693,7 +714,9 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
@Override
protected Void doInBackground(Void... voids) {
try {
accountManager.requestVoiceVerificationCode(Locale.getDefault());
accountManager.requestVoiceVerificationCode(Locale.getDefault(), registrationState.captchaToken);
} catch (CaptchaRequiredException e) {
requestCaptcha(false);
} catch (IOException e) {
Log.w(TAG, e);
}
@@ -892,7 +915,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
@Override
public void onClick(View widget) {
displayInitialView(false);
registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, null);
registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, Optional.absent(), Optional.absent());
}
@Override
@@ -1157,28 +1180,51 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
}
}
private static class VerificationRequestResult {
private final String password;
private final Optional<String> fcmToken;
private final Optional<IOException> exception;
private VerificationRequestResult(String password, Optional<String> fcmToken, Optional<IOException> exception) {
this.password = password;
this.fcmToken = fcmToken;
this.exception = exception;
}
}
private static class RegistrationState {
private enum State {
INITIAL, VERIFYING, CHECKING, PIN
}
private final State state;
private final String e164number;
private final String password;
private final State state;
private final String e164number;
private final String password;
private final Optional<String> gcmToken;
private final Optional<String> captchaToken;
RegistrationState(State state, String e164number, String password, Optional<String> gcmToken) {
this.state = state;
this.e164number = e164number;
this.password = password;
this.gcmToken = gcmToken;
RegistrationState(State state, String e164number, String password, Optional<String> gcmToken, Optional<String> captchaToken) {
this.state = state;
this.e164number = e164number;
this.password = password;
this.gcmToken = gcmToken;
this.captchaToken = captchaToken;
}
RegistrationState(State state, RegistrationState previous) {
this.state = state;
this.e164number = previous.e164number;
this.password = previous.password;
this.gcmToken = previous.gcmToken;
this.state = state;
this.e164number = previous.e164number;
this.password = previous.password;
this.gcmToken = previous.gcmToken;
this.captchaToken = previous.captchaToken;
}
RegistrationState(Optional<String> captchaToken, RegistrationState previous) {
this.state = previous.state;
this.e164number = previous.e164number;
this.password = previous.password;
this.gcmToken = previous.gcmToken;
this.captchaToken = captchaToken;
}
}

View File

@@ -32,8 +32,10 @@ import android.support.annotation.Nullable;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.logging.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
@@ -131,9 +133,10 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
super.onPause();
if (!isPassingAlongMedia && resolvedExtra != null) {
PersistentBlobProvider.getInstance(this).delete(this, resolvedExtra);
}
if (!isFinishing()) {
finish();
if (!isFinishing()) {
finish();
}
}
}

View File

@@ -7,6 +7,8 @@ import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.logging.Log;
import android.widget.Toast;

View File

@@ -34,7 +34,7 @@ public class BackupDialog {
button.setOnClickListener(v -> {
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
if (confirmationCheckBox.isChecked()) {
TextSecurePreferences.setBackupPassphrase(context, Util.join(password, " "));
BackupPassphrase.set(context, Util.join(password, " "));
TextSecurePreferences.setBackupEnabled(context, true);
LocalBackupListener.schedule(context);
@@ -75,7 +75,7 @@ public class BackupDialog {
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
TextSecurePreferences.setBackupPassphrase(context, null);
BackupPassphrase.set(context, null);
TextSecurePreferences.setBackupEnabled(context, false);
BackupUtil.deleteAllBackups();
preference.setChecked(false);

View File

@@ -0,0 +1,47 @@
package org.thoughtcrime.securesms.backup;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.crypto.KeyStoreHelper;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
* Allows the getting and setting of the backup passphrase, which is stored encrypted on API >= 23.
*/
public class BackupPassphrase {
private static final String TAG = BackupPassphrase.class.getSimpleName();
public static String get(@NonNull Context context) {
String passphrase = TextSecurePreferences.getBackupPassphrase(context);
String encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
if (Build.VERSION.SDK_INT < 23 || (passphrase == null && encryptedPassphrase == null)) {
return passphrase;
}
if (encryptedPassphrase == null) {
Log.i(TAG, "Migrating to encrypted passphrase.");
set(context, passphrase);
encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
}
KeyStoreHelper.SealedData data = KeyStoreHelper.SealedData.fromString(encryptedPassphrase);
return new String(KeyStoreHelper.unseal(data));
}
public static void set(@NonNull Context context, @Nullable String passphrase) {
if (passphrase == null || Build.VERSION.SDK_INT < 23) {
TextSecurePreferences.setBackupPassphrase(context, passphrase);
TextSecurePreferences.setEncryptedBackupPassphrase(context, null);
} else {
KeyStoreHelper.SealedData encryptedPassphrase = KeyStoreHelper.seal(passphrase.getBytes());
TextSecurePreferences.setEncryptedBackupPassphrase(context, encryptedPassphrase.serialize());
TextSecurePreferences.setBackupPassphrase(context, null);
}
}
}

View File

@@ -11,6 +11,7 @@ import android.support.v13.view.inputmethod.EditorInfoCompat;
import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.support.v4.os.BuildCompat;
import android.text.Editable;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableString;
@@ -33,7 +34,8 @@ public class ComposeText extends EmojiEditText {
private CharSequence hint;
private SpannableString subHint;
@Nullable private InputPanel.MediaListener mediaListener;
@Nullable private InputPanel.MediaListener mediaListener;
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
public ComposeText(Context context) {
super(context);
@@ -69,6 +71,15 @@ public class ComposeText extends EmojiEditText {
}
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (cursorPositionChangedListener != null) {
cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd);
}
}
private CharSequence ellipsizeToWidth(CharSequence text) {
return TextUtils.ellipsize(text,
getPaint(),
@@ -104,6 +115,10 @@ public class ComposeText extends EmojiEditText {
setSelection(getText().length());
}
public void setCursorPositionChangedListener(@Nullable CursorPositionChangedListener listener) {
this.cursorPositionChangedListener = listener;
}
private boolean isLandscape() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
@@ -189,4 +204,7 @@ public class ComposeText extends EmojiEditText {
}
}
public interface CursorPositionChangedListener {
void onCursorPositionChanged(int start, int end);
}
}

View File

@@ -0,0 +1,88 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.constraint.ConstraintLayout;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
/**
* Bottom navigation bar shown in the {@link org.thoughtcrime.securesms.conversation.ConversationActivity}
* when the user is searching within a conversation. Shows details about the results and allows the
* user to move between them.
*/
public class ConversationSearchBottomBar extends ConstraintLayout {
private View searchDown;
private View searchUp;
private TextView searchPositionText;
private View progressWheel;
private EventListener eventListener;
public ConversationSearchBottomBar(Context context) {
super(context);
}
public ConversationSearchBottomBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
this.searchUp = findViewById(R.id.conversation_search_up);
this.searchDown = findViewById(R.id.conversation_search_down);
this.searchPositionText = findViewById(R.id.conversation_search_position);
this.progressWheel = findViewById(R.id.conversation_search_progress_wheel);
}
public void setData(int position, int count) {
progressWheel.setVisibility(GONE);
searchUp.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onSearchMoveUpPressed();
}
});
searchDown.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onSearchMoveDownPressed();
}
});
if (count > 0) {
searchPositionText.setText(getResources().getString(R.string.ConversationActivity_search_position, position + 1, count));
} else {
searchPositionText.setText(R.string.ConversationActivity_no_results);
}
setViewEnabled(searchUp, position < (count - 1));
setViewEnabled(searchDown, position > 0);
}
public void showLoading() {
progressWheel.setVisibility(VISIBLE);
}
private void setViewEnabled(@NonNull View view, boolean enabled) {
view.setEnabled(enabled);
view.setAlpha(enabled ? 1f : 0.25f);
}
public void setEventListener(@Nullable EventListener eventListener) {
this.eventListener = eventListener;
}
public interface EventListener {
void onSearchMoveUpPressed();
void onSearchMoveDownPressed();
}
}

View File

@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ResUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.spans.CenterAlignedRelativeSizeSpan;
public class FromTextView extends EmojiTextView {
@@ -52,7 +53,10 @@ public class FromTextView extends EmojiTextView {
fromSpan.setSpan(new StyleSpan(typeface), 0, builder.length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) {
if (recipient.isLocalNumber()) {
builder.append(getContext().getString(R.string.note_to_self));
} else if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) {
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") ");
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
profileName.setSpan(new TypefaceSpan("sans-serif-light"), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

View File

@@ -29,9 +29,11 @@ import android.provider.ContactsContract.PhoneLookup;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.ArrayList;
import java.util.Collection;
@@ -204,6 +206,12 @@ public class ContactAccessor {
reader.close();
}
if (context.getString(R.string.note_to_self).toLowerCase().contains(constraint.toLowerCase()) &&
!numberList.contains(TextSecurePreferences.getLocalNumber(context)))
{
numberList.add(TextSecurePreferences.getLocalNumber(context));
}
return numberList;
}

View File

@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import java.util.HashMap;

View File

@@ -75,6 +75,10 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientM
this.numberView.setTextColor(color);
this.contactPhotoImage.setAvatar(glideRequests, recipient, false);
if (!multiSelect && recipient != null && recipient.isLocalNumber()) {
name = getContext().getString(R.string.note_to_self);
}
setText(type, name, number, label);
if (multiSelect) this.checkBox.setVisibility(View.VISIBLE);

View File

@@ -19,10 +19,13 @@ package org.thoughtcrime.securesms.contacts;
import android.accounts.Account;
import android.annotation.SuppressLint;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
@@ -37,6 +40,7 @@ import android.util.Pair;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -101,22 +105,27 @@ public class ContactsDatabase {
boolean remove)
throws RemoteException, OperationApplicationException
{
Set<Address> registeredAddressSet = new HashSet<>();
Set<Address> registeredAddressSet = new HashSet<>(registeredAddressList);
ArrayList<ContentProviderOperation> operations = new ArrayList<>();
Map<Address, SignalContact> currentContacts = getSignalRawContacts(account);
List<List<Address>> registeredChunks = Util.chunk(registeredAddressList, 50);
for (Address registeredAddress : registeredAddressList) {
registeredAddressSet.add(registeredAddress);
for (List<Address> registeredChunk : registeredChunks) {
for (Address registeredAddress : registeredChunk) {
if (!currentContacts.containsKey(registeredAddress)) {
Optional<SystemContactInfo> systemContactInfo = getSystemContactInfo(registeredAddress);
if (!currentContacts.containsKey(registeredAddress)) {
Optional<SystemContactInfo> systemContactInfo = getSystemContactInfo(registeredAddress);
if (systemContactInfo.isPresent()) {
Log.i(TAG, "Adding number: " + registeredAddress);
addTextSecureRawContact(operations, account, systemContactInfo.get().number,
systemContactInfo.get().name, systemContactInfo.get().id);
if (systemContactInfo.isPresent()) {
Log.i(TAG, "Adding number: " + registeredAddress);
addTextSecureRawContact(operations, account, systemContactInfo.get().number,
systemContactInfo.get().name, systemContactInfo.get().id);
}
}
}
if (!operations.isEmpty()) {
context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
operations.clear();
}
}
for (Map.Entry<Address, SignalContact> currentContactEntry : currentContacts.entrySet()) {
@@ -137,7 +146,7 @@ public class ContactsDatabase {
}
if (!operations.isEmpty()) {
context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
applyOperationsInBatches(context.getContentResolver(), ContactsContract.AUTHORITY, operations, 50);
}
}
@@ -216,6 +225,25 @@ public class ContactsDatabase {
new String[] {CONTACT_MIMETYPE,
"%" + filter + "%", "%" + filter + "%"},
sort);
if (context.getString(R.string.note_to_self).toLowerCase().contains(filter.toLowerCase())) {
Optional<SystemContactInfo> self = getSystemContactInfo(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
boolean shouldAdd = true;
if (self.isPresent()) {
boolean nameMatch = self.get().name != null && self.get().name.toLowerCase().contains(filter.toLowerCase());
boolean numberMatch = self.get().number != null && self.get().number.contains(filter);
shouldAdd = !nameMatch && !numberMatch;
}
if (shouldAdd) {
MatrixCursor selfCursor = new MatrixCursor(projection);
selfCursor.addRow(new Object[]{ context.getString(R.string.note_to_self), TextSecurePreferences.getLocalNumber(context)});
cursor = cursor == null ? selfCursor : new MergeCursor(new Cursor[]{ cursor, selfCursor });
}
}
}
return new ProjectionMappingCursor(cursor, projectionMap,
@@ -532,6 +560,18 @@ public class ContactsDatabase {
}
}
private void applyOperationsInBatches(@NonNull ContentResolver contentResolver,
@NonNull String authority,
@NonNull List<ContentProviderOperation> operations,
int batchSize)
throws OperationApplicationException, RemoteException
{
List<List<ContentProviderOperation>> batches = Util.chunk(operations, batchSize);
for (List<ContentProviderOperation> batch : batches) {
contentResolver.applyBatch(authority, new ArrayList<>(batch));
}
}
private static class ProjectionMappingCursor extends CursorWrapper {
private final Map<String, String> projectionMap;

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversation;
import android.Manifest;
import android.annotation.SuppressLint;
@@ -49,6 +49,7 @@ import android.support.v4.view.MenuItemCompat;
import android.support.v4.view.WindowCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.SearchView;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@@ -74,6 +75,22 @@ import com.google.android.gms.location.places.ui.PlacePicker;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.ConversationListArchiveActivity;
import org.thoughtcrime.securesms.ExpirationDialog;
import org.thoughtcrime.securesms.GroupCreateActivity;
import org.thoughtcrime.securesms.GroupMembersDialog;
import org.thoughtcrime.securesms.MediaOverviewActivity;
import org.thoughtcrime.securesms.MuteDialog;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.PromptMmsActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RecipientPreferenceActivity;
import org.thoughtcrime.securesms.RegistrationActivity;
import org.thoughtcrime.securesms.ShortcutLauncherActivity;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.audio.AudioRecorder;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.camera.CameraActivity;
@@ -81,6 +98,7 @@ import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.AttachmentTypeSelector;
import org.thoughtcrime.securesms.components.ComposeText;
import org.thoughtcrime.securesms.components.ConversationSearchBottomBar;
import org.thoughtcrime.securesms.components.HidingLinearLayout;
import org.thoughtcrime.securesms.components.InputAwareLayout;
import org.thoughtcrime.securesms.components.InputPanel;
@@ -163,6 +181,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
@@ -219,7 +238,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
OnKeyboardShownListener,
AttachmentDrawerListener,
InputPanel.Listener,
InputPanel.MediaListener
InputPanel.MediaListener,
ComposeText.CursorPositionChangedListener,
ConversationSearchBottomBar.EventListener
{
private static final String TAG = ConversationActivity.class.getSimpleName();
@@ -247,23 +268,25 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private static final int PICK_CAMERA = 12;
private static final int MEDIA_SENDER = 13;
private GlideRequests glideRequests;
protected ComposeText composeText;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private ImageButton attachButton;
protected ConversationTitleView titleView;
private TextView charactersLeft;
private ConversationFragment fragment;
private Button unblockButton;
private Button makeDefaultSmsButton;
private Button registerButton;
private InputAwareLayout container;
private View composePanel;
protected Stub<ReminderView> reminderView;
private Stub<UnverifiedBannerView> unverifiedBannerView;
private GlideRequests glideRequests;
protected ComposeText composeText;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private ImageButton attachButton;
protected ConversationTitleView titleView;
private TextView charactersLeft;
private ConversationFragment fragment;
private Button unblockButton;
private Button makeDefaultSmsButton;
private Button registerButton;
private InputAwareLayout container;
private View composePanel;
protected Stub<ReminderView> reminderView;
private Stub<UnverifiedBannerView> unverifiedBannerView;
private Stub<GroupShareProfileView> groupShareProfileView;
private TypingStatusTextWatcher typingTextWatcher;
private ConversationSearchBottomBar searchNav;
private MenuItem searchViewItem;
private AttachmentTypeSelector attachmentTypeSelector;
private AttachmentManager attachmentManager;
@@ -274,7 +297,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
protected HidingLinearLayout inlineAttachmentToggle;
private QuickAttachmentDrawer quickAttachmentDrawer;
private InputPanel inputPanel;
private LinkPreviewViewModel linkPreviewViewModel;
private LinkPreviewViewModel linkPreviewViewModel;
private ConversationSearchViewModel searchViewModel;
private Recipient recipient;
private long threadId;
@@ -315,6 +340,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeViews();
initializeResources();
initializeLinkPreviewObserver();
initializeSearchObserver();
initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
@@ -336,6 +362,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (TextSecurePreferences.isTypingIndicatorsEnabled(ConversationActivity.this)) {
composeText.addTextChangedListener(typingTextWatcher);
}
composeText.setSelection(composeText.length(), composeText.length());
}
});
}
@@ -627,6 +654,67 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
inflater.inflate(R.menu.conversation_add_to_contacts, menu);
}
if (recipient != null && recipient.isLocalNumber()) {
if (isSecureText) menu.findItem(R.id.menu_call_secure).setVisible(false);
else menu.findItem(R.id.menu_call_insecure).setVisible(false);
MenuItem muteItem = menu.findItem(R.id.menu_mute_notifications);
if (muteItem != null) {
muteItem.setVisible(false);
}
}
searchViewItem = menu.findItem(R.id.menu_search);
SearchView searchView = (SearchView) searchViewItem.getActionView();
SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
searchViewModel.onQueryUpdated(query, threadId);
searchNav.showLoading();
fragment.onSearchQueryUpdated(query);
return true;
}
@Override
public boolean onQueryTextChange(String query) {
searchViewModel.onQueryUpdated(query, threadId);
searchNav.showLoading();
fragment.onSearchQueryUpdated(query);
return true;
}
};
searchViewItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
searchView.setOnQueryTextListener(queryListener);
searchViewModel.onSearchOpened();
searchNav.setVisibility(View.VISIBLE);
searchNav.setData(0, 0);
inputPanel.setVisibility(View.GONE);
for (int i = 0; i < menu.size(); i++) {
if (!menu.getItem(i).equals(searchViewItem)) {
menu.getItem(i).setVisible(false);
}
}
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
searchView.setOnQueryTextListener(null);
searchViewModel.onSearchClosed();
searchNav.setVisibility(View.GONE);
inputPanel.setVisibility(View.VISIBLE);
fragment.onSearchQueryUpdated(null);
invalidateOptionsMenu();
return true;
}
});
super.onPrepareOptionsMenu(menu);
return true;
}
@@ -639,6 +727,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case R.id.menu_call_insecure: handleDial(getRecipient(), false); return true;
case R.id.menu_view_media: handleViewMedia(); return true;
case R.id.menu_add_shortcut: handleAddShortcut(); return true;
case R.id.menu_search: handleSearch(); return true;
case R.id.menu_add_to_contacts: handleAddToContacts(); return true;
case R.id.menu_reset_secure_session: handleResetSecureSession(); return true;
case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true;
@@ -904,6 +993,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute();
}
private void handleSearch() {
searchViewModel.onSearchOpened();
}
private void handleLeavePushGroup() {
if (getRecipient() == null) {
Toast.makeText(this, getString(R.string.ConversationActivity_invalid_recipient),
@@ -1422,6 +1515,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
searchNav = ViewUtil.findById(this, R.id.conversation_search_nav);
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
@@ -1439,6 +1533,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
composeText.setOnEditorActionListener(sendButtonListener);
composeText.setCursorPositionChangedListener(this);
attachButton.setOnClickListener(new AttachButtonListener());
attachButton.setOnLongClickListener(new AttachButtonLongClickListener());
sendButton.setOnClickListener(sendButtonListener);
@@ -1473,6 +1568,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
quickCameraToggle.setEnabled(false);
}
searchNav.setEventListener(this);
inlineAttachmentButton.setOnClickListener(v -> handleAddAttachment());
}
@@ -1528,6 +1625,30 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
});
}
private void initializeSearchObserver() {
searchViewModel = ViewModelProviders.of(this).get(ConversationSearchViewModel.class);
searchViewModel.getSearchResults().observe(this, result -> {
if (result == null) return;
if (!result.getResults().isEmpty()) {
MessageResult messageResult = result.getResults().get(result.getPosition());
fragment.jumpToMessage(messageResult.messageRecipient.getAddress(), messageResult.receivedTimestampMs, searchViewModel::onMissingResult);
}
searchNav.setData(result.getPosition(), result.getResults().size());
});
}
@Override
public void onSearchMoveUpPressed() {
searchViewModel.onMoveUp();
}
@Override
public void onSearchMoveDownPressed() {
searchViewModel.onMoveDown();
}
private void initializeProfiles() {
if (!isSecureText) {
@@ -1553,7 +1674,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
updateReminders(recipient.hasSeenInviteReminder());
updateDefaultSubscriptionId(recipient.getDefaultSubscriptionId());
initializeSecurity(isSecureText, isDefaultSms);
invalidateOptionsMenu();
if (searchViewItem == null || !searchViewItem.isActionViewExpanded()) {
invalidateOptionsMenu();
}
});
}
@@ -1613,7 +1737,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
openContactShareEditor(uri);
return new SettableFuture<>(false);
} else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, Optional.absent(), Optional.absent());
Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, Optional.absent(), Optional.absent());
startActivityForResult(MediaSendActivity.getIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
return new SettableFuture<>(false);
} else {
@@ -2070,7 +2194,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void updateLinkPreviewState() {
if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent()) {
linkPreviewViewModel.onEnabled();
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed());
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd());
} else {
linkPreviewViewModel.onUserCancel();
}
@@ -2241,6 +2365,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
@Override
public void onCursorPositionChanged(int start, int end) {
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), start, end);
}
private void silentlySetComposeText(String text) {
typingTextWatcher.setEnabled(false);
composeText.setText(text);
@@ -2258,7 +2387,8 @@ 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());
// TODO: Carry over size?
Media media = new Media(uri, mimeType, dateTaken, width, height, 0, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent());
startActivityForResult(MediaSendActivity.getIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
}
}
@@ -2343,11 +2473,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void afterTextChanged(Editable s) {
calculateCharactersRemaining();
String trimmed = composeText.getTextTrimmed();
linkPreviewViewModel.onTextChanged(ConversationActivity.this, trimmed);
if (trimmed.length() == 0 || beforeLength == 0) {
if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) {
composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50);
}
}
@@ -2429,6 +2555,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
@Override
public void onMessageActionToolbarOpened() {
searchViewItem.collapseActionView();
}
@Override
public void onAttachmentChanged() {
handleSecurityChange(isSecureText, isDefaultSms);

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.database.Cursor;
@@ -23,6 +23,9 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -31,7 +34,7 @@ import android.widget.TextView;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.FastCursorRecyclerViewAdapter;
@@ -102,6 +105,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
private final @NonNull MessageDigest digest;
private MessageRecord recordToPulseHighlight;
private String searchQuery;
protected static class ViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
@@ -202,6 +206,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
locale,
batchSelected,
recipient,
searchQuery,
messageRecord == recordToPulseHighlight);
if (messageRecord == recordToPulseHighlight) {
@@ -360,6 +365,11 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
}
}
public void onSearchQueryUpdated(@Nullable String query) {
this.searchQuery = query;
notifyDataSetChanged();
}
private boolean hasAudio(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null;
}

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversation;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -27,6 +27,7 @@ import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.app.Fragment;
@@ -41,9 +42,15 @@ import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.text.ClipboardManager;
import android.text.TextUtils;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShareActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.ConversationTypingView;
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.logging.Log;
@@ -62,8 +69,8 @@ import android.widget.ViewSwitcher;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
import org.thoughtcrime.securesms.contactshare.Contact;
@@ -89,6 +96,7 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -121,6 +129,7 @@ public class ConversationFragment extends Fragment
private long lastSeen;
private int startingPosition;
private int previousOffset;
private int activeOffset;
private boolean firstLoad;
private long loaderStartTime;
private ActionMode actionMode;
@@ -528,6 +537,7 @@ public class ConversationFragment extends Fragment
System.currentTimeMillis(),
attachment.getWidth(),
attachment.getHeight(),
attachment.getSize(),
Optional.absent(),
Optional.fromNullable(attachment.getCaption())));
}
@@ -626,9 +636,14 @@ public class ConversationFragment extends Fragment
if (loader.hasOffset()) {
adapter.setHeaderView(bottomLoadMoreView);
}
if (firstLoad || loader.hasOffset()) {
previousOffset = loader.getOffset();
}
activeOffset = loader.getOffset();
adapter.changeCursor(cursor);
int lastSeenPosition = adapter.findLastSeenPosition(lastSeen);
@@ -729,9 +744,44 @@ public class ConversationFragment extends Fragment
return firstVisiblePosition == 0 && list.getChildAt(0).getBottom() <= list.getHeight();
}
public void onSearchQueryUpdated(@Nullable String query) {
if (getListAdapter() != null) {
getListAdapter().onSearchQueryUpdated(query);
}
}
public void jumpToMessage(@NonNull Address author, long timestamp, @Nullable Runnable onMessageNotFound) {
SimpleTask.run(getLifecycle(), () -> {
return DatabaseFactory.getMmsSmsDatabase(getContext())
.getMessagePositionInConversation(threadId, timestamp, author);
}, p -> moveToMessagePosition(p, onMessageNotFound));
}
private void moveToMessagePosition(int position, @Nullable Runnable onMessageNotFound) {
Log.d(TAG, "Moving to message position: " + position + " activeOffset: " + activeOffset + " cursorCount: " + getListAdapter().getCursorCount());
if (position >= activeOffset && position >= 0 && position < getListAdapter().getCursorCount()) {
int offset = activeOffset > 0 ? activeOffset - 1 : 0;
list.scrollToPosition(position - offset);
getListAdapter().pulseHighlightItem(position - offset);
} else if (position < 0) {
Log.w(TAG, "Tried to navigate to message, but it wasn't found.");
if (onMessageNotFound != null) {
onMessageNotFound.run();
}
} else {
Log.i(TAG, "Message was outside of the loaded range. Need to restart the loader.");
firstLoad = true;
startingPosition = position;
getLoaderManager().restartLoader(0, Bundle.EMPTY, ConversationFragment.this);
}
}
public interface ConversationFragmentListener {
void setThreadId(long threadId);
void handleReplyMessage(MessageRecord messageRecord);
void onMessageActionToolbarOpened();
}
private class ConversationScrollListener extends OnScrollListener {
@@ -843,41 +893,14 @@ public class ConversationFragment extends Fragment
return;
}
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... voids) {
if (getActivity() == null || getActivity().isFinishing()) {
Log.w(TAG, "Task to retrieve quote position started after the fragment was detached.");
return 0;
}
return DatabaseFactory.getMmsSmsDatabase(getContext())
.getQuotedMessagePosition(threadId,
messageRecord.getQuote().getId(),
messageRecord.getQuote().getAuthor());
}
@Override
protected void onPostExecute(Integer position) {
if (getActivity() == null || getActivity().isFinishing()) {
Log.w(TAG, "Task to retrieve quote position finished after the fragment was detached.");
return;
}
if (position >= 0 && position < getListAdapter().getItemCount()) {
list.scrollToPosition(position);
getListAdapter().pulseHighlightItem(position);
} else if (position < 0) {
Log.w(TAG, "Tried to navigate to quoted message, but it was deleted.");
Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_no_longer_available, Toast.LENGTH_SHORT).show();
} else {
Log.i(TAG, "Quoted message was outside of the loaded range. Need to restart the loader.");
firstLoad = true;
startingPosition = position;
getLoaderManager().restartLoader(0, Bundle.EMPTY, ConversationFragment.this);
}
}
}.execute();
SimpleTask.run(getLifecycle(), () -> {
return DatabaseFactory.getMmsSmsDatabase(getContext())
.getQuotedMessagePosition(threadId,
messageRecord.getQuote().getId(),
messageRecord.getQuote().getAuthor());
}, p -> moveToMessagePosition(p, () -> {
Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_no_longer_available, Toast.LENGTH_SHORT).show();
}));
}
@Override
@@ -960,6 +983,7 @@ public class ConversationFragment extends Fragment
}
setCorrectMenuVisibility(menu);
listener.onMessageActionToolbarOpened();
return true;
}

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversation;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
@@ -28,13 +28,22 @@ import android.support.annotation.DimenRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.util.AttributeSet;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.ConfirmIdentityDialog;
import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.LinkPreviewView;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
@@ -81,6 +90,7 @@ import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -200,6 +210,7 @@ public class ConversationItem extends LinearLayout
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient conversationRecipient,
@Nullable String searchQuery,
boolean pulseHighlight)
{
this.messageRecord = messageRecord;
@@ -217,7 +228,7 @@ public class ConversationItem extends LinearLayout
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread);
setInteractionState(messageRecord, pulseHighlight);
setBodyText(messageRecord);
setBodyText(messageRecord, searchQuery);
setBubbleState(messageRecord);
setStatusIcons(messageRecord);
setContactPhoto(recipient);
@@ -395,7 +406,7 @@ public class ConversationItem extends LinearLayout
return messageRecord.isMms() && !((MmsMessageRecord)messageRecord).getLinkPreviews().isEmpty();
}
private void setBodyText(MessageRecord messageRecord) {
private void setBodyText(MessageRecord messageRecord, @Nullable String searchQuery) {
bodyText.setClickable(false);
bodyText.setFocusable(false);
bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context));
@@ -403,7 +414,11 @@ public class ConversationItem extends LinearLayout
if (isCaptionlessMms(messageRecord)) {
bodyText.setVisibility(View.GONE);
} else {
bodyText.setText(linkifyMessageBody(messageRecord.getDisplayBody(), batchSelected.isEmpty()));
Spannable styledText = linkifyMessageBody(messageRecord.getDisplayBody(), batchSelected.isEmpty());
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery);
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery);
bodyText.setText(styledText);
bodyText.setVisibility(View.VISIBLE);
}
}
@@ -414,7 +429,7 @@ public class ConversationItem extends LinearLayout
@NonNull Recipient conversationRecipient,
boolean isGroupThread)
{
boolean showControls = !messageRecord.isFailed() && !Util.isOwnNumber(context, conversationRecipient.getAddress());
boolean showControls = !messageRecord.isFailed();
if (hasSharedContact(messageRecord)) {
sharedContactStub.get().setVisibility(VISIBLE);

View File

@@ -1,10 +1,12 @@
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversation;
import android.content.Intent;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.ActivityOptionsCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import android.view.Display;
import android.view.Gravity;

View File

@@ -0,0 +1,147 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.content.Context;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.database.CursorList;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.util.CloseableLiveData;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Util;
import java.io.Closeable;
import java.util.List;
public class ConversationSearchViewModel extends AndroidViewModel {
private final SearchRepository searchRepository;
private final CloseableLiveData<SearchResult> result;
private final Debouncer debouncer;
private boolean firstSearch;
private boolean searchOpen;
private String activeQuery;
private long activeThreadId;
public ConversationSearchViewModel(@NonNull Application application) {
super(application);
Context context = application.getApplicationContext();
result = new CloseableLiveData<>();
debouncer = new Debouncer(500);
searchRepository = new SearchRepository(context,
DatabaseFactory.getSearchDatabase(context),
DatabaseFactory.getContactsDatabase(context),
DatabaseFactory.getThreadDatabase(context),
ContactAccessor.getInstance(),
AsyncTask.THREAD_POOL_EXECUTOR);
}
LiveData<SearchResult> getSearchResults() {
return result;
}
void onQueryUpdated(@NonNull String query, long threadId) {
if (firstSearch && query.length() < 2) {
result.postValue(new SearchResult(CursorList.emptyList(), 0));
return;
}
if (query.equals(activeQuery)) {
return;
}
updateQuery(query, threadId);
}
void onMissingResult() {
if (activeQuery != null) {
updateQuery(activeQuery, activeThreadId);
}
}
void onMoveUp() {
debouncer.clear();
CursorList<MessageResult> messages = (CursorList<MessageResult>) result.getValue().getResults();
int position = Math.min(result.getValue().getPosition() + 1, messages.size() - 1);
result.setValue(new SearchResult(messages, position), false);
}
void onMoveDown() {
debouncer.clear();
CursorList<MessageResult> messages = (CursorList<MessageResult>) result.getValue().getResults();
int position = Math.max(result.getValue().getPosition() - 1, 0);
result.setValue(new SearchResult(messages, position), false);
}
void onSearchOpened() {
searchOpen = true;
firstSearch = true;
}
void onSearchClosed() {
searchOpen = false;
debouncer.clear();
result.close();
}
@Override
protected void onCleared() {
super.onCleared();
result.close();
}
private void updateQuery(@NonNull String query, long threadId) {
activeQuery = query;
activeThreadId = threadId;
debouncer.publish(() -> {
firstSearch = false;
searchRepository.query(query, threadId, messages -> {
Util.runOnMain(() -> {
if (searchOpen && query.equals(activeQuery)) {
result.setValue(new SearchResult(messages, 0));
} else {
messages.close();
}
});
});
});
}
static class SearchResult implements Closeable {
private final CursorList<MessageResult> results;
private final int position;
SearchResult(CursorList<MessageResult> results, int position) {
this.results = results;
this.position = position;
}
public List<MessageResult> getResults() {
return results;
}
public int getPosition() {
return position;
}
@Override
public void close() {
results.close();
}
}
}

View File

@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversation;
import android.annotation.SuppressLint;
import android.content.Context;
@@ -14,6 +14,7 @@ import android.widget.TextView;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -31,6 +32,7 @@ public class ConversationTitleView extends RelativeLayout {
private TextView title;
private TextView subtitle;
private ImageView verified;
private View subtitleContainer;
public ConversationTitleView(Context context) {
this(context, null);
@@ -45,12 +47,13 @@ public class ConversationTitleView extends RelativeLayout {
public void onFinishInflate() {
super.onFinishInflate();
this.back = ViewUtil.findById(this, R.id.up_button);
this.content = ViewUtil.findById(this, R.id.content);
this.title = ViewUtil.findById(this, R.id.title);
this.subtitle = ViewUtil.findById(this, R.id.subtitle);
this.verified = ViewUtil.findById(this, R.id.verified_indicator);
this.avatar = ViewUtil.findById(this, R.id.contact_photo_image);
this.back = ViewUtil.findById(this, R.id.up_button);
this.content = ViewUtil.findById(this, R.id.content);
this.title = ViewUtil.findById(this, R.id.title);
this.subtitle = ViewUtil.findById(this, R.id.subtitle);
this.verified = ViewUtil.findById(this, R.id.verified_indicator);
this.subtitleContainer = ViewUtil.findById(this, R.id.subtitle_container);
this.avatar = ViewUtil.findById(this, R.id.contact_photo_image);
ViewUtil.setTextViewGravityStart(this.title, getContext());
ViewUtil.setTextViewGravityStart(this.subtitle, getContext());
@@ -101,6 +104,7 @@ public class ConversationTitleView extends RelativeLayout {
private void setRecipientTitle(Recipient recipient) {
if (recipient.isGroupRecipient()) setGroupRecipientTitle(recipient);
else if (recipient.isLocalNumber()) setSelfTitle();
else if (TextUtils.isEmpty(recipient.getName())) setNonContactRecipientTitle(recipient);
else setContactRecipientTitle(recipient);
}
@@ -115,11 +119,18 @@ public class ConversationTitleView extends RelativeLayout {
.collect(Collectors.joining(", ")));
this.subtitle.setVisibility(View.VISIBLE);
this.subtitleContainer.setVisibility(VISIBLE);
}
private void setSelfTitle() {
this.title.setText(R.string.note_to_self);
this.subtitleContainer.setVisibility(View.GONE);
}
@SuppressLint("SetTextI18n")
private void setNonContactRecipientTitle(Recipient recipient) {
this.title.setText(recipient.getAddress().serialize());
this.subtitleContainer.setVisibility(VISIBLE);
if (TextUtils.isEmpty(recipient.getProfileName())) {
this.subtitle.setText(null);
@@ -137,5 +148,6 @@ public class ConversationTitleView extends RelativeLayout {
else this.subtitle.setText(recipient.getAddress().serialize());
this.subtitle.setVisibility(View.VISIBLE);
this.subtitleContainer.setVisibility(VISIBLE);
}
}

View File

@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.content.Intent;
@@ -13,6 +13,9 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
@@ -76,6 +79,7 @@ public class ConversationUpdateItem extends LinearLayout
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient conversationRecipient,
@Nullable String searchQuery,
boolean pulseUpdate)
{
this.batchSelected = batchSelected;

View File

@@ -30,6 +30,8 @@ public class CursorList<T> implements List<T>, Closeable {
public CursorList(@NonNull Cursor cursor, @NonNull ModelBuilder<T> modelBuilder) {
this.cursor = cursor;
this.modelBuilder = modelBuilder;
forceQueryLoad();
}
public static <T> CursorList<T> emptyList() {
@@ -195,6 +197,10 @@ public class CursorList<T> implements List<T>, Closeable {
cursor.unregisterContentObserver(observer);
}
private void forceQueryLoad() {
cursor.getCount();
}
public interface ModelBuilder<T> {
T build(@NonNull Cursor cursor);
}

View File

@@ -124,6 +124,10 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
+ (hasFooterView() ? 1 : 0);
}
public int getCursorCount() {
return cursor.getCount();
}
@SuppressWarnings("unchecked")
@Override
public final void onViewRecycled(ViewHolder holder) {
@@ -190,7 +194,7 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
throw new IllegalStateException("this should only be called when the cursor is valid");
}
if (!cursor.moveToPosition(getCursorPosition(position))) {
throw new IllegalStateException("couldn't move cursor to position " + position);
throw new IllegalStateException("couldn't move cursor to position " + position + " (actual cursor position " + getCursorPosition(position) + ")");
}
return cursor;
}

View File

@@ -248,6 +248,10 @@ public class GroupDatabase extends Database {
databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?",
new String[] {groupId});
Recipient.applyCached(Address.fromSerialized(groupId), recipient -> {
recipient.setParticipants(Stream.of(members).map(a -> Recipient.from(context, a, false)).toList());
});
}
public void remove(String groupId, Address source) {
@@ -259,6 +263,14 @@ public class GroupDatabase extends Database {
databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?",
new String[] {groupId});
Recipient.applyCached(Address.fromSerialized(groupId), recipient -> {
List<Recipient> current = recipient.getParticipants();
Recipient removal = Recipient.from(context, source, false);
current.remove(removal);
recipient.setParticipants(current);
});
}
private List<Address> getCurrentMembers(String groupId) {

View File

@@ -183,6 +183,26 @@ public class MmsSmsDatabase extends Database {
return -1;
}
public int getMessagePositionInConversation(long threadId, long receivedTimestamp, @NonNull Address address) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.ADDRESS }, selection, order, null)) {
String serializedAddress = address.serialize();
boolean isOwnNumber = Util.isOwnNumber(context, address);
while (cursor != null && cursor.moveToNext()) {
boolean timestampMatches = cursor.getLong(0) == receivedTimestamp;
boolean addressMatches = serializedAddress.equals(cursor.getString(1));
if (timestampMatches && (addressMatches || isOwnNumber)) {
return cursor.getPosition();
}
}
}
return -1;
}
/**
* Retrieves the position of the message with the provided timestamp in the query results you'd
* get from calling {@link #getConversation(long)}.

View File

@@ -21,78 +21,122 @@ public class SearchDatabase extends Database {
public static final String SMS_FTS_TABLE_NAME = "sms_fts";
public static final String MMS_FTS_TABLE_NAME = "mms_fts";
public static final String ID = "rowid";
public static final String BODY = MmsSmsColumns.BODY;
public static final String RANK = "rank";
public static final String SNIPPET = "snippet";
public static final String ID = "rowid";
public static final String BODY = MmsSmsColumns.BODY;
public static final String THREAD_ID = MmsSmsColumns.THREAD_ID;
public static final String SNIPPET = "snippet";
public static final String CONVERSATION_ADDRESS = "conversation_address";
public static final String MESSAGE_ADDRESS = "message_address";
public static final String[] CREATE_TABLE = {
"CREATE VIRTUAL TABLE " + SMS_FTS_TABLE_NAME + " USING fts5(" + BODY + ", content=" + SmsDatabase.TABLE_NAME + ", content_rowid=" + SmsDatabase.ID + ");",
"CREATE VIRTUAL TABLE " + SMS_FTS_TABLE_NAME + " USING fts5(" + BODY + ", " + THREAD_ID + " UNINDEXED, content=" + SmsDatabase.TABLE_NAME + ", content_rowid=" + SmsDatabase.ID + ");",
"CREATE TRIGGER sms_ai AFTER INSERT ON " + SmsDatabase.TABLE_NAME + " BEGIN\n" +
" INSERT INTO " + SMS_FTS_TABLE_NAME + "(" + ID + ", " + BODY + ") VALUES (new." + SmsDatabase.ID + ", new." + SmsDatabase.BODY + ");\n" +
" INSERT INTO " + SMS_FTS_TABLE_NAME + "(" + ID + ", " + BODY + ", " + THREAD_ID + ") VALUES (new." + SmsDatabase.ID + ", new." + SmsDatabase.BODY + ", new." + SmsDatabase.THREAD_ID + ");\n" +
"END;\n",
"CREATE TRIGGER sms_ad AFTER DELETE ON " + SmsDatabase.TABLE_NAME + " BEGIN\n" +
" INSERT INTO " + SMS_FTS_TABLE_NAME + "(" + SMS_FTS_TABLE_NAME + ", " + ID + ", " + BODY + ") VALUES('delete', old." + SmsDatabase.ID + ", old." + SmsDatabase.BODY + ");\n" +
" INSERT INTO " + SMS_FTS_TABLE_NAME + "(" + SMS_FTS_TABLE_NAME + ", " + ID + ", " + BODY + ", " + THREAD_ID + ") VALUES('delete', old." + SmsDatabase.ID + ", old." + SmsDatabase.BODY + ", old." + SmsDatabase.THREAD_ID + ");\n" +
"END;\n",
"CREATE TRIGGER sms_au AFTER UPDATE ON " + SmsDatabase.TABLE_NAME + " BEGIN\n" +
" INSERT INTO " + SMS_FTS_TABLE_NAME + "(" + SMS_FTS_TABLE_NAME + ", " + ID + ", " + BODY + ") VALUES('delete', old." + SmsDatabase.ID + ", old." + SmsDatabase.BODY + ");\n" +
" INSERT INTO " + SMS_FTS_TABLE_NAME + "(" + ID + ", " + BODY + ") VALUES(new." + SmsDatabase.ID + ", new." + SmsDatabase.BODY + ");\n" +
" INSERT INTO " + SMS_FTS_TABLE_NAME + "(" + SMS_FTS_TABLE_NAME + ", " + ID + ", " + BODY + ", " + THREAD_ID + ") VALUES('delete', old." + SmsDatabase.ID + ", old." + SmsDatabase.BODY + ", old." + SmsDatabase.THREAD_ID + ");\n" +
" INSERT INTO " + SMS_FTS_TABLE_NAME + "(" + ID + ", " + BODY + ", " + THREAD_ID + ") VALUES(new." + SmsDatabase.ID + ", new." + SmsDatabase.BODY + ", new." + SmsDatabase.THREAD_ID + ");\n" +
"END;",
"CREATE VIRTUAL TABLE " + MMS_FTS_TABLE_NAME + " USING fts5(" + BODY + ", content=" + MmsDatabase.TABLE_NAME + ", content_rowid=" + MmsDatabase.ID + ");",
"CREATE VIRTUAL TABLE " + MMS_FTS_TABLE_NAME + " USING fts5(" + BODY + ", " + THREAD_ID + " UNINDEXED, content=" + MmsDatabase.TABLE_NAME + ", content_rowid=" + MmsDatabase.ID + ");",
"CREATE TRIGGER mms_ai AFTER INSERT ON " + MmsDatabase.TABLE_NAME + " BEGIN\n" +
" INSERT INTO " + MMS_FTS_TABLE_NAME + "(" + ID + ", " + BODY + ") VALUES (new." + MmsDatabase.ID + ", new." + MmsDatabase.BODY + ");\n" +
" INSERT INTO " + MMS_FTS_TABLE_NAME + "(" + ID + ", " + BODY + ", " + THREAD_ID + ") VALUES (new." + MmsDatabase.ID + ", new." + MmsDatabase.BODY + ", new." + MmsDatabase.THREAD_ID + ");\n" +
"END;\n",
"CREATE TRIGGER mms_ad AFTER DELETE ON " + MmsDatabase.TABLE_NAME + " BEGIN\n" +
" INSERT INTO " + MMS_FTS_TABLE_NAME + "(" + MMS_FTS_TABLE_NAME + ", " + ID + ", " + BODY + ") VALUES('delete', old." + MmsDatabase.ID + ", old." + MmsDatabase.BODY + ");\n" +
" INSERT INTO " + MMS_FTS_TABLE_NAME + "(" + MMS_FTS_TABLE_NAME + ", " + ID + ", " + BODY + ", " + THREAD_ID + ") VALUES('delete', old." + MmsDatabase.ID + ", old." + MmsDatabase.BODY + ", old." + MmsDatabase.THREAD_ID + ");\n" +
"END;\n",
"CREATE TRIGGER mms_au AFTER UPDATE ON " + MmsDatabase.TABLE_NAME + " BEGIN\n" +
" INSERT INTO " + MMS_FTS_TABLE_NAME + "(" + MMS_FTS_TABLE_NAME + ", " + ID + ", " + BODY + ") VALUES('delete', old." + MmsDatabase.ID + ", old." + MmsDatabase.BODY + ");\n" +
" INSERT INTO " + MMS_FTS_TABLE_NAME + "(" + ID + ", " + BODY + ") VALUES (new." + MmsDatabase.ID + ", new." + MmsDatabase.BODY + ");\n" +
" INSERT INTO " + MMS_FTS_TABLE_NAME + "(" + MMS_FTS_TABLE_NAME + ", " + ID + ", " + BODY + ", " + THREAD_ID + ") VALUES('delete', old." + MmsDatabase.ID + ", old." + MmsDatabase.BODY + ", old." + MmsDatabase.THREAD_ID + ");\n" +
" INSERT INTO " + MMS_FTS_TABLE_NAME + "(" + ID + ", " + BODY + ", " + THREAD_ID + ") VALUES (new." + MmsDatabase.ID + ", new." + MmsDatabase.BODY + ", new." + MmsDatabase.THREAD_ID + ");\n" +
"END;"
};
private static final String MESSAGES_QUERY =
"SELECT " +
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + ", " +
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
"snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " +
MmsSmsColumns.THREAD_ID + " " +
SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
"FROM " + SmsDatabase.TABLE_NAME + " " +
"INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " +
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + SmsDatabase.TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
"WHERE " + SMS_FTS_TABLE_NAME + " MATCH ? " +
"UNION ALL " +
"SELECT " +
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + ", " +
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
"snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " +
MmsSmsColumns.THREAD_ID + " " +
MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
"FROM " + MmsDatabase.TABLE_NAME + " " +
"INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " +
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MmsDatabase.TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " +
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
"LIMIT 500";
private static final String MESSAGES_FOR_THREAD_QUERY =
"SELECT " +
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
"snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " +
SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
"FROM " + SmsDatabase.TABLE_NAME + " " +
"INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " +
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
"WHERE " + SMS_FTS_TABLE_NAME + " MATCH ? AND " + SmsDatabase.TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? " +
"UNION ALL " +
"SELECT " +
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
"snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " +
MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
"FROM " + MmsDatabase.TABLE_NAME + " " +
"INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " +
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? AND " + MmsDatabase.TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? " +
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
"LIMIT 500";
public SearchDatabase(@NonNull Context context, @NonNull SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public Cursor queryMessages(@NonNull String query) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
List<String> tokens = Stream.of(query.split(" ")).filter(s -> s.trim().length() > 0).toList();
String prefixQuery = Util.join(tokens, "* ");
prefixQuery += "*";
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String prefixQuery = adjustQuery(query);
Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { prefixQuery, prefixQuery });
setNotifyConverationListListeners(cursor);
return cursor;
}
public Cursor queryMessages(@NonNull String query, long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String prefixQuery = adjustQuery(query);
Cursor cursor = db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { prefixQuery, String.valueOf(threadId), prefixQuery, String.valueOf(threadId) });
setNotifyConverationListListeners(cursor);
return cursor;
}
private String adjustQuery(@NonNull String query) {
List<String> tokens = Stream.of(query.split(" ")).filter(s -> s.trim().length() > 0).toList();
String prefixQuery = Util.join(tokens, "* ");
prefixQuery += "*";
return prefixQuery;
}
}

View File

@@ -60,8 +60,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
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 CONVERSATION_SEARCH = 17;
private static final int SELF_ATTACHMENT_CLEANUP = 18;
private static final int DATABASE_VERSION = 16;
private static final int DATABASE_VERSION = 18;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -201,9 +203,29 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
}
if (oldVersion < FULL_TEXT_SEARCH) {
for (String sql : SearchDatabase.CREATE_TABLE) {
db.execSQL(sql);
}
db.execSQL("CREATE VIRTUAL TABLE sms_fts USING fts5(body, content=sms, content_rowid=_id)");
db.execSQL("CREATE TRIGGER sms_ai AFTER INSERT ON sms BEGIN\n" +
" INSERT INTO sms_fts(rowid, body) VALUES (new._id, new.body);\n" +
"END;");
db.execSQL("CREATE TRIGGER sms_ad AFTER DELETE ON sms BEGIN\n" +
" INSERT INTO sms_fts(sms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" +
"END;\n");
db.execSQL("CREATE TRIGGER sms_au AFTER UPDATE ON sms BEGIN\n" +
" INSERT INTO sms_fts(sms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" +
" INSERT INTO sms_fts(rowid, body) VALUES(new._id, new.body);\n" +
"END;");
db.execSQL("CREATE VIRTUAL TABLE mms_fts USING fts5(body, content=mms, content_rowid=_id)");
db.execSQL("CREATE TRIGGER mms_ai AFTER INSERT ON mms BEGIN\n" +
" INSERT INTO mms_fts(rowid, body) VALUES (new._id, new.body);\n" +
"END;");
db.execSQL("CREATE TRIGGER mms_ad AFTER DELETE ON mms BEGIN\n" +
" INSERT INTO mms_fts(mms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" +
"END;\n");
db.execSQL("CREATE TRIGGER mms_au AFTER UPDATE ON mms BEGIN\n" +
" INSERT INTO mms_fts(mms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" +
" INSERT INTO mms_fts(rowid, body) VALUES(new._id, new.body);\n" +
"END;");
Log.i(TAG, "Beginning to build search index.");
long start = SystemClock.elapsedRealtime();
@@ -313,6 +335,73 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE mms ADD COLUMN previews TEXT");
}
if (oldVersion < CONVERSATION_SEARCH) {
db.execSQL("DROP TABLE sms_fts");
db.execSQL("DROP TABLE mms_fts");
db.execSQL("DROP TRIGGER sms_ai");
db.execSQL("DROP TRIGGER sms_au");
db.execSQL("DROP TRIGGER sms_ad");
db.execSQL("DROP TRIGGER mms_ai");
db.execSQL("DROP TRIGGER mms_au");
db.execSQL("DROP TRIGGER mms_ad");
db.execSQL("CREATE VIRTUAL TABLE sms_fts USING fts5(body, thread_id UNINDEXED, content=sms, content_rowid=_id)");
db.execSQL("CREATE TRIGGER sms_ai AFTER INSERT ON sms BEGIN\n" +
" INSERT INTO sms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n" +
"END;");
db.execSQL("CREATE TRIGGER sms_ad AFTER DELETE ON sms BEGIN\n" +
" INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" +
"END;\n");
db.execSQL("CREATE TRIGGER sms_au AFTER UPDATE ON sms BEGIN\n" +
" INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" +
" INSERT INTO sms_fts(rowid, body, thread_id) VALUES(new._id, new.body, new.thread_id);\n" +
"END;");
db.execSQL("CREATE VIRTUAL TABLE mms_fts USING fts5(body, thread_id UNINDEXED, content=mms, content_rowid=_id)");
db.execSQL("CREATE TRIGGER mms_ai AFTER INSERT ON mms BEGIN\n" +
" INSERT INTO mms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n" +
"END;");
db.execSQL("CREATE TRIGGER mms_ad AFTER DELETE ON mms BEGIN\n" +
" INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" +
"END;\n");
db.execSQL("CREATE TRIGGER mms_au AFTER UPDATE ON mms BEGIN\n" +
" INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" +
" INSERT INTO mms_fts(rowid, body, thread_id) VALUES(new._id, new.body, new.thread_id);\n" +
"END;");
Log.i(TAG, "Beginning to build search index.");
long start = SystemClock.elapsedRealtime();
db.execSQL("INSERT INTO sms_fts (rowid, body, thread_id) SELECT _id, body, thread_id FROM sms");
long smsFinished = SystemClock.elapsedRealtime();
Log.i(TAG, "Indexing SMS completed in " + (smsFinished - start) + " ms");
db.execSQL("INSERT INTO mms_fts (rowid, body, thread_id) SELECT _id, body, thread_id FROM mms");
long mmsFinished = SystemClock.elapsedRealtime();
Log.i(TAG, "Indexing MMS completed in " + (mmsFinished - smsFinished) + " ms");
Log.i(TAG, "Indexing finished. Total time: " + (mmsFinished - start) + " ms");
}
if (oldVersion < SELF_ATTACHMENT_CLEANUP) {
String localNumber = TextSecurePreferences.getLocalNumber(context);
if (!TextUtils.isEmpty(localNumber)) {
try (Cursor threadCursor = db.rawQuery("SELECT _id FROM thread WHERE recipient_ids = ?", new String[]{ localNumber })) {
if (threadCursor != null && threadCursor.moveToFirst()) {
long threadId = threadCursor.getLong(0);
ContentValues updateValues = new ContentValues(1);
updateValues.put("pending_push", 0);
int count = db.update("part", updateValues, "mid IN (SELECT _id FROM mms WHERE thread_id = ?)", new String[]{ String.valueOf(threadId) });
Log.i(TAG, "Updated " + count + " self-sent attachments.");
}
}
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.util.PowerManagerCompat;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.WakeLockUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
@@ -36,6 +37,7 @@ public class FcmService extends FirebaseMessagingService implements InjectableTy
private static final String TAG = FcmService.class.getSimpleName();
private static final Executor MESSAGE_EXECUTOR = SignalExecutors.newCachedSingleThreadExecutor("FcmMessageProcessing");
private static final String WAKE_LOCK_TAG = "FcmMessageProcessing";
@Inject SignalServiceMessageReceiver messageReceiver;
@@ -45,7 +47,10 @@ public class FcmService extends FirebaseMessagingService implements InjectableTy
public void onMessageReceived(RemoteMessage remoteMessage) {
Log.i(TAG, "FCM message... Original Priority: " + remoteMessage.getOriginalPriority() + ", Actual Priority: " + remoteMessage.getPriority());
ApplicationContext.getInstance(getApplicationContext()).injectDependencies(this);
handleReceivedNotification(getApplicationContext());
WakeLockUtil.runWithLock(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK, 60000, WAKE_LOCK_TAG, () -> {
handleReceivedNotification(getApplicationContext());
});
}
@Override

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.gcm;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import com.google.firebase.iid.FirebaseInstanceId;
@@ -23,7 +24,7 @@ public final class FcmUtil {
AtomicReference<String> token = new AtomicReference<>(null);
FirebaseInstanceId.getInstance().getInstanceId().addOnCompleteListener(task -> {
if (task.isSuccessful() && task.getResult() != null) {
if (task.isSuccessful() && task.getResult() != null && !TextUtils.isEmpty(task.getResult().getToken())) {
token.set(task.getResult().getToken());
} else {
Log.w(TAG, "Failed to get the token.", task.getException());

View File

@@ -91,8 +91,13 @@ public class FcmRefreshJob extends ContextJob implements InjectableType {
Optional<String> token = FcmUtil.getToken();
if (token.isPresent()) {
if (!token.get().equals(TextSecurePreferences.getFcmToken(context))) {
Log.i(TAG, "New token differs from the old token.");
String oldToken = TextSecurePreferences.getFcmToken(context);
if (!token.get().equals(oldToken)) {
int oldLength = oldToken != null ? oldToken.length() : -1;
Log.i(TAG, "Token changed. oldLength: " + oldLength + " newLength: " + token.get().length());
} else {
Log.i(TAG, "Token didn't change.");
}
textSecureAccountManager.setGcmId(token);

View File

@@ -5,6 +5,7 @@ import android.Manifest;
import android.content.Context;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.backup.BackupPassphrase;
import org.thoughtcrime.securesms.jobmanager.SafeData;
import org.thoughtcrime.securesms.logging.Log;
@@ -68,7 +69,7 @@ public class LocalBackupJob extends ContextJob {
R.drawable.ic_signal_backup);
try {
String backupPassword = TextSecurePreferences.getBackupPassphrase(context);
String backupPassword = BackupPassphrase.get(context);
File backupDirectory = StorageUtil.getBackupDirectory();
String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(new Date());
String fileName = String.format("signal-%s.backup", timestamp);

View File

@@ -199,11 +199,12 @@ public class MultiDeviceContactUpdateJob extends ContextJob implements Injectabl
}
if (ProfileKeyUtil.hasProfileKey(context)) {
Recipient self = Recipient.from(context, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), false);
out.write(new DeviceContact(TextSecurePreferences.getLocalNumber(context),
Optional.absent(), Optional.absent(),
Optional.absent(), Optional.absent(),
Optional.of(self.getColor().serialize()), Optional.absent(),
Optional.of(ProfileKeyUtil.getProfileKey(context)),
false, Optional.absent()));
false, self.getExpireMessages() > 0 ? Optional.of(self.getExpireMessages()) : Optional.absent()));
}
out.close();

View File

@@ -12,6 +12,7 @@ import android.support.v4.app.NotificationManagerCompat;
import android.text.TextUtils;
import android.util.Pair;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
@@ -56,6 +57,7 @@ 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.Link;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
@@ -733,6 +735,12 @@ public class PushDecryptJob extends ContextJob {
message.getMessage().getExpiresInSeconds() * 1000L);
}
if (recipients.isLocalNumber()) {
SyncMessageId id = new SyncMessageId(recipients.getAddress(), message.getTimestamp());
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
}
return threadId;
}
@@ -825,6 +833,12 @@ public class PushDecryptJob extends ContextJob {
.scheduleDeletion(messageId, isGroup, message.getExpirationStartTimestamp(), expiresInMillis);
}
if (recipient.isLocalNumber()) {
SyncMessageId id = new SyncMessageId(recipient.getAddress(), message.getTimestamp());
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
}
return threadId;
}
@@ -1058,7 +1072,7 @@ public class PushDecryptJob extends ContextJob {
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 presentInBody = url.isPresent() && Stream.of(LinkPreviewUtil.findWhitelistedUrls(message)).map(Link::getUrl).collect(Collectors.toSet()).contains(url.get());
boolean validDomain = url.isPresent() && LinkPreviewUtil.isWhitelistedLinkUrl(url.get());
if (hasContent && presentInBody && validDomain) {

View File

@@ -11,8 +11,8 @@ 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.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
@@ -20,7 +20,6 @@ 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,10 +31,12 @@ import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
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.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
@@ -131,7 +132,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
try {
log(TAG, "Sending message: " + messageId);
Recipient recipient = message.getRecipient().resolve();
byte[] profileKey = recipient.getProfileKey();
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
@@ -142,6 +143,12 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
markAttachmentsUploaded(messageId, message.getAttachments());
database.markUnidentified(messageId, unidentified);
if (recipient.isLocalNumber()) {
SyncMessageId id = new SyncMessageId(recipient.getAddress(), message.getSentTimeMillis());
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
}
if (TextSecurePreferences.isUnidentifiedDeliveryEnabled(context)) {
if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) {
log(TAG, "Marking recipient as UD-unrestricted following a UD send.");
@@ -215,7 +222,15 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
.asExpirationUpdate(message.isExpirationUpdate())
.build();
return messageSender.sendMessage(address, UnidentifiedAccessUtil.getAccessFor(context, message.getRecipient()), mediaMessage).getSuccess().isUnidentified();
if (address.getNumber().equals(TextSecurePreferences.getLocalNumber(context))) {
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, mediaMessage, syncAccess);
messageSender.sendMessage(syncMessage, syncAccess);
return syncAccess.isPresent();
} else {
return messageSender.sendMessage(address, UnidentifiedAccessUtil.getAccessFor(context, message.getRecipient()), mediaMessage).getSuccess().isUnidentified();
}
} catch (UnregisteredUserException e) {
warn(TAG, e);
throw new InsecureFallbackApprovalException(e);

View File

@@ -20,7 +20,6 @@ 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;
@@ -34,16 +33,20 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
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.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -280,5 +283,16 @@ public abstract class PushSendJob extends SendJob {
}
}
protected SignalServiceSyncMessage buildSelfSendSyncMessage(@NonNull Context context, @NonNull SignalServiceDataMessage message, Optional<UnidentifiedAccessPair> syncAccess) {
String localNumber = TextSecurePreferences.getLocalNumber(context);
SentTranscriptMessage transcript = new SentTranscriptMessage(localNumber,
message.getTimestamp(),
message,
message.getExpiresInSeconds(),
Collections.singletonMap(localNumber, syncAccess.isPresent()));
return SignalServiceSyncMessage.forSentTranscript(transcript);
}
protected abstract void onPushSend() throws Exception;
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
import org.thoughtcrime.securesms.jobmanager.SafeData;
@@ -25,6 +26,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
@@ -94,6 +96,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
database.markAsSent(messageId, true);
database.markUnidentified(messageId, unidentified);
if (recipient.isLocalNumber()) {
SyncMessageId id = new SyncMessageId(recipient.getAddress(), record.getDateSent());
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
}
if (TextSecurePreferences.isUnidentifiedDeliveryEnabled(context)) {
if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) {
log(TAG, "Marking recipient as UD-unrestricted following a UD send.");
@@ -166,7 +174,15 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
.asEndSessionMessage(message.isEndSession())
.build();
return messageSender.sendMessage(address, unidentifiedAccess, textSecureMessage).getSuccess().isUnidentified();
if (address.getNumber().equals(TextSecurePreferences.getLocalNumber(context))) {
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, textSecureMessage, syncAccess);
messageSender.sendMessage(syncMessage, syncAccess);
return syncAccess.isPresent();
} else {
return messageSender.sendMessage(address, unidentifiedAccess, textSecureMessage).getSuccess().isUnidentified();
}
} catch (UnregisteredUserException e) {
warn(TAG, "Failure", e);
throw new InsecureFallbackApprovalException(e);

View File

@@ -0,0 +1,20 @@
package org.thoughtcrime.securesms.linkpreview;
public class Link {
private final String url;
private final int position;
public Link(String url, int position) {
this.url = url;
this.position = position;
}
public String getUrl() {
return url;
}
public int getPosition() {
return position;
}
}

View File

@@ -18,7 +18,7 @@ public final class LinkPreviewUtil {
/**
* @return All whitelisted URLs in the source text.
*/
public static @NonNull List<String> findWhitelistedUrls(@NonNull String text) {
public static @NonNull List<Link> findWhitelistedUrls(@NonNull String text) {
SpannableString spannable = new SpannableString(text);
boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS);
@@ -27,8 +27,8 @@ public final class LinkPreviewUtil {
}
return Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
.map(URLSpan::getURL)
.filter(LinkPreviewUtil::isWhitelistedLinkUrl)
.map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
.filter(link -> isWhitelistedLinkUrl(link.getUrl()))
.toList();
}

View File

@@ -7,6 +7,9 @@ import android.arch.lifecycle.ViewModelProvider;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import org.thoughtcrime.securesms.attachments.Attachment;
@@ -81,7 +84,7 @@ public class LinkPreviewViewModel extends ViewModel {
return Collections.singletonList(new LinkPreview(originalPreview.getUrl(), originalPreview.getTitle(), Optional.of(newAttachment)));
}
public void onTextChanged(@NonNull Context context, @NonNull String text) {
public void onTextChanged(@NonNull Context context, @NonNull String text, int cursorStart, int cursorEnd) {
debouncer.publish(() -> {
if (TextUtils.isEmpty(text)) {
userCanceled = false;
@@ -91,10 +94,10 @@ public class LinkPreviewViewModel extends ViewModel {
return;
}
List<String> urls = LinkPreviewUtil.findWhitelistedUrls(text);
Optional<String> url = urls.isEmpty() ? Optional.absent() : Optional.of(urls.get(0));
List<Link> links = LinkPreviewUtil.findWhitelistedUrls(text);
Optional<Link> link = links.isEmpty() ? Optional.absent() : Optional.of(links.get(0));
if (url.isPresent() && url.get().equals(activeUrl)) {
if (link.isPresent() && link.get().getUrl().equals(activeUrl)) {
return;
}
@@ -103,7 +106,7 @@ public class LinkPreviewViewModel extends ViewModel {
activeRequest = null;
}
if (!url.isPresent()) {
if (!link.isPresent() || !isCursorPositionValid(text, link.get(), cursorStart, cursorEnd)) {
activeUrl = null;
linkPreviewState.setValue(LinkPreviewState.forEmpty());
return;
@@ -111,8 +114,8 @@ public class LinkPreviewViewModel extends ViewModel {
linkPreviewState.setValue(LinkPreviewState.forLoading());
activeUrl = url.get();
activeRequest = repository.getLinkPreview(context, url.get(), lp -> {
activeUrl = link.get().getUrl();
activeRequest = repository.getLinkPreview(context, link.get().getUrl(), lp -> {
Util.runOnMain(() -> {
if (!userCanceled) {
linkPreviewState.setValue(LinkPreviewState.forPreview(lp));
@@ -123,7 +126,6 @@ public class LinkPreviewViewModel extends ViewModel {
});
}
public void onUserCancel() {
if (activeRequest != null) {
activeRequest.cancel();
@@ -150,6 +152,18 @@ public class LinkPreviewViewModel extends ViewModel {
debouncer.clear();
}
private boolean isCursorPositionValid(@NonNull String text, @NonNull Link link, int cursorStart, int cursorEnd) {
if (cursorStart != cursorEnd) {
return true;
}
if (text.endsWith(link.getUrl()) && cursorStart == link.getPosition() + link.getUrl().length()) {
return true;
}
return cursorStart < link.getPosition() || cursorStart > link.getPosition() + link.getUrl().length();
}
public static class LinkPreviewState {
private final boolean isLoading;
private final Optional<LinkPreview> linkPreview;

View File

@@ -105,6 +105,7 @@ public class MediaPreviewViewModel extends ViewModel {
mediaRecord.getDate(),
mediaRecord.getAttachment().getWidth(),
mediaRecord.getAttachment().getHeight(),
mediaRecord.getAttachment().getSize(),
Optional.absent(),
Optional.fromNullable(mediaRecord.getAttachment().getCaption()));
}

View File

@@ -19,16 +19,18 @@ public class Media implements Parcelable {
private final long date;
private final int width;
private final int height;
private final long size;
private Optional<String> bucketId;
private Optional<String> caption;
public Media(@NonNull Uri uri, @NonNull String mimeType, long date, int width, int height, Optional<String> bucketId, Optional<String> caption) {
public Media(@NonNull Uri uri, @NonNull String mimeType, long date, int width, int height, long size, Optional<String> bucketId, Optional<String> caption) {
this.uri = uri;
this.mimeType = mimeType;
this.date = date;
this.width = width;
this.height = height;
this.size = size;
this.bucketId = bucketId;
this.caption = caption;
}
@@ -39,6 +41,7 @@ public class Media implements Parcelable {
date = in.readLong();
width = in.readInt();
height = in.readInt();
size = in.readLong();
bucketId = Optional.fromNullable(in.readString());
caption = Optional.fromNullable(in.readString());
}
@@ -63,6 +66,10 @@ public class Media implements Parcelable {
return height;
}
public long getSize() {
return size;
}
public Optional<String> getBucketId() {
return bucketId;
}
@@ -87,6 +94,7 @@ public class Media implements Parcelable {
dest.writeLong(date);
dest.writeInt(width);
dest.writeInt(height);
dest.writeLong(size);
dest.writeString(bucketId.orNull());
dest.writeString(caption.orNull());
}

View File

@@ -149,7 +149,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
@Override
public void onMediaChosen(@NonNull Media media) {
controller.onMediaSelected(bucketId, Collections.singleton(media));
viewModel.onSelectedMediaChanged(Collections.singletonList(media));
viewModel.onSelectedMediaChanged(requireContext(), Collections.singletonList(media));
}
@Override
@@ -165,7 +165,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
actionMode.setTitle(String.valueOf(selected.size()));
}
viewModel.onSelectedMediaChanged(selected);
viewModel.onSelectedMediaChanged(requireContext(), selected);
}
@Override
@@ -221,7 +221,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
if (menuItem.getItemId() == R.id.mediapicker_menu_confirm) {
List<Media> selected = new ArrayList<>(adapter.getSelected());
actionMode.finish();
viewModel.onSelectedMediaChanged(selected);
viewModel.onSelectedMediaChanged(requireContext(), selected);
controller.onMediaSelected(bucketId, selected);
return true;
}
@@ -232,7 +232,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
adapter.setSelected(Collections.emptySet());
viewModel.onSelectedMediaChanged(Collections.emptyList());
viewModel.onSelectedMediaChanged(requireContext(), Collections.emptyList());
if (Build.VERSION.SDK_INT >= 21) {
requireActivity().getWindow().setStatusBarColor(statusBarColor);

View File

@@ -7,20 +7,24 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.util.Pair;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -47,6 +51,19 @@ class MediaRepository {
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getMediaInBucket(context, bucketId)));
}
/**
* Given an existing list of {@link Media}, this will ensure that the media is populate with as
* much data as we have, like width/height.
*/
void getPopulatedMedia(@NonNull Context context, @NonNull List<Media> media, @NonNull Callback<List<Media>> callback) {
if (Stream.of(media).allMatch(this::isPopulated)) {
callback.onComplete(media);
return;
}
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getPopulatedMedia(context, media)));
}
@WorkerThread
private @NonNull List<MediaFolder> getFolders(@NonNull Context context) {
FolderResult imageFolders = getFolders(context, Images.Media.EXTERNAL_CONTENT_URI);
@@ -151,11 +168,11 @@ class MediaRepository {
String[] projection;
if (hasOrienation) {
projection = Build.VERSION.SDK_INT >= 16 ? new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT}
: new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION};
projection = Build.VERSION.SDK_INT >= 16 ? new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE}
: new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.SIZE};
} else {
projection = Build.VERSION.SDK_INT >= 16 ? new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT}
: new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN };
projection = Build.VERSION.SDK_INT >= 16 ? new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE}
: new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.SIZE};
}
if (Media.ALL_MEDIA_BUCKET_ID.equals(bucketId)) {
@@ -171,19 +188,36 @@ class MediaRepository {
int orientation = hasOrienation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
int width = 0;
int height = 0;
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
if (Build.VERSION.SDK_INT >= 16) {
width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)));
height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)));
}
media.add(new Media(uri, mimetype, dateTaken, width, height, Optional.of(bucketId), Optional.absent()));
media.add(new Media(uri, mimetype, dateTaken, width, height, size, Optional.of(bucketId), Optional.absent()));
}
}
return media;
}
@WorkerThread
private List<Media> getPopulatedMedia(@NonNull Context context, @NonNull List<Media> media) {
return Stream.of(media).map(m -> {
try {
if (isPopulated(m)) {
return m;
} else if (PartAuthority.isLocalUri(m.getUri())) {
return getLocallyPopulatedMedia(context, m);
} else {
return getContentResolverPopulatedMedia(context, m);
}
} catch (IOException e) {
return m;
}
}).toList();
}
@TargetApi(16)
@SuppressWarnings("SuspiciousNameCombination")
@@ -199,6 +233,59 @@ class MediaRepository {
else return Images.Media.WIDTH;
}
private boolean isPopulated(@NonNull Media media) {
return media.getWidth() > 0 && media.getHeight() > 0 && media.getSize() > 0;
}
private Media getLocallyPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException {
int width = media.getWidth();
int height = media.getHeight();
long size = media.getSize();
if (size <= 0) {
Optional<Long> optionalSize = Optional.fromNullable(PartAuthority.getAttachmentSize(context, media.getUri()));
size = optionalSize.isPresent() ? optionalSize.get() : 0;
}
if (size <= 0) {
size = MediaUtil.getMediaSize(context, media.getUri());
}
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, media.getMimeType(), media.getUri());
width = dimens.first;
height = dimens.second;
}
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, media.getBucketId(), media.getCaption());
}
private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException {
int width = media.getWidth();
int height = media.getHeight();
long size = media.getSize();
if (size <= 0) {
try (Cursor cursor = context.getContentResolver().query(media.getUri(), null, null, null, null)) {
if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.SIZE) >= 0) {
size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
}
}
}
if (size <= 0) {
size = MediaUtil.getMediaSize(context, media.getUri());
}
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, media.getMimeType(), media.getUri());
width = dimens.first;
height = dimens.second;
}
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, media.getBucketId(), media.getCaption());
}
private static class FolderResult {
private final String cameraBucketId;
private final Uri thumbnail;

View File

@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.scribbles.ScribbleFragment;
import org.thoughtcrime.securesms.util.DynamicLanguage;
@@ -105,6 +106,9 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
body = getIntent().getStringExtra(KEY_BODY);
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
viewModel.setMediaConstraints(transport.isSms() ? MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1))
: MediaConstraints.getPushMediaConstraints());
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
if (!Util.isEmpty(media)) {
@@ -211,7 +215,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
}
private void navigateToMediaSend(List<Media> media, String body, TransportOption transport) {
viewModel.setInitialSelectedMedia(media);
viewModel.setInitialSelectedMedia(this, media);
MediaSendFragment sendFragment = MediaSendFragment.newInstance(body, transport, dynamicLanguage.getCurrentLocale());
getSupportFragmentManager().beginTransaction()

View File

@@ -27,6 +27,7 @@ import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
@@ -334,6 +335,12 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
addButton.setOnClickListener(v -> controller.onAddMediaClicked(bucketId.get()));
}
});
viewModel.getError().observe(this, error -> {
if (error == MediaSendViewModel.Error.ITEM_TOO_LARGE) {
Toast.makeText(requireContext(), R.string.MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit, Toast.LENGTH_LONG).show();
}
});
}
private EmojiEditText getActiveInputField() {
@@ -428,7 +435,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
Uri uri = PersistentBlobProvider.getInstance(context).create(context, baos.toByteArray(), MediaUtil.IMAGE_JPEG, null);
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), media.getBucketId(), media.getCaption());
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), baos.size(), media.getBucketId(), media.getCaption());
updatedMedia.add(updated);
renderTimer.split("item");

View File

@@ -11,7 +11,9 @@ import android.text.TextUtils;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -31,8 +33,11 @@ class MediaSendViewModel extends ViewModel {
private final MutableLiveData<Integer> position;
private final MutableLiveData<Optional<String>> bucketId;
private final MutableLiveData<List<MediaFolder>> folders;
private final SingleLiveEvent<Error> error;
private final Map<Uri, Object> savedDrawState;
private MediaConstraints mediaConstraints;
private MediaSendViewModel(@NonNull MediaRepository repository) {
this.repository = repository;
this.selectedMedia = new MutableLiveData<>();
@@ -40,21 +45,37 @@ class MediaSendViewModel extends ViewModel {
this.position = new MutableLiveData<>();
this.bucketId = new MutableLiveData<>();
this.folders = new MutableLiveData<>();
this.error = new SingleLiveEvent<>();
this.savedDrawState = new HashMap<>();
position.setValue(-1);
}
void setInitialSelectedMedia(@NonNull List<Media> newMedia) {
List<Media> filteredMedia = getFilteredMedia(newMedia);
boolean allBucketsPopulated = Stream.of(filteredMedia).reduce(true, (populated, m) -> populated && m.getBucketId().isPresent());
selectedMedia.setValue(filteredMedia);
bucketId.setValue(allBucketsPopulated ? computeBucketId(filteredMedia) : Optional.absent());
void setMediaConstraints(@NonNull MediaConstraints mediaConstraints) {
this.mediaConstraints = mediaConstraints;
}
void onSelectedMediaChanged(@NonNull List<Media> newMedia) {
List<Media> filteredMedia = getFilteredMedia(newMedia);
void setInitialSelectedMedia(@NonNull Context context, @NonNull List<Media> newMedia) {
repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
List<Media> filteredMedia = getFilteredMedia(context, populatedMedia, mediaConstraints);
if (filteredMedia.size() != newMedia.size()) {
error.postValue(Error.ITEM_TOO_LARGE);
}
boolean allBucketsPopulated = Stream.of(filteredMedia).reduce(true, (populated, m) -> populated && m.getBucketId().isPresent());
selectedMedia.postValue(filteredMedia);
bucketId.postValue(allBucketsPopulated ? computeBucketId(filteredMedia) : Optional.absent());
});
}
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
List<Media> filteredMedia = getFilteredMedia(context, newMedia, mediaConstraints);
if (filteredMedia.size() != newMedia.size()) {
error.setValue(Error.ITEM_TOO_LARGE);
}
selectedMedia.setValue(filteredMedia);
position.setValue(filteredMedia.isEmpty() ? -1 : 0);
@@ -111,6 +132,10 @@ class MediaSendViewModel extends ViewModel {
return bucketId;
}
LiveData<Error> getError() {
return error;
}
private Optional<String> computeBucketId(@NonNull List<Media> media) {
if (media.isEmpty() || !media.get(0).getBucketId().isPresent()) return Optional.absent();
@@ -124,13 +149,22 @@ class MediaSendViewModel extends ViewModel {
return Optional.of(candidate);
}
private @NonNull List<Media> getFilteredMedia(@NonNull List<Media> media) {
private @NonNull List<Media> getFilteredMedia(@NonNull Context context, @NonNull List<Media> media, @NonNull MediaConstraints mediaConstraints) {
return Stream.of(media).filter(m -> MediaUtil.isGif(m.getMimeType()) ||
MediaUtil.isImageType(m.getMimeType()) ||
MediaUtil.isVideoType(m.getMimeType())).toList();
MediaUtil.isVideoType(m.getMimeType()))
.filter(m -> {
return (MediaUtil.isImageType(m.getMimeType()) && !MediaUtil.isGif(m.getMimeType())) ||
(MediaUtil.isGif(m.getMimeType()) && m.getSize() < mediaConstraints.getGifMaxSize(context)) ||
(MediaUtil.isVideoType(m.getMimeType()) && m.getSize() < mediaConstraints.getVideoMaxSize(context));
}).toList();
}
enum Error {
ITEM_TOO_LARGE
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final MediaRepository repository;

View File

@@ -39,7 +39,7 @@ import android.support.v4.app.NotificationManagerCompat;
import android.text.TextUtils;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.contactshare.Contact;
@@ -308,7 +308,6 @@ public class MessageNotifier {
builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(),
notifications.get(0).getText(), notifications.get(0).getSlideDeck());
builder.setContentIntent(notifications.get(0).getPendingIntent(context));
builder.setGroup(NOTIFICATION_GROUP);
builder.setDeleteIntent(notificationState.getDeleteIntent(context));
builder.setOnlyAlertOnce(!signal);
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
@@ -336,8 +335,9 @@ public class MessageNotifier {
notifications.get(0).getText());
}
if (!bundled) {
builder.setGroupSummary(true);
if (bundled) {
builder.setGroup(NOTIFICATION_GROUP);
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
}
Notification notification = builder.build();

View File

@@ -8,7 +8,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.TaskStackBuilder;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;

View File

@@ -7,8 +7,8 @@ import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.ConversationPopupActivity;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.conversation.ConversationPopupActivity;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;

View File

@@ -73,6 +73,7 @@ public class Recipient implements RecipientModifiedListener {
private @Nullable String name;
private @Nullable String customLabel;
private boolean resolving;
private boolean isLocalNumber;
private @Nullable Uri systemContactPhoto;
private @Nullable Long groupAvatarId;
@@ -119,15 +120,16 @@ public class Recipient implements RecipientModifiedListener {
@NonNull Optional<RecipientDetails> details,
@NonNull ListenableFutureTask<RecipientDetails> future)
{
this.address = address;
this.color = null;
this.resolving = true;
this.address = address;
this.color = null;
this.resolving = true;
if (stale != null) {
this.name = stale.name;
this.contactUri = stale.contactUri;
this.systemContactPhoto = stale.systemContactPhoto;
this.groupAvatarId = stale.groupAvatarId;
this.isLocalNumber = stale.isLocalNumber;
this.color = stale.color;
this.customLabel = stale.customLabel;
this.messageRingtone = stale.messageRingtone;
@@ -155,6 +157,7 @@ public class Recipient implements RecipientModifiedListener {
this.name = details.get().name;
this.systemContactPhoto = details.get().systemContactPhoto;
this.groupAvatarId = details.get().groupAvatarId;
this.isLocalNumber = details.get().isLocalNumber;
this.color = details.get().color;
this.messageRingtone = details.get().messageRingtone;
this.callRingtone = details.get().callRingtone;
@@ -186,6 +189,7 @@ public class Recipient implements RecipientModifiedListener {
Recipient.this.contactUri = result.contactUri;
Recipient.this.systemContactPhoto = result.systemContactPhoto;
Recipient.this.groupAvatarId = result.groupAvatarId;
Recipient.this.isLocalNumber = result.isLocalNumber;
Recipient.this.color = result.color;
Recipient.this.customLabel = result.customLabel;
Recipient.this.messageRingtone = result.messageRingtone;
@@ -234,6 +238,7 @@ public class Recipient implements RecipientModifiedListener {
this.name = details.name;
this.systemContactPhoto = details.systemContactPhoto;
this.groupAvatarId = details.groupAvatarId;
this.isLocalNumber = details.isLocalNumber;
this.color = details.color;
this.customLabel = details.customLabel;
this.messageRingtone = details.messageRingtone;
@@ -257,6 +262,10 @@ public class Recipient implements RecipientModifiedListener {
this.resolving = false;
}
public boolean isLocalNumber() {
return isLocalNumber;
}
public synchronized @Nullable Uri getContactUri() {
return this.contactUri;
}
@@ -434,6 +443,7 @@ public class Recipient implements RecipientModifiedListener {
}
public synchronized @NonNull FallbackContactPhoto getFallbackContactPhoto() {
if (isLocalNumber) return new ResourceContactPhoto(R.drawable.ic_note_to_self);
if (isResolving()) return new TransparentContactPhoto();
else if (isGroupRecipient()) return new ResourceContactPhoto(R.drawable.ic_group_white_24dp, R.drawable.ic_group_large);
else if (!TextUtils.isEmpty(name)) return new GeneratedContactPhoto(name, R.drawable.ic_profile_default);
@@ -441,7 +451,8 @@ public class Recipient implements RecipientModifiedListener {
}
public synchronized @Nullable ContactPhoto getContactPhoto() {
if (isGroupRecipient() && groupAvatarId != null) return new GroupRecordContactPhoto(address, groupAvatarId);
if (isLocalNumber) return null;
else if (isGroupRecipient() && groupAvatarId != null) return new GroupRecordContactPhoto(address, groupAvatarId);
else if (systemContactPhoto != null) return new SystemContactPhoto(address, systemContactPhoto, 0);
else if (profileAvatar != null) return new ProfileContactPhoto(address, profileAvatar);
else return null;

View File

@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessM
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.SoftHashMap;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -52,7 +53,7 @@ class RecipientProvider {
private static final ExecutorService asyncRecipientResolver = Util.newSingleThreadedLifoExecutor();
private static final Map<String, RecipientDetails> STATIC_DETAILS = new HashMap<String, RecipientDetails>() {{
put("262966", new RecipientDetails("Amazon", null, false, null, null));
put("262966", new RecipientDetails("Amazon", null, false, false, null, null));
}};
@NonNull Recipient getRecipient(@NonNull Context context, @NonNull Address address, @NonNull Optional<RecipientSettings> settings, @NonNull Optional<GroupRecord> groupRecord, boolean asynchronous) {
@@ -85,7 +86,8 @@ class RecipientProvider {
if (address.isGroup() && settings.isPresent() && groupRecord.isPresent()) {
return Optional.of(getGroupRecipientDetails(context, address, groupRecord, settings, true));
} else if (!address.isGroup() && settings.isPresent()) {
return Optional.of(new RecipientDetails(null, null, !TextUtils.isEmpty(settings.get().getSystemDisplayName()), settings.get(), null));
boolean isLocalNumber = address.serialize().equals(TextSecurePreferences.getLocalNumber(context));
return Optional.of(new RecipientDetails(null, null, !TextUtils.isEmpty(settings.get().getSystemDisplayName()), isLocalNumber, settings.get(), null));
}
return Optional.absent();
@@ -114,7 +116,8 @@ class RecipientProvider {
return STATIC_DETAILS.get(address.serialize());
} else {
boolean systemContact = settings.isPresent() && !TextUtils.isEmpty(settings.get().getSystemDisplayName());
return new RecipientDetails(null, null, systemContact, settings.orNull(), null);
boolean isLocalNumber = address.serialize().equals(TextSecurePreferences.getLocalNumber(context));
return new RecipientDetails(null, null, systemContact, isLocalNumber, settings.orNull(), null);
}
}
@@ -146,10 +149,10 @@ class RecipientProvider {
avatarId = groupRecord.get().getAvatarId();
}
return new RecipientDetails(title, avatarId, false, settings.orNull(), members);
return new RecipientDetails(title, avatarId, false, false, settings.orNull(), members);
}
return new RecipientDetails(context.getString(R.string.RecipientProvider_unnamed_group), null, false, settings.orNull(), null);
return new RecipientDetails(context.getString(R.string.RecipientProvider_unnamed_group), null, false, false, settings.orNull(), null);
}
static class RecipientDetails {
@@ -175,11 +178,12 @@ class RecipientProvider {
@Nullable final String profileAvatar;
final boolean profileSharing;
final boolean systemContact;
final boolean isLocalNumber;
@Nullable final String notificationChannel;
@NonNull final UnidentifiedAccessMode unidentifiedAccessMode;
RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId,
boolean systemContact, @Nullable RecipientSettings settings,
boolean systemContact, boolean isLocalNumber, @Nullable RecipientSettings settings,
@Nullable List<Recipient> participants)
{
this.groupAvatarId = groupAvatarId;
@@ -203,6 +207,7 @@ class RecipientProvider {
this.profileAvatar = settings != null ? settings.getProfileAvatar() : null;
this.profileSharing = settings != null && settings.isProfileSharing();
this.systemContact = systemContact;
this.isLocalNumber = isLocalNumber;
this.notificationChannel = settings != null ? settings.getNotificationChannel() : null;
this.unidentifiedAccessMode = settings != null ? settings.getUnidentifiedAccessMode() : UnidentifiedAccessMode.DISABLED;

View File

@@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.registration;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.R;
public class CaptchaActivity extends BaseActionBarActivity {
public static final String KEY_TOKEN = "token";
public static final String KEY_IS_SMS = "is_sms";
private static final String SIGNAL_SCHEME = "signalcaptcha://";
public static Intent getIntent(@NonNull Context context, boolean isSms) {
Intent intent = new Intent(context, CaptchaActivity.class);
intent.putExtra(KEY_IS_SMS, isSms);
return intent;
}
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.captcha_activity);
WebView webView = findViewById(R.id.registration_captcha_web_view);
webView.getSettings().setJavaScriptEnabled(true);
webView.clearCache(true);
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url != null && url.startsWith(SIGNAL_SCHEME)) {
handleToken(url.substring(SIGNAL_SCHEME.length()));
return true;
}
return false;
}
});
webView.loadUrl("https://signalcaptchas.org/registration/generate.html");
}
public void handleToken(String token) {
if (!TextUtils.isEmpty(token)) {
Intent result = new Intent();
result.putExtra(KEY_TOKEN, token);
result.putExtra(KEY_IS_SMS, getIntent().getBooleanExtra(KEY_IS_SMS, true));
setResult(RESULT_OK, result);
} else {
setResult(RESULT_CANCELED);
}
finish();
}
}

View File

@@ -17,7 +17,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
@@ -121,16 +121,6 @@ public class SearchFragment extends Fragment implements SearchListAdapter.EventL
});
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (listDecoration != null) {
listDecoration.invalidateLayouts();
}
}
@Override
public void onConversationClicked(@NonNull ThreadRecord threadRecord) {
ConversationListActivity conversationList = (ConversationListActivity) getActivity();
@@ -172,7 +162,7 @@ public class SearchFragment extends Fragment implements SearchListAdapter.EventL
ConversationListActivity conversationList = (ConversationListActivity) getActivity();
if (conversationList != null) {
conversationList.openConversation(message.threadId,
message.recipient,
message.conversationRecipient,
ThreadDatabase.DistributionTypes.DEFAULT,
-1,
startingPosition);

View File

@@ -19,11 +19,14 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.search.model.SearchResult;
import org.thoughtcrime.securesms.util.Stopwatch;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -32,7 +35,9 @@ import java.util.concurrent.Executor;
/**
* Manages data retrieval for search.
*/
class SearchRepository {
public class SearchRepository {
private static final String TAG = SearchRepository.class.getSimpleName();
private static final Set<Character> BANNED_CHARACTERS = new HashSet<>();
static {
@@ -58,12 +63,12 @@ class SearchRepository {
private final ContactAccessor contactAccessor;
private final Executor executor;
SearchRepository(@NonNull Context context,
@NonNull SearchDatabase searchDatabase,
@NonNull ContactsDatabase contactsDatabase,
@NonNull ThreadDatabase threadDatabase,
@NonNull ContactAccessor contactAccessor,
@NonNull Executor executor)
public SearchRepository(@NonNull Context context,
@NonNull SearchDatabase searchDatabase,
@NonNull ContactsDatabase contactsDatabase,
@NonNull ThreadDatabase threadDatabase,
@NonNull ContactAccessor contactAccessor,
@NonNull Executor executor)
{
this.context = context.getApplicationContext();
this.searchDatabase = searchDatabase;
@@ -73,22 +78,48 @@ class SearchRepository {
this.executor = executor;
}
void query(@NonNull String query, @NonNull Callback callback) {
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
if (TextUtils.isEmpty(query)) {
callback.onResult(SearchResult.EMPTY);
return;
}
executor.execute(() -> {
String cleanQuery = sanitizeQuery(query);
CursorList<Recipient> contacts = queryContacts(cleanQuery);
CursorList<ThreadRecord> conversations = queryConversations(cleanQuery);
CursorList<MessageResult> messages = queryMessages(cleanQuery);
Stopwatch timer = new Stopwatch("FtsQuery");
String cleanQuery = sanitizeQuery(query);
timer.split("clean");
CursorList<Recipient> contacts = queryContacts(cleanQuery);
timer.split("contacts");
CursorList<ThreadRecord> conversations = queryConversations(cleanQuery);
timer.split("conversations");
CursorList<MessageResult> messages = queryMessages(cleanQuery);
timer.split("messages");
timer.stop(TAG);
callback.onResult(new SearchResult(cleanQuery, contacts, conversations, messages));
});
}
public void query(@NonNull String query, long threadId, @NonNull Callback<CursorList<MessageResult>> callback) {
if (TextUtils.isEmpty(query)) {
callback.onResult(CursorList.emptyList());
return;
}
executor.execute(() -> {
long startTime = System.currentTimeMillis();
CursorList<MessageResult> messages = queryMessages(sanitizeQuery(query), threadId);
Log.d(TAG, "[ConversationQuery] " + (System.currentTimeMillis() - startTime) + " ms");
callback.onResult(messages);
});
}
private CursorList<Recipient> queryContacts(String query) {
if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
return CursorList.emptyList();
@@ -116,6 +147,12 @@ class SearchRepository {
: CursorList.emptyList();
}
private CursorList<MessageResult> queryMessages(@NonNull String query, long threadId) {
Cursor messages = searchDatabase.queryMessages(query, threadId);
return messages != null ? new CursorList<>(messages, new MessageModelBuilder(context))
: CursorList.emptyList();
}
/**
* Unfortunately {@link DatabaseUtils#sqlEscapeString(String)} is not sufficient for our purposes.
* MATCH queries have a separate format of their own that disallow most "special" characters.
@@ -177,17 +214,19 @@ class SearchRepository {
@Override
public MessageResult build(@NonNull Cursor cursor) {
Address address = Address.fromSerialized(cursor.getString(0));
Recipient recipient = Recipient.from(context, address, false);
String body = cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.SNIPPET));
long receivedMs = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.THREAD_ID));
Address conversationAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndex(SearchDatabase.CONVERSATION_ADDRESS)));
Address messageAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.MESSAGE_ADDRESS)));
Recipient conversationRecipient = Recipient.from(context, conversationAddress, false);
Recipient messageRecipient = Recipient.from(context, messageAddress, false);
String body = cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.SNIPPET));
long receivedMs = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.THREAD_ID));
return new MessageResult(recipient, body, threadId, receivedMs);
return new MessageResult(conversationRecipient, messageRecipient, body, threadId, receivedMs);
}
}
public interface Callback {
void onResult(@NonNull SearchResult result);
public interface Callback<E> {
void onResult(@NonNull E result);
}
}

View File

@@ -11,6 +11,7 @@ import android.text.TextUtils;
import org.thoughtcrime.securesms.search.model.SearchResult;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Util;
/**
* A {@link ViewModel} for handling all the business logic and interactions that take place inside
@@ -28,7 +29,7 @@ class SearchViewModel extends ViewModel {
private String lastQuery;
SearchViewModel(@NonNull SearchRepository searchRepository) {
private SearchViewModel(@NonNull SearchRepository searchRepository) {
this.searchResult = new ObservingLiveData();
this.searchRepository = searchRepository;
this.debouncer = new Debouncer(500);
@@ -49,7 +50,15 @@ class SearchViewModel extends ViewModel {
void updateQuery(String query) {
lastQuery = query;
debouncer.publish(() -> searchRepository.query(query, searchResult::postValue));
debouncer.publish(() -> searchRepository.query(query, result -> {
Util.runOnMain(() -> {
if (query.equals(lastQuery)) {
searchResult.setValue(result);
} else {
result.close();
}
});
}));
}
@NonNull

View File

@@ -9,19 +9,22 @@ import org.thoughtcrime.securesms.recipients.Recipient;
*/
public class MessageResult {
public final Recipient recipient;
public final Recipient conversationRecipient;
public final Recipient messageRecipient;
public final String bodySnippet;
public final long threadId;
public final long receivedTimestampMs;
public MessageResult(@NonNull Recipient recipient,
public MessageResult(@NonNull Recipient conversationRecipient,
@NonNull Recipient messageRecipient,
@NonNull String bodySnippet,
long threadId,
long receivedTimestampMs)
{
this.recipient = recipient;
this.bodySnippet = bodySnippet;
this.threadId = threadId;
this.receivedTimestampMs = receivedTimestampMs;
this.conversationRecipient = conversationRecipient;
this.messageRecipient = messageRecipient;
this.bodySnippet = bodySnippet;
this.threadId = threadId;
this.receivedTimestampMs = receivedTimestampMs;
}
}

View File

@@ -18,8 +18,12 @@ package org.thoughtcrime.securesms.sms;
import android.content.Context;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment;
@@ -31,7 +35,6 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
@@ -44,7 +47,6 @@ import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
@@ -75,7 +77,7 @@ public class MessageSender {
long messageId = database.insertMessageOutbox(allocatedThreadId, message, forceSms, System.currentTimeMillis(), insertListener);
sendTextMessage(context, recipient, forceSms, keyExchange, messageId, message.getExpiresIn());
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
return allocatedThreadId;
}
@@ -116,28 +118,23 @@ public class MessageSender {
}
public static void resend(Context context, MessageRecord messageRecord) {
try {
long messageId = messageRecord.getId();
boolean forceSms = messageRecord.isForcedSms();
boolean keyExchange = messageRecord.isKeyExchange();
long expiresIn = messageRecord.getExpiresIn();
Recipient recipient = messageRecord.getRecipient();
long messageId = messageRecord.getId();
boolean forceSms = messageRecord.isForcedSms();
boolean keyExchange = messageRecord.isKeyExchange();
long expiresIn = messageRecord.getExpiresIn();
Recipient recipient = messageRecord.getRecipient();
if (messageRecord.isMms()) {
sendMediaMessage(context, recipient, forceSms, messageId, expiresIn);
} else {
sendTextMessage(context, recipient, forceSms, keyExchange, messageId, expiresIn);
}
} catch (MmsException e) {
Log.w(TAG, e);
if (messageRecord.isMms()) {
sendMediaMessage(context, recipient, forceSms, messageId, expiresIn);
} else {
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
}
}
private static void sendMediaMessage(Context context, Recipient recipient, boolean forceSms, long messageId, long expiresIn)
throws MmsException
{
if (!forceSms && isSelfSend(context, recipient)) {
sendMediaSelf(context, messageId, expiresIn);
if (isLocalSelfSend(context, recipient, forceSms)) {
sendLocalMediaSelf(context, messageId);
} else if (isGroupPushSend(recipient)) {
sendGroupPush(context, recipient, messageId, null);
} else if (!forceSms && isPushMediaSend(context, recipient)) {
@@ -149,10 +146,10 @@ public class MessageSender {
private static void sendTextMessage(Context context, Recipient recipient,
boolean forceSms, boolean keyExchange,
long messageId, long expiresIn)
long messageId)
{
if (!forceSms && isSelfSend(context, recipient)) {
sendTextSelf(context, messageId, expiresIn);
if (isLocalSelfSend(context, recipient, forceSms)) {
sendLocalTextSelf(context, messageId);
} else if (!forceSms && isPushTextSend(context, recipient, keyExchange)) {
sendTextPush(context, recipient, messageId);
} else {
@@ -160,38 +157,6 @@ public class MessageSender {
}
}
private static void sendTextSelf(Context context, long messageId, long expiresIn) {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
database.markAsSent(messageId, true);
Pair<Long, Long> messageAndThreadId = database.copyMessageInbox(messageId);
database.markAsPush(messageAndThreadId.first);
if (expiresIn > 0) {
ExpiringMessageManager expiringMessageManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
database.markExpireStarted(messageId);
expiringMessageManager.scheduleDeletion(messageId, false, expiresIn);
}
}
private static void sendMediaSelf(Context context, long messageId, long expiresIn)
throws MmsException
{
ExpiringMessageManager expiringMessageManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.markAsSent(messageId, true);
database.copyMessageInbox(messageId);
markAttachmentsAsUploaded(messageId, database, DatabaseFactory.getAttachmentDatabase(context));
if (expiresIn > 0) {
database.markExpireStarted(messageId);
expiringMessageManager.scheduleDeletion(messageId, true, expiresIn);
}
}
private static void sendTextPush(Context context, Recipient recipient, long messageId) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new PushTextSendJob(context, messageId, recipient.getAddress()));
@@ -246,18 +211,6 @@ public class MessageSender {
!recipient.getAddress().isMmsGroup();
}
private static boolean isSelfSend(Context context, Recipient recipient) {
if (!TextSecurePreferences.isPushRegistered(context)) {
return false;
}
if (recipient.isGroupRecipient()) {
return false;
}
return Util.isOwnNumber(context, recipient.getAddress());
}
private static boolean isPushDestination(Context context, Recipient destination) {
if (destination.resolve().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
return true;
@@ -282,15 +235,61 @@ public class MessageSender {
}
}
private static void markAttachmentsAsUploaded(long mmsId, @NonNull MmsDatabase mmsDatabase, @NonNull AttachmentDatabase attachmentDatabase) {
try (MmsDatabase.Reader reader = mmsDatabase.readerFor(mmsDatabase.getMessage(mmsId))) {
MessageRecord message = reader.getNext();
private static boolean isLocalSelfSend(@NonNull Context context, @NonNull Recipient recipient, boolean forceSms) {
return recipient.isLocalNumber() &&
!forceSms &&
TextSecurePreferences.isPushRegistered(context) &&
!TextSecurePreferences.isMultiDevice(context);
}
if (message != null && message.isMms()) {
for (Attachment attachment : ((MmsMessageRecord) message).getSlideDeck().asAttachments()) {
attachmentDatabase.markAttachmentUploaded(mmsId, attachment);
}
private static void sendLocalMediaSelf(Context context, long messageId) {
try {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
OutgoingMediaMessage message = mmsDatabase.getOutgoingMessage(messageId);
SyncMessageId syncId = new SyncMessageId(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), message.getSentTimeMillis());
for (Attachment attachment : message.getAttachments()) {
attachmentDatabase.markAttachmentUploaded(messageId, attachment);
}
mmsDatabase.markAsSent(messageId, true);
mmsDatabase.markUnidentified(messageId, true);
mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis());
mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis());
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
mmsDatabase.markExpireStarted(messageId);
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
}
} catch (NoSuchMessageException | MmsException e) {
Log.w("Failed to update self-sent message.", e);
}
}
private static void sendLocalTextSelf(Context context, long messageId) {
try {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
SmsMessageRecord message = smsDatabase.getMessage(messageId);
SyncMessageId syncId = new SyncMessageId(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), message.getDateSent());
smsDatabase.markAsSent(messageId, true);
smsDatabase.markUnidentified(messageId, true);
mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis());
mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis());
if (message.getExpiresIn() > 0) {
smsDatabase.markExpireStarted(messageId);
expirationManager.scheduleDeletion(message.getId(), message.isMms(), message.getExpiresIn());
}
} catch (NoSuchMessageException e) {
Log.w("Failed to update self-sent message.", e);
}
}
}

View File

@@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.util;
import android.arch.lifecycle.MutableLiveData;
import java.io.Closeable;
/**
* Implementation of {@link android.arch.lifecycle.LiveData} that will handle closing the contained
* {@link Closeable} when the value changes.
*/
public class CloseableLiveData<E extends Closeable> extends MutableLiveData<E> {
@Override
public void setValue(E value) {
setValue(value, true);
}
public void setValue(E value, boolean closePrevious) {
E previous = getValue();
if (previous != null && closePrevious) {
Util.close(previous);
}
super.setValue(value);
}
public void close() {
E value = getValue();
if (value != null) {
Util.close(value);
}
}
}

View File

@@ -13,10 +13,9 @@ import android.support.v4.app.TaskStackBuilder;
import android.text.TextUtils;
import android.widget.Toast;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.permissions.Permissions;

View File

@@ -12,6 +12,7 @@ import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.thoughtcrime.securesms.logging.Log;
@@ -53,6 +54,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashSet;
@@ -218,7 +220,7 @@ public class DirectoryHelper {
}
}
} catch (RemoteException | OperationApplicationException e) {
Log.w(TAG, e);
Log.w(TAG, "Failed to update contacts.", e);
}
}
}
@@ -402,19 +404,19 @@ public class DirectoryHelper {
}
} catch (InterruptedException e) {
Log.w(TAG, "Contact discovery batch was interrupted.", e);
accountManager.reportContactDiscoveryServiceUnexpectedError();
accountManager.reportContactDiscoveryServiceUnexpectedError(buildErrorReason(e));
return Optional.absent();
} catch (ExecutionException e) {
if (isAttestationError(e.getCause())) {
Log.w(TAG, "Failed during attestation.", e);
accountManager.reportContactDiscoveryServiceAttestationError();
accountManager.reportContactDiscoveryServiceAttestationError(buildErrorReason(e.getCause()));
return Optional.absent();
} else if (e.getCause() instanceof PushNetworkException) {
Log.w(TAG, "Failed due to poor network.", e);
return Optional.absent();
} else {
Log.w(TAG, "Failed for an unknown reason.", e);
accountManager.reportContactDiscoveryServiceUnexpectedError();
accountManager.reportContactDiscoveryServiceUnexpectedError(buildErrorReason(e.getCause()));
return Optional.absent();
}
}
@@ -441,6 +443,29 @@ public class DirectoryHelper {
return keyStore;
}
private static String buildErrorReason(@Nullable Throwable t) {
if (t == null) {
return "null";
}
String rawString = android.util.Log.getStackTraceString(t);
List<String> lines = Arrays.asList(rawString.split("\\n"));
String errorString;
if (lines.size() > 1) {
errorString = t.getClass().getName() + "\n" + Util.join(lines.subList(1, lines.size()), "\n");
} else {
errorString = t.getClass().getName();
}
if (errorString.length() > 1000) {
return errorString.substring(0, 1000);
} else {
return errorString;
}
}
private static class DirectoryResult {
private final Set<String> numbers;

View File

@@ -0,0 +1,96 @@
package org.thoughtcrime.securesms.util;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import com.annimon.stream.Stream;
import org.whispersystems.libsignal.util.Pair;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
public class SearchUtil {
public static Spannable getHighlightedSpan(@NonNull Locale locale,
@NonNull StyleFactory styleFactory,
@Nullable String text,
@Nullable String highlight)
{
if (TextUtils.isEmpty(text)) {
return new SpannableString("");
}
text = text.replaceAll("\n", " ");
return getHighlightedSpan(locale, styleFactory, new SpannableString(text), highlight);
}
public static Spannable getHighlightedSpan(@NonNull Locale locale,
@NonNull StyleFactory styleFactory,
@Nullable Spannable text,
@Nullable String highlight)
{
if (TextUtils.isEmpty(text)) {
return new SpannableString("");
}
if (TextUtils.isEmpty(highlight)) {
return text;
}
List<Pair<Integer, Integer>> ranges = getHighlightRanges(locale, text.toString(), highlight);
SpannableString spanned = new SpannableString(text);
for (Pair<Integer, Integer> range : ranges) {
spanned.setSpan(styleFactory.create(), range.first(), range.second(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
return spanned;
}
static List<Pair<Integer, Integer>> getHighlightRanges(@NonNull Locale locale,
@NonNull String text,
@NonNull String highlight)
{
String normalizedText = text.toLowerCase(locale);
String normalizedHighlight = highlight.toLowerCase(locale);
List<String> highlightTokens = Stream.of(normalizedHighlight.split("\\s")).filter(s -> s.trim().length() > 0).toList();
List<String> textTokens = Stream.of(normalizedText.split("\\s")).filter(s -> s.trim().length() > 0).toList();
List<Pair<Integer, Integer>> ranges = new LinkedList<>();
int textListIndex = 0;
int textCharIndex = 0;
for (String highlightToken : highlightTokens) {
for (int i = textListIndex; i < textTokens.size(); i++) {
if (textTokens.get(i).startsWith(highlightToken)) {
textListIndex = i + 1;
ranges.add(new Pair<>(textCharIndex, textCharIndex + highlightToken.length()));
textCharIndex += textTokens.get(i).length() + 1;
break;
}
textCharIndex += textTokens.get(i).length() + 1;
}
}
if (ranges.size() != highlightTokens.size()) {
return Collections.emptyList();
}
return ranges;
}
public interface StyleFactory {
CharacterStyle create();
}
}

View File

@@ -76,30 +76,30 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
protected ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter adapter, int position) {
final long key = adapter.getHeaderId(position);
if (headerCache.containsKey(key)) {
return headerCache.get(key);
} else {
final ViewHolder holder = adapter.onCreateHeaderViewHolder(parent);
final View header = holder.itemView;
ViewHolder headerHolder = headerCache.get(key);
if (headerHolder == null) {
headerHolder = adapter.onCreateHeaderViewHolder(parent);
//noinspection unchecked
adapter.onBindHeaderViewHolder(holder, position);
adapter.onBindHeaderViewHolder(headerHolder, position);
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width);
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height);
header.measure(childWidth, childHeight);
header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
headerCache.put(key, holder);
return holder;
headerCache.put(key, headerHolder);
}
final View header = headerHolder.itemView;
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width);
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height);
header.measure(childWidth, childHeight);
header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
return headerHolder;
}
/**
@@ -180,10 +180,6 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
((LinearLayoutManager)parent.getLayoutManager()).getReverseLayout();
}
public void invalidateLayouts() {
headerCache.clear();
}
/**
* The adapter to assist the {@link StickyHeaderDecoration} in creating and binding the header views.
*

View File

@@ -138,10 +138,11 @@ public class TextSecurePreferences {
private static final String ACTIVE_SIGNED_PRE_KEY_ID = "pref_active_signed_pre_key_id";
private static final String NEXT_SIGNED_PRE_KEY_ID = "pref_next_signed_pre_key_id";
public static final String BACKUP_ENABLED = "pref_backup_enabled";
private static final String BACKUP_PASSPHRASE = "pref_backup_passphrase";
private static final String BACKUP_TIME = "pref_backup_next_time";
public static final String BACKUP_NOW = "pref_backup_create";
public static final String BACKUP_ENABLED = "pref_backup_enabled";
private static final String BACKUP_PASSPHRASE = "pref_backup_passphrase";
private static final String ENCRYPTED_BACKUP_PASSPHRASE = "pref_encrypted_backup_passphrase";
private static final String BACKUP_TIME = "pref_backup_next_time";
public static final String BACKUP_NOW = "pref_backup_create";
public static final String SCREEN_LOCK = "pref_android_screen_lock";
public static final String SCREEN_LOCK_TIMEOUT = "pref_android_screen_lock_timeout";
@@ -231,6 +232,14 @@ public class TextSecurePreferences {
return getStringPreference(context, BACKUP_PASSPHRASE, null);
}
public static void setEncryptedBackupPassphrase(@NonNull Context context, @Nullable String encryptedPassphrase) {
setStringPreference(context, ENCRYPTED_BACKUP_PASSPHRASE, encryptedPassphrase);
}
public static @Nullable String getEncryptedBackupPassphrase(@NonNull Context context) {
return getStringPreference(context, ENCRYPTED_BACKUP_PASSPHRASE, null);
}
public static void setBackupEnabled(@NonNull Context context, boolean value) {
setBooleanPreference(context, BACKUP_ENABLED, value);
}

View File

@@ -63,6 +63,7 @@ import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -77,7 +78,7 @@ import java.util.concurrent.TimeUnit;
public class Util {
private static final String TAG = Util.class.getSimpleName();
public static Handler handler = new Handler(Looper.getMainLooper());
private static volatile Handler handler;
public static <T> List<T> asList(T... elements) {
List<T> result = new LinkedList<>();
@@ -141,6 +142,17 @@ public class Util {
return map.containsKey(key) ? map.get(key) : defaultValue;
}
public static <E> List<List<E>> chunk(@NonNull List<E> list, int chunkSize) {
List<List<E>> chunks = new ArrayList<>(list.size() / chunkSize);
for (int i = 0; i < list.size(); i += chunkSize) {
List<E> chunk = list.subList(i, Math.min(list.size(), i + chunkSize));
chunks.add(chunk);
}
return chunks;
}
public static CharSequence getBoldedString(String value) {
SpannableString spanned = new SpannableString(value);
spanned.setSpan(new StyleSpan(Typeface.BOLD), 0,
@@ -394,20 +406,20 @@ public class Util {
}
public static void postToMain(final @NonNull Runnable runnable) {
handler.post(runnable);
getHandler().post(runnable);
}
public static void runOnMain(final @NonNull Runnable runnable) {
if (isMainThread()) runnable.run();
else handler.post(runnable);
else getHandler().post(runnable);
}
public static void runOnMainDelayed(final @NonNull Runnable runnable, long delayMillis) {
handler.postDelayed(runnable, delayMillis);
getHandler().postDelayed(runnable, delayMillis);
}
public static void cancelRunnableOnMain(@NonNull Runnable runnable) {
handler.removeCallbacks(runnable);
getHandler().removeCallbacks(runnable);
}
public static void runOnMainSync(final @NonNull Runnable runnable) {
@@ -510,4 +522,15 @@ public class Util {
return new DecimalFormat("#,##0.#").format(sizeBytes/Math.pow(1024, digitGroups)) + " " + units[digitGroups];
}
private static Handler getHandler() {
if (handler == null) {
synchronized (Util.class) {
if (handler == null) {
handler = new Handler(Looper.getMainLooper());
}
}
}
return handler;
}
}

View File

@@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.logging.Log;
public class WakeLockUtil {
private static final String TAG = WakeLockUtil.class.getSimpleName();
/**
* Run a runnable with a wake lock. Ensures that the lock is safely acquired and released.
*/
public static void runWithLock(@NonNull Context context, int lockType, long timeout, @NonNull String tag, @NonNull Runnable task) {
WakeLock wakeLock = null;
try {
wakeLock = acquire(context, lockType, timeout, tag);
task.run();
} finally {
if (wakeLock != null) {
release(wakeLock, tag);
}
}
}
public static WakeLock acquire(@NonNull Context context, int lockType, long timeout, @NonNull String tag) {
try {
PowerManager powerManager = ServiceUtil.getPowerManager(context);
WakeLock wakeLock = powerManager.newWakeLock(lockType, tag);
wakeLock.acquire(timeout);
Log.d(TAG, "Acquired wakelock with tag: " + tag);
return wakeLock;
} catch (Exception e) {
Log.w(TAG, "Failed to acquire wakelock with tag: " + tag, e);
return null;
}
}
public static void release(@NonNull WakeLock wakeLock, @NonNull String tag) {
try {
if (wakeLock.isHeld()) {
wakeLock.release();
Log.d(TAG, "Released wakelock with tag: " + tag);
} else {
Log.d(TAG, "Wakelock wasn't held at time of release: " + tag);
}
} catch (Exception e) {
Log.w(TAG, "Failed to release wakelock with tag: " + tag, e);
}
}
}

View File

@@ -1,9 +1,11 @@
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversation;
import android.database.Cursor;
import org.junit.Before;
import org.junit.Test;
import org.thoughtcrime.securesms.BaseUnitTest;
import org.thoughtcrime.securesms.conversation.ConversationAdapter;
import static org.junit.Assert.*;
import static org.mockito.Matchers.anyInt;
@@ -12,7 +14,7 @@ import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.when;
public class ConversationAdapterTest extends BaseUnitTest {
private Cursor cursor = mock(Cursor.class);
private Cursor cursor = mock(Cursor.class);
private ConversationAdapter adapter;
@Override

View File

@@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.util;
import org.junit.Test;
import org.whispersystems.libsignal.util.Pair;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class SearchUtilTest {
private static final Locale LOCALE = Locale.ENGLISH;
@Test
public void getHighlightRanges_singleHighlightToken() {
String text = "abc";
String highlight = "a";
List<Pair<Integer, Integer>> result = SearchUtil.getHighlightRanges(LOCALE, text, highlight);
assertEquals(Arrays.asList(new Pair<>(0, 1)), result);
}
@Test
public void getHighlightRanges_multipleHighlightTokens() {
String text = "a bc";
String highlight = "a b";
List<Pair<Integer, Integer>> result = SearchUtil.getHighlightRanges(LOCALE, text, highlight);
assertEquals(Arrays.asList(new Pair<>(0, 1), new Pair<>(2, 3)), result);
text = "abc def";
highlight = "ab de";
result = SearchUtil.getHighlightRanges(LOCALE, text, highlight);
assertEquals(Arrays.asList(new Pair<>(0, 2), new Pair<>(4, 6)), result);
}
@Test
public void getHighlightRanges_onlyHighlightPrefixes() {
String text = "abc";
String highlight = "b";
List<Pair<Integer, Integer>> result = SearchUtil.getHighlightRanges(LOCALE, text, highlight);
assertTrue(result.isEmpty());
text = "abc";
highlight = "c";
result = SearchUtil.getHighlightRanges(LOCALE, text, highlight);
assertTrue(result.isEmpty());
}
@Test
public void getHighlightRanges_resultNotInFirstToken() {
String text = "abc def ghi";
String highlight = "gh";
List<Pair<Integer, Integer>> result = SearchUtil.getHighlightRanges(LOCALE, text, highlight);
assertEquals(Arrays.asList(new Pair<>(8, 10)), result);
}
}

View File

@@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.util;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class UtilTest {
@Test
public void chunk_oneChunk() {
List<String> input = Arrays.asList("A", "B", "C");
List<List<String>> output = Util.chunk(input, 3);
assertEquals(1, output.size());
assertEquals(input, output.get(0));
output = Util.chunk(input, 4);
assertEquals(1, output.size());
assertEquals(input, output.get(0));
output = Util.chunk(input, 100);
assertEquals(1, output.size());
assertEquals(input, output.get(0));
}
@Test
public void chunk_multipleChunks() {
List<String> input = Arrays.asList("A", "B", "C", "D", "E");
List<List<String>> output = Util.chunk(input, 4);
assertEquals(2, output.size());
assertEquals(Arrays.asList("A", "B", "C", "D"), output.get(0));
assertEquals(Arrays.asList("E"), output.get(1));
output = Util.chunk(input, 2);
assertEquals(3, output.size());
assertEquals(Arrays.asList("A", "B"), output.get(0));
assertEquals(Arrays.asList("C", "D"), output.get(1));
assertEquals(Arrays.asList("E"), output.get(2));
output = Util.chunk(input, 1);
assertEquals(5, output.size());
assertEquals(Arrays.asList("A"), output.get(0));
assertEquals(Arrays.asList("B"), output.get(1));
assertEquals(Arrays.asList("C"), output.get(2));
assertEquals(Arrays.asList("D"), output.get(3));
assertEquals(Arrays.asList("E"), output.get(4));
}
}