mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-12 13:03:17 +01:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d21c36ddf | ||
|
|
983290aa5b | ||
|
|
88b9fc25d2 | ||
|
|
60c7fb0056 | ||
|
|
fa6da1902f | ||
|
|
5cc3ac00c7 | ||
|
|
33daa21ad9 | ||
|
|
c4d1bdc44d | ||
|
|
ca99c732f8 | ||
|
|
1f79808cf0 | ||
|
|
5c0e1100ed | ||
|
|
d0b763c16e | ||
|
|
b962751c96 | ||
|
|
94e8553b73 | ||
|
|
351b625975 | ||
|
|
a2b6dbda14 | ||
|
|
a6564f8f84 | ||
|
|
4dbe165c18 | ||
|
|
f29a42411e | ||
|
|
02b0800b22 | ||
|
|
2cfa431cad | ||
|
|
fe4068afce | ||
|
|
1c23603c25 | ||
|
|
c2a86fcc74 | ||
|
|
d42c9b5dbc | ||
|
|
3b6429c163 | ||
|
|
6896f8ea15 | ||
|
|
a3768c7d74 | ||
|
|
c9a0a66f18 | ||
|
|
db1ad39c6b | ||
|
|
9f04c28bfd | ||
|
|
10631d7e71 | ||
|
|
cfff10622a | ||
|
|
b769c7d9b6 | ||
|
|
1e0f691a56 |
@@ -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"/>
|
||||
|
||||
37
build.gradle
37
build.gradle
@@ -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'
|
||||
|
||||
@@ -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
1
gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
1
proguard-firebase-messaging.pro
Normal file
1
proguard-firebase-messaging.pro
Normal file
@@ -0,0 +1 @@
|
||||
-dontwarn com.google.firebase.analytics.connector.AnalyticsConnector
|
||||
BIN
res/drawable-hdpi/ic_note_to_self.png
Normal file
BIN
res/drawable-hdpi/ic_note_to_self.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 274 B |
BIN
res/drawable-mdpi/ic_note_to_self.png
Normal file
BIN
res/drawable-mdpi/ic_note_to_self.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 191 B |
BIN
res/drawable-xhdpi/ic_note_to_self.png
Normal file
BIN
res/drawable-xhdpi/ic_note_to_self.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 282 B |
BIN
res/drawable-xxhdpi/ic_note_to_self.png
Normal file
BIN
res/drawable-xxhdpi/ic_note_to_self.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 452 B |
BIN
res/drawable-xxxhdpi/ic_note_to_self.png
Normal file
BIN
res/drawable-xxxhdpi/ic_note_to_self.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 545 B |
41
res/layout/captcha_activity.xml
Normal file
41
res/layout/captcha_activity.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
76
res/layout/conversation_search_nav.xml
Normal file
76
res/layout/conversation_search_nav.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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"/>
|
||||
|
||||
|
||||
10
res/values/firebase_messaging.xml
Normal file
10
res/values/firebase_messaging.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>() {{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() -> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
47
src/org/thoughtcrime/securesms/backup/BackupPassphrase.java
Normal file
47
src/org/thoughtcrime/securesms/backup/BackupPassphrase.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)}.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
20
src/org/thoughtcrime/securesms/linkpreview/Link.java
Normal file
20
src/org/thoughtcrime/securesms/linkpreview/Link.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
src/org/thoughtcrime/securesms/util/CloseableLiveData.java
Normal file
35
src/org/thoughtcrime/securesms/util/CloseableLiveData.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
96
src/org/thoughtcrime/securesms/util/SearchUtil.java
Normal file
96
src/org/thoughtcrime/securesms/util/SearchUtil.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
56
src/org/thoughtcrime/securesms/util/WakeLockUtil.java
Normal file
56
src/org/thoughtcrime/securesms/util/WakeLockUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user