mirror of
https://github.com/oxen-io/session-android.git
synced 2025-04-21 20:01:30 +00:00
Merge remote-tracking branch 'upstream/dev' into pinned-message-icon-color
# Conflicts: # app/src/main/res/values/themes.xml
This commit is contained in:
commit
fff0e5a32d
@ -4,11 +4,11 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
classpath "com.android.tools.build:gradle:$gradlePluginVersion"
|
||||||
classpath files('libs/gradle-witness.jar')
|
classpath files('libs/gradle-witness.jar')
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
||||||
classpath "com.google.gms:google-services:4.3.10"
|
classpath "com.google.gms:google-services:$googleServicesVersion"
|
||||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
|
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,26 +27,27 @@ configurations.all {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
implementation 'com.google.android.material:material:1.2.1'
|
implementation "com.google.android.material:material:$materialVersion"
|
||||||
implementation 'com.google.android:flexbox:2.0.1'
|
implementation 'com.google.android:flexbox:2.0.1'
|
||||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
implementation "androidx.preference:preference-ktx:$preferenceVersion"
|
||||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
implementation 'androidx.exifinterface:exifinterface:1.3.4'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
||||||
implementation 'androidx.activity:activity-ktx:1.2.2'
|
implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.3.2'
|
implementation 'androidx.activity:activity-ktx:1.5.1'
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||||
implementation "androidx.work:work-runtime-ktx:2.4.0"
|
implementation "androidx.core:core-ktx:$coreVersion"
|
||||||
|
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||||
implementation ("com.google.firebase:firebase-messaging:18.0.0") {
|
implementation ("com.google.firebase:firebase-messaging:18.0.0") {
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||||
@ -94,7 +95,8 @@ dependencies {
|
|||||||
implementation 'com.takisoft.fix:colorpicker:1.0.1'
|
implementation 'com.takisoft.fix:colorpicker:1.0.1'
|
||||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||||
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
|
implementation 'androidx.sqlite:sqlite-ktx:2.2.0'
|
||||||
|
implementation 'net.zetetic:sqlcipher-android:4.5.3@aar'
|
||||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||||
exclude group: 'com.fasterxml.jackson.core'
|
exclude group: 'com.fasterxml.jackson.core'
|
||||||
exclude group: 'org.freemarker'
|
exclude group: 'org.freemarker'
|
||||||
@ -119,7 +121,7 @@ dependencies {
|
|||||||
implementation "com.github.tbruyelle:rxpermissions:0.10.2"
|
implementation "com.github.tbruyelle:rxpermissions:0.10.2"
|
||||||
implementation "com.github.ybq:Android-SpinKit:1.4.0"
|
implementation "com.github.ybq:Android-SpinKit:1.4.0"
|
||||||
implementation "com.opencsv:opencsv:4.6"
|
implementation "com.opencsv:opencsv:4.6"
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation "junit:junit:$junitVersion"
|
||||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||||
testImplementation "org.mockito:mockito-inline:4.0.0"
|
testImplementation "org.mockito:mockito-inline:4.0.0"
|
||||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||||
@ -127,7 +129,7 @@ dependencies {
|
|||||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
||||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
||||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
|
||||||
testImplementation 'androidx.test:core:1.3.0'
|
testImplementation "androidx.test:core:$testCoreVersion"
|
||||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
|
||||||
@ -141,7 +143,7 @@ dependencies {
|
|||||||
// Assertions
|
// Assertions
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
androidTestImplementation 'androidx.test.ext:truth:1.4.0'
|
androidTestImplementation 'androidx.test.ext:truth:1.4.0'
|
||||||
androidTestImplementation 'com.google.truth:truth:1.0'
|
androidTestImplementation 'com.google.truth:truth:1.1.3'
|
||||||
|
|
||||||
// Espresso dependencies
|
// Espresso dependencies
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
@ -151,14 +153,14 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0'
|
||||||
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
|
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
|
||||||
androidTestUtil 'androidx.test:orchestrator:1.4.0'
|
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||||
|
|
||||||
testImplementation 'org.robolectric:robolectric:4.4'
|
testImplementation 'org.robolectric:robolectric:4.4'
|
||||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 309
|
def canonicalVersionCode = 323
|
||||||
def canonicalVersionName = "1.16.0"
|
def canonicalVersionName = "1.16.3"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
@ -169,13 +171,9 @@ def abiPostFix = ['armeabi-v7a' : 1,
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion androidCompileSdkVersion
|
compileSdkVersion androidCompileSdkVersion
|
||||||
buildToolsVersion '29.0.3'
|
namespace 'network.loki.messenger'
|
||||||
useLibrary 'org.apache.http.legacy'
|
useLibrary 'org.apache.http.legacy'
|
||||||
|
|
||||||
dexOptions {
|
|
||||||
javaMaxHeapSize "4g"
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -209,7 +207,7 @@ android {
|
|||||||
versionName canonicalVersionName
|
versionName canonicalVersionName
|
||||||
|
|
||||||
minSdkVersion androidMinimumSdkVersion
|
minSdkVersion androidMinimumSdkVersion
|
||||||
targetSdkVersion androidCompileSdkVersion
|
targetSdkVersion androidTargetSdkVersion
|
||||||
|
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
1
app/proguard/proguard.pro
vendored
1
app/proguard/proguard.pro
vendored
@ -2,6 +2,7 @@
|
|||||||
-keepattributes SourceFile,LineNumberTable
|
-keepattributes SourceFile,LineNumberTable
|
||||||
-keep class org.whispersystems.** { *; }
|
-keep class org.whispersystems.** { *; }
|
||||||
-keep class org.thoughtcrime.securesms.** { *; }
|
-keep class org.thoughtcrime.securesms.** { *; }
|
||||||
|
-keep class org.thoughtcrime.securesms.components.menu.** { *; }
|
||||||
-keep class org.session.** { *; }
|
-keep class org.session.** { *; }
|
||||||
-keepclassmembers class ** {
|
-keepclassmembers class ** {
|
||||||
public void onEvent*(**);
|
public void onEvent*(**);
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest
|
<manifest
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="network.loki.messenger">
|
|
||||||
|
|
||||||
<uses-sdk tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference" />
|
<uses-sdk tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference" />
|
||||||
|
|
||||||
@ -31,6 +30,7 @@
|
|||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||||
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
|
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
|
||||||
@ -174,6 +174,7 @@
|
|||||||
android:screenOrientation="portrait"/>
|
android:screenOrientation="portrait"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
android:exported="true"
|
||||||
android:name="org.thoughtcrime.securesms.ShareActivity"
|
android:name="org.thoughtcrime.securesms.ShareActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
@ -321,6 +322,7 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="org.thoughtcrime.securesms.service.DirectShareService"
|
android:name="org.thoughtcrime.securesms.service.DirectShareService"
|
||||||
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
|
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.chooser.ChooserTargetService" />
|
<action android:name="android.service.chooser.ChooserTargetService" />
|
||||||
@ -398,42 +400,48 @@
|
|||||||
android:authorities="network.loki.securesms.database.recipient"
|
android:authorities="network.loki.securesms.database.recipient"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver android:name="org.thoughtcrime.securesms.service.BootReceiver">
|
<receiver android:name="org.thoughtcrime.securesms.service.BootReceiver"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="network.loki.securesms.RESTART" />
|
<action android:name="network.loki.securesms.RESTART" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name="org.thoughtcrime.securesms.service.LocalBackupListener">
|
<receiver android:name="org.thoughtcrime.securesms.service.LocalBackupListener"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name="org.thoughtcrime.securesms.service.PersistentConnectionBootListener">
|
<receiver android:name="org.thoughtcrime.securesms.service.PersistentConnectionBootListener"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name="org.thoughtcrime.securesms.notifications.LocaleChangedReceiver">
|
<receiver android:name="org.thoughtcrime.securesms.notifications.LocaleChangedReceiver"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name="org.thoughtcrime.securesms.notifications.DeleteNotificationReceiver">
|
<receiver android:name="org.thoughtcrime.securesms.notifications.DeleteNotificationReceiver"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="network.loki.securesms.DELETE_NOTIFICATION" />
|
<action android:name="network.loki.securesms.DELETE_NOTIFICATION" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
<receiver
|
||||||
android:name="org.thoughtcrime.securesms.service.PanicResponderListener"
|
android:name="org.thoughtcrime.securesms.service.PanicResponderListener"
|
||||||
android:exported="true">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
<receiver
|
||||||
android:name="org.thoughtcrime.securesms.notifications.BackgroundPollWorker$BootBroadcastReceiver"
|
android:name="org.thoughtcrime.securesms.notifications.BackgroundPollWorker$BootBroadcastReceiver"
|
||||||
android:enabled="true">
|
android:enabled="true"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -47,6 +47,7 @@ import org.session.libsession.utilities.Util;
|
|||||||
import org.session.libsession.utilities.WindowDebouncer;
|
import org.session.libsession.utilities.WindowDebouncer;
|
||||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||||
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
|
||||||
|
import org.session.libsignal.utilities.HTTP;
|
||||||
import org.session.libsignal.utilities.JsonUtil;
|
import org.session.libsignal.utilities.JsonUtil;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.session.libsignal.utilities.ThreadUtils;
|
import org.session.libsignal.utilities.ThreadUtils;
|
||||||
@ -57,6 +58,7 @@ import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
|||||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
||||||
import org.thoughtcrime.securesms.database.Storage;
|
import org.thoughtcrime.securesms.database.Storage;
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.database.model.EmojiSearchData;
|
import org.thoughtcrime.securesms.database.model.EmojiSearchData;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
|
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
|
||||||
@ -66,6 +68,7 @@ import org.thoughtcrime.securesms.groups.OpenGroupMigrator;
|
|||||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
||||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
||||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||||
@ -236,6 +239,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
resubmitProfilePictureIfNeeded();
|
resubmitProfilePictureIfNeeded();
|
||||||
loadEmojiSearchIndexIfNeeded();
|
loadEmojiSearchIndexIfNeeded();
|
||||||
EmojiSource.refresh();
|
EmojiSource.refresh();
|
||||||
|
|
||||||
|
NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
|
||||||
|
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -244,6 +250,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
Log.i(TAG, "App is now visible.");
|
Log.i(TAG, "App is now visible.");
|
||||||
KeyCachingService.onAppForegrounded(this);
|
KeyCachingService.onAppForegrounded(this);
|
||||||
|
|
||||||
|
// If the user account hasn't been created or onboarding wasn't finished then don't start
|
||||||
|
// the pollers
|
||||||
|
if (TextSecurePreferences.getLocalNumber(this) == null || !TextSecurePreferences.hasSeenWelcomeScreen(this)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ThreadUtils.queue(()->{
|
ThreadUtils.queue(()->{
|
||||||
if (poller != null) {
|
if (poller != null) {
|
||||||
poller.setCaughtUp(false);
|
poller.setCaughtUp(false);
|
||||||
@ -481,6 +493,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
|
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
|
||||||
ThreadUtils.queue(() -> {
|
ThreadUtils.queue(() -> {
|
||||||
// Don't generate a new profile key here; we do that when the user changes their profile picture
|
// Don't generate a new profile key here; we do that when the user changes their profile picture
|
||||||
|
Log.d("Loki-Avatar", "Uploading Avatar Started");
|
||||||
String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
|
String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
|
||||||
try {
|
try {
|
||||||
// Read the file into a byte array
|
// Read the file into a byte array
|
||||||
@ -497,6 +510,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
|
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
|
||||||
// Update the last profile picture upload date
|
// Update the last profile picture upload date
|
||||||
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
|
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
|
||||||
|
Log.d("Loki-Avatar", "Uploading Avatar Finished");
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
@ -535,7 +549,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
TextSecurePreferences.setProfileName(this, displayName);
|
TextSecurePreferences.setProfileName(this, displayName);
|
||||||
}
|
}
|
||||||
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||||
if (!deleteDatabase("signal.db")) {
|
if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) {
|
||||||
Log.d("Loki", "Failed to delete database.");
|
Log.d("Loki", "Failed to delete database.");
|
||||||
}
|
}
|
||||||
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
|
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
|
||||||
|
@ -114,7 +114,7 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
|
|||||||
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
|
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
|
||||||
|
|
||||||
if (slide != null) {
|
if (slide != null) {
|
||||||
thumbnailView.setImageResource(glideRequests, slide, false, false);
|
thumbnailView.setImageResource(glideRequests, slide, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||||
|
@ -148,12 +148,11 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
|||||||
// Finish and proceed with the next intent.
|
// Finish and proceed with the next intent.
|
||||||
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
|
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
|
||||||
if (nextIntent != null) {
|
if (nextIntent != null) {
|
||||||
|
try {
|
||||||
startActivity(nextIntent);
|
startActivity(nextIntent);
|
||||||
// try {
|
} catch (java.lang.SecurityException e) {
|
||||||
// startActivity(nextIntent);
|
Log.w(TAG, "Access permission not passed from PassphraseActivity, retry sharing.", e);
|
||||||
// } catch (java.lang.SecurityException e) {
|
}
|
||||||
// Log.w(TAG, "Access permission not passed from PassphraseActivity, retry sharing.");
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
@ -176,6 +176,11 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
return messageDB.getMessageID(serverId, threadId)
|
return messageDB.getMessageID(serverId, threadId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getMessageIDs(serverIds: List<Long>, threadId: Long): Pair<List<Long>, List<Long>> {
|
||||||
|
val messageDB = DatabaseComponent.get(context).lokiMessageDatabase()
|
||||||
|
return messageDB.getMessageIDs(serverIds, threadId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
||||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||||
else DatabaseComponent.get(context).mmsDatabase()
|
else DatabaseComponent.get(context).mmsDatabase()
|
||||||
@ -184,6 +189,15 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID)
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
|
||||||
|
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||||
|
else DatabaseComponent.get(context).mmsDatabase()
|
||||||
|
|
||||||
|
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
|
||||||
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
|
||||||
|
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs)
|
||||||
|
}
|
||||||
|
|
||||||
override fun updateMessageAsDeleted(timestamp: Long, author: String) {
|
override fun updateMessageAsDeleted(timestamp: Long, author: String) {
|
||||||
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
val database = DatabaseComponent.get(context).mmsSmsDatabase()
|
||||||
val address = Address.fromSerialized(author)
|
val address = Address.fromSerialized(author)
|
||||||
|
@ -13,6 +13,11 @@ class ScreenshotObserver(private val context: Context, handler: Handler, private
|
|||||||
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||||
super.onChange(selfChange, uri)
|
super.onChange(selfChange, uri)
|
||||||
uri ?: return
|
uri ?: return
|
||||||
|
|
||||||
|
// There is an odd bug where we can get notified for changes to 'content://media/external'
|
||||||
|
// directly which is a protected folder, this code is to prevent that crash
|
||||||
|
if (uri.scheme == "content" && uri.host == "media" && uri.path == "/external") { return }
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
queryRelativeDataColumn(uri)
|
queryRelativeDataColumn(uri)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,104 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.backup;
|
|
||||||
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.thoughtcrime.securesms.util.BackupDirSelector;
|
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class BackupDialog {
|
|
||||||
private static final String TAG = "BackupDialog";
|
|
||||||
|
|
||||||
public static void showEnableBackupDialog(
|
|
||||||
@NonNull Context context,
|
|
||||||
@NonNull SwitchPreferenceCompat preference,
|
|
||||||
@NonNull BackupDirSelector backupDirSelector) {
|
|
||||||
|
|
||||||
String[] password = BackupUtil.generateBackupPassphrase();
|
|
||||||
String passwordSt = Util.join(password, "");
|
|
||||||
|
|
||||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
|
||||||
.setTitle(R.string.BackupDialog_enable_local_backups)
|
|
||||||
.setView(R.layout.backup_enable_dialog)
|
|
||||||
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.create();
|
|
||||||
|
|
||||||
dialog.setOnShowListener(created -> {
|
|
||||||
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
|
|
||||||
button.setOnClickListener(v -> {
|
|
||||||
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
|
|
||||||
if (confirmationCheckBox.isChecked()) {
|
|
||||||
backupDirSelector.selectBackupDir(true, uri -> {
|
|
||||||
try {
|
|
||||||
BackupUtil.enableBackups(context, passwordSt);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Failed to activate backups.", e);
|
|
||||||
Toast.makeText(context,
|
|
||||||
context.getString(R.string.dialog_backup_activation_failed),
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
preference.setChecked(true);
|
|
||||||
created.dismiss();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.show();
|
|
||||||
|
|
||||||
CheckBox checkBox = dialog.findViewById(R.id.confirmation_check);
|
|
||||||
TextView textView = dialog.findViewById(R.id.confirmation_text);
|
|
||||||
|
|
||||||
((TextView)dialog.findViewById(R.id.code_first)).setText(password[0]);
|
|
||||||
((TextView)dialog.findViewById(R.id.code_second)).setText(password[1]);
|
|
||||||
((TextView)dialog.findViewById(R.id.code_third)).setText(password[2]);
|
|
||||||
|
|
||||||
((TextView)dialog.findViewById(R.id.code_fourth)).setText(password[3]);
|
|
||||||
((TextView)dialog.findViewById(R.id.code_fifth)).setText(password[4]);
|
|
||||||
((TextView)dialog.findViewById(R.id.code_sixth)).setText(password[5]);
|
|
||||||
|
|
||||||
textView.setOnClickListener(v -> checkBox.toggle());
|
|
||||||
|
|
||||||
dialog.findViewById(R.id.number_table).setOnClickListener(v -> {
|
|
||||||
((ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", passwordSt));
|
|
||||||
Toast.makeText(context, R.string.BackupDialog_copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void showDisableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
|
||||||
new AlertDialog.Builder(context)
|
|
||||||
.setTitle(R.string.BackupDialog_delete_backups)
|
|
||||||
.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) -> {
|
|
||||||
BackupUtil.disableBackups(context, true);
|
|
||||||
preference.setChecked(false);
|
|
||||||
})
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,206 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.backup
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.text.Spannable
|
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.text.style.ClickableSpan
|
|
||||||
import android.text.style.StyleSpan
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.google.android.gms.common.util.Strings
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
|
||||||
import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
|
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
|
||||||
import org.thoughtcrime.securesms.util.BackupUtil
|
|
||||||
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
|
||||||
import org.thoughtcrime.securesms.util.show
|
|
||||||
|
|
||||||
class BackupRestoreActivity : BaseActionBarActivity() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "BackupRestoreActivity"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val viewModel by viewModels<BackupRestoreViewModel>()
|
|
||||||
|
|
||||||
private val fileSelectionResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()
|
|
||||||
) { result: ActivityResult ->
|
|
||||||
if (result.resultCode == Activity.RESULT_OK && result.data != null && result.data!!.data != null) {
|
|
||||||
viewModel.backupFile.value = result.data!!.data!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setUpActionBarSessionLogo()
|
|
||||||
|
|
||||||
// val viewBinding = DataBindingUtil.setContentView<ActivityBackupRestoreBinding>(this, R.layout.activity_backup_restore)
|
|
||||||
// viewBinding.lifecycleOwner = this
|
|
||||||
// viewBinding.viewModel = viewModel
|
|
||||||
|
|
||||||
// viewBinding.restoreButton.setOnClickListener { viewModel.tryRestoreBackup() }
|
|
||||||
|
|
||||||
// viewBinding.buttonSelectFile.setOnClickListener {
|
|
||||||
// fileSelectionResultLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
// //FIXME On some old APIs (tested on 21 & 23) the mime type doesn't filter properly
|
|
||||||
// // and the backup files are unavailable for selection.
|
|
||||||
//// type = BackupUtil.BACKUP_FILE_MIME_TYPE
|
|
||||||
// type = "*/*"
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// viewBinding.backupCode.addTextChangedListener { text -> viewModel.backupPassphrase.value = text.toString() }
|
|
||||||
|
|
||||||
// Focus passphrase text edit when backup file is selected.
|
|
||||||
// viewModel.backupFile.observe(this, { backupFile ->
|
|
||||||
// if (backupFile != null) viewBinding.backupCode.post {
|
|
||||||
// viewBinding.backupCode.requestFocus()
|
|
||||||
// (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
|
|
||||||
// .showSoftInput(viewBinding.backupCode, InputMethodManager.SHOW_IMPLICIT)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// React to backup import result.
|
|
||||||
viewModel.backupImportResult.observe(this) { result ->
|
|
||||||
if (result != null) when (result) {
|
|
||||||
BackupRestoreViewModel.BackupRestoreResult.SUCCESS -> {
|
|
||||||
val intent = Intent(this, HomeActivity::class.java)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
this.show(intent)
|
|
||||||
}
|
|
||||||
BackupRestoreViewModel.BackupRestoreResult.FAILURE_VERSION_DOWNGRADE ->
|
|
||||||
Toast.makeText(this, R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show()
|
|
||||||
BackupRestoreViewModel.BackupRestoreResult.FAILURE_UNKNOWN ->
|
|
||||||
Toast.makeText(this, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//region Legal info views
|
|
||||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")
|
|
||||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(object : ClickableSpan() {
|
|
||||||
override fun onClick(widget: View) {
|
|
||||||
openURL("https://getsession.org/terms-of-service/")
|
|
||||||
}
|
|
||||||
}, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
termsExplanation.setSpan(object : ClickableSpan() {
|
|
||||||
override fun onClick(widget: View) {
|
|
||||||
openURL("https://getsession.org/privacy-policy/")
|
|
||||||
}
|
|
||||||
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
// viewBinding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
// viewBinding.termsTextView.text = termsExplanation
|
|
||||||
//endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openURL(url: String) {
|
|
||||||
try {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
|
||||||
startActivity(intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BackupRestoreViewModel(application: Application): AndroidViewModel(application) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "BackupRestoreViewModel"
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun uriToFileName(view: View, fileUri: Uri?): String? {
|
|
||||||
fileUri ?: return null
|
|
||||||
|
|
||||||
view.context.contentResolver.query(fileUri, null, null, null, null).use {
|
|
||||||
val nameIndex = it!!.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
||||||
it.moveToFirst()
|
|
||||||
return it.getString(nameIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun validateData(fileUri: Uri?, passphrase: String?): Boolean {
|
|
||||||
return fileUri != null &&
|
|
||||||
!Strings.isEmptyOrWhitespace(passphrase) &&
|
|
||||||
passphrase!!.length == BackupUtil.BACKUP_PASSPHRASE_LENGTH
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val backupFile = MutableLiveData<Uri>(null)
|
|
||||||
val backupPassphrase = MutableLiveData<String>(null)
|
|
||||||
|
|
||||||
val processingBackupFile = MutableLiveData<Boolean>(false)
|
|
||||||
val backupImportResult = MutableLiveData<BackupRestoreResult>(null)
|
|
||||||
|
|
||||||
fun tryRestoreBackup() = viewModelScope.launch {
|
|
||||||
if (processingBackupFile.value == true) return@launch
|
|
||||||
if (backupImportResult.value == BackupRestoreResult.SUCCESS) return@launch
|
|
||||||
if (!validateData(backupFile.value, backupPassphrase.value)) return@launch
|
|
||||||
|
|
||||||
val context = getApplication<Application>()
|
|
||||||
val backupFile = backupFile.value!!
|
|
||||||
val passphrase = backupPassphrase.value!!
|
|
||||||
|
|
||||||
val result: BackupRestoreResult
|
|
||||||
|
|
||||||
processingBackupFile.value = true
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
result = try {
|
|
||||||
val database = DatabaseComponent.get(context).openHelper().readableDatabase
|
|
||||||
FullBackupImporter.importFromUri(
|
|
||||||
context,
|
|
||||||
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
|
||||||
database,
|
|
||||||
backupFile,
|
|
||||||
passphrase
|
|
||||||
)
|
|
||||||
DatabaseFactory.upgradeRestored(context, database)
|
|
||||||
NotificationChannels.restoreContactNotificationChannels(context)
|
|
||||||
TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis())
|
|
||||||
TextSecurePreferences.setHasViewedSeed(context, true)
|
|
||||||
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
|
|
||||||
|
|
||||||
BackupRestoreResult.SUCCESS
|
|
||||||
} catch (e: DatabaseDowngradeException) {
|
|
||||||
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e)
|
|
||||||
BackupRestoreResult.FAILURE_VERSION_DOWNGRADE
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, e)
|
|
||||||
BackupRestoreResult.FAILURE_UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processingBackupFile.value = false
|
|
||||||
|
|
||||||
backupImportResult.value = result
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class BackupRestoreResult {
|
|
||||||
SUCCESS, FAILURE_VERSION_DOWNGRADE, FAILURE_UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,7 +8,7 @@ import androidx.annotation.WorkerThread
|
|||||||
import com.annimon.stream.function.Consumer
|
import com.annimon.stream.function.Consumer
|
||||||
import com.annimon.stream.function.Predicate
|
import com.annimon.stream.function.Predicate
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import net.sqlcipher.database.SQLiteDatabase
|
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.session.libsession.avatars.AvatarHelper
|
import org.session.libsession.avatars.AvatarHelper
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||||
|
@ -5,7 +5,7 @@ import android.content.ContentValues
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import net.sqlcipher.database.SQLiteDatabase
|
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.session.libsession.avatars.AvatarHelper
|
import org.session.libsession.avatars.AvatarHelper
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||||
|
@ -1,158 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
|
|
||||||
public class LinkPreviewView extends FrameLayout {
|
|
||||||
|
|
||||||
private static final int TYPE_CONVERSATION = 0;
|
|
||||||
private static final int TYPE_COMPOSE = 1;
|
|
||||||
|
|
||||||
private ViewGroup container;
|
|
||||||
private OutlinedThumbnailView thumbnail;
|
|
||||||
private TextView title;
|
|
||||||
private TextView site;
|
|
||||||
private View divider;
|
|
||||||
private View closeButton;
|
|
||||||
private View spinner;
|
|
||||||
|
|
||||||
private int type;
|
|
||||||
private int defaultRadius;
|
|
||||||
private CornerMask cornerMask;
|
|
||||||
private Outliner outliner;
|
|
||||||
private CloseClickedListener closeClickedListener;
|
|
||||||
|
|
||||||
public LinkPreviewView(Context context) {
|
|
||||||
super(context);
|
|
||||||
init(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LinkPreviewView(Context context, @Nullable AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
init(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init(@Nullable AttributeSet attrs) {
|
|
||||||
inflate(getContext(), R.layout.link_preview, this);
|
|
||||||
|
|
||||||
container = findViewById(R.id.linkpreview_container);
|
|
||||||
thumbnail = findViewById(R.id.linkpreview_thumbnail);
|
|
||||||
title = findViewById(R.id.linkpreview_title);
|
|
||||||
site = findViewById(R.id.linkpreview_site);
|
|
||||||
divider = findViewById(R.id.linkpreview_divider);
|
|
||||||
spinner = findViewById(R.id.linkpreview_progress_wheel);
|
|
||||||
closeButton = findViewById(R.id.linkpreview_close);
|
|
||||||
defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
|
|
||||||
cornerMask = new CornerMask(this);
|
|
||||||
outliner = new Outliner();
|
|
||||||
|
|
||||||
outliner.setColor(getResources().getColor(R.color.transparent));
|
|
||||||
|
|
||||||
if (attrs != null) {
|
|
||||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.LinkPreviewView, 0, 0);
|
|
||||||
type = typedArray.getInt(R.styleable.LinkPreviewView_linkpreview_type, 0);
|
|
||||||
typedArray.recycle();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == TYPE_COMPOSE) {
|
|
||||||
container.setBackgroundColor(Color.TRANSPARENT);
|
|
||||||
container.setPadding(0, 0, 0, 0);
|
|
||||||
divider.setVisibility(VISIBLE);
|
|
||||||
|
|
||||||
closeButton.setOnClickListener(v -> {
|
|
||||||
if (closeClickedListener != null) {
|
|
||||||
closeClickedListener.onCloseClicked();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setWillNotDraw(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void dispatchDraw(Canvas canvas) {
|
|
||||||
super.dispatchDraw(canvas);
|
|
||||||
if (type == TYPE_COMPOSE) return;
|
|
||||||
|
|
||||||
cornerMask.mask(canvas);
|
|
||||||
outliner.draw(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLoading() {
|
|
||||||
title.setVisibility(GONE);
|
|
||||||
site.setVisibility(GONE);
|
|
||||||
thumbnail.setVisibility(GONE);
|
|
||||||
spinner.setVisibility(VISIBLE);
|
|
||||||
closeButton.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showCloseButton) {
|
|
||||||
setLinkPreview(glideRequests, linkPreview, showThumbnail);
|
|
||||||
if (showCloseButton) {
|
|
||||||
closeButton.setVisibility(VISIBLE);
|
|
||||||
} else {
|
|
||||||
closeButton.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
|
||||||
title.setVisibility(VISIBLE);
|
|
||||||
site.setVisibility(VISIBLE);
|
|
||||||
thumbnail.setVisibility(VISIBLE);
|
|
||||||
spinner.setVisibility(GONE);
|
|
||||||
closeButton.setVisibility(VISIBLE);
|
|
||||||
|
|
||||||
title.setText(linkPreview.getTitle());
|
|
||||||
|
|
||||||
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
|
|
||||||
if (url != null) {
|
|
||||||
site.setText(url.topPrivateDomain());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
|
|
||||||
thumbnail.setVisibility(VISIBLE);
|
|
||||||
thumbnail.setImageResource(glideRequests, new ImageSlide(getContext(), linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
|
|
||||||
thumbnail.showDownloadText(false);
|
|
||||||
} else {
|
|
||||||
thumbnail.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCorners(int topLeft, int topRight) {
|
|
||||||
cornerMask.setRadii(topLeft, topRight, 0, 0);
|
|
||||||
outliner.setRadii(topLeft, topRight, 0, 0);
|
|
||||||
thumbnail.setCorners(topLeft, defaultRadius, defaultRadius, defaultRadius);
|
|
||||||
postInvalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCloseClickedListener(@Nullable CloseClickedListener closeClickedListener) {
|
|
||||||
this.closeClickedListener = closeClickedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDownloadClickedListener(SlidesClickedListener listener) {
|
|
||||||
thumbnail.setDownloadClickListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface CloseClickedListener {
|
|
||||||
void onCloseClicked();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.ThemeUtil;
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class OutlinedThumbnailView extends ThumbnailView {
|
|
||||||
|
|
||||||
private CornerMask cornerMask;
|
|
||||||
private Outliner outliner;
|
|
||||||
|
|
||||||
public OutlinedThumbnailView(Context context) {
|
|
||||||
super(context);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public OutlinedThumbnailView(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
cornerMask = new CornerMask(this);
|
|
||||||
outliner = new Outliner();
|
|
||||||
|
|
||||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
|
||||||
setWillNotDraw(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void dispatchDraw(Canvas canvas) {
|
|
||||||
super.dispatchDraw(canvas);
|
|
||||||
|
|
||||||
cornerMask.mask(canvas);
|
|
||||||
outliner.draw(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
|
||||||
cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
|
||||||
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
|
||||||
postInvalidate();
|
|
||||||
}
|
|
||||||
}
|
|
@ -34,6 +34,8 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
private val profilePicturesCache = mutableMapOf<String, String?>()
|
private val profilePicturesCache = mutableMapOf<String, String?>()
|
||||||
private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
|
private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
|
||||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
||||||
|
private val unknownOpenGroupDrawable = ResourceContactPhoto(R.drawable.ic_notification)
|
||||||
|
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -43,10 +45,8 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
|
||||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||||
}
|
}
|
||||||
fun isOpenGroupWithProfilePicture(recipient: Recipient): Boolean {
|
|
||||||
return recipient.isOpenGroupRecipient && recipient.groupAvatarId != null
|
if (recipient.isClosedGroupRecipient) {
|
||||||
}
|
|
||||||
if (recipient.isGroupRecipient && !isOpenGroupWithProfilePicture(recipient)) {
|
|
||||||
val members = DatabaseComponent.get(context).groupDatabase()
|
val members = DatabaseComponent.get(context).groupDatabase()
|
||||||
.getGroupMemberAddresses(recipient.address.toGroupString(), true)
|
.getGroupMemberAddresses(recipient.address.toGroupString(), true)
|
||||||
.sorted()
|
.sorted()
|
||||||
@ -107,7 +107,7 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
|
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
|
||||||
val signalProfilePicture = recipient.contactPhoto
|
val signalProfilePicture = recipient.contactPhoto
|
||||||
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
||||||
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
|
||||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
glide.load(signalProfilePicture)
|
glide.load(signalProfilePicture)
|
||||||
@ -117,7 +117,12 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
|
} else if (recipient.isOpenGroupRecipient && recipient.groupAvatarId == null) {
|
||||||
|
glide.clear(imageView)
|
||||||
|
imageView.setImageDrawable(unknownOpenGroupDrawable)
|
||||||
} else {
|
} else {
|
||||||
|
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||||
|
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
glide.load(placeholder)
|
glide.load(placeholder)
|
||||||
.placeholder(unknownRecipientDrawable)
|
.placeholder(unknownRecipientDrawable)
|
||||||
|
@ -52,19 +52,4 @@ public class StickerView extends FrameLayout {
|
|||||||
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
|
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
|
||||||
image.setOnLongClickListener(l);
|
image.setOnLongClickListener(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSticker(@NonNull GlideRequests glideRequests, @NonNull Slide stickerSlide) {
|
|
||||||
boolean showControls = stickerSlide.asAttachment().getDataUri() == null;
|
|
||||||
|
|
||||||
image.setImageResource(glideRequests, stickerSlide, showControls, false);
|
|
||||||
missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setThumbnailClickListener(@NonNull SlideClickListener listener) {
|
|
||||||
image.setThumbnailClickListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDownloadClickListener(@NonNull SlidesClickedListener listener) {
|
|
||||||
image.setDownloadClickListener(listener);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ public class EmojiTextView extends AppCompatTextView {
|
|||||||
private static final char ELLIPSIS = '…';
|
private static final char ELLIPSIS = '…';
|
||||||
|
|
||||||
private CharSequence previousText;
|
private CharSequence previousText;
|
||||||
private BufferType previousBufferType;
|
private BufferType previousBufferType = BufferType.NORMAL;
|
||||||
private float originalFontSize;
|
private float originalFontSize;
|
||||||
private boolean useSystemEmoji;
|
private boolean useSystemEmoji;
|
||||||
private boolean sizeChangeInProgress;
|
private boolean sizeChangeInProgress;
|
||||||
@ -49,6 +49,15 @@ public class EmojiTextView extends AppCompatTextView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override public void setText(@Nullable CharSequence text, BufferType type) {
|
@Override public void setText(@Nullable CharSequence text, BufferType type) {
|
||||||
|
// No need to do anything special if the text is null or empty
|
||||||
|
if (text == null || text.length() == 0) {
|
||||||
|
previousText = text;
|
||||||
|
previousOverflowText = overflowText;
|
||||||
|
previousBufferType = type;
|
||||||
|
super.setText(text, type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
EmojiParser.CandidateList candidates = EmojiProvider.getCandidates(text);
|
EmojiParser.CandidateList candidates = EmojiProvider.getCandidates(text);
|
||||||
|
|
||||||
if (scaleEmojis && candidates != null && candidates.allEmojis) {
|
if (scaleEmojis && candidates != null && candidates.allEmojis) {
|
||||||
@ -149,8 +158,13 @@ public class EmojiTextView extends AppCompatTextView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) {
|
private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) {
|
||||||
return Util.equals(previousText, text) &&
|
CharSequence finalPrevText = (previousText == null || previousText.length() == 0 ? "" : previousText);
|
||||||
Util.equals(previousOverflowText, overflowText) &&
|
CharSequence finalText = (text == null || text.length() == 0 ? "" : text);
|
||||||
|
CharSequence finalPrevOverflowText = (previousOverflowText == null || previousOverflowText.length() == 0 ? "" : previousOverflowText);
|
||||||
|
CharSequence finalOverflowText = (overflowText == null || overflowText.length() == 0 ? "" : overflowText);
|
||||||
|
|
||||||
|
return Util.equals(finalPrevText, finalText) &&
|
||||||
|
Util.equals(finalPrevOverflowText, finalOverflowText) &&
|
||||||
Util.equals(previousBufferType, bufferType) &&
|
Util.equals(previousBufferType, bufferType) &&
|
||||||
useSystemEmoji == useSystemEmoji() &&
|
useSystemEmoji == useSystemEmoji() &&
|
||||||
!sizeChangeInProgress;
|
!sizeChangeInProgress;
|
||||||
|
@ -8,7 +8,6 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.loader.app.LoaderManager
|
import androidx.loader.app.LoaderManager
|
||||||
import androidx.loader.content.Loader
|
import androidx.loader.content.Loader
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
|
||||||
import network.loki.messenger.databinding.ContactSelectionListFragmentBinding
|
import network.loki.messenger.databinding.ContactSelectionListFragmentBinding
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
@ -58,7 +57,6 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(activity)
|
binding.recyclerView.layoutManager = LinearLayoutManager(activity)
|
||||||
binding.recyclerView.adapter = listAdapter
|
binding.recyclerView.adapter = listAdapter
|
||||||
binding.swipeRefreshLayout.isEnabled = requireActivity().intent.getBooleanExtra(REFRESHABLE, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
@ -73,15 +71,6 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
|||||||
|
|
||||||
fun resetQueryFilter() {
|
fun resetQueryFilter() {
|
||||||
setQueryFilter(null)
|
setQueryFilter(null)
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setRefreshing(refreshing: Boolean) {
|
|
||||||
binding.swipeRefreshLayout.isRefreshing = refreshing
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setOnRefreshListener(onRefreshListener: OnRefreshListener?) {
|
|
||||||
binding.swipeRefreshLayout.setOnRefreshListener(onRefreshListener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ContactSelectionListItem>> {
|
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ContactSelectionListItem>> {
|
||||||
@ -106,7 +95,7 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
listAdapter.items = items
|
listAdapter.items = items
|
||||||
binding.mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
|
binding.recyclerView.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
|
||||||
binding.emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
|
binding.emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,12 +3,12 @@ package org.thoughtcrime.securesms.contacts
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.loader.app.LoaderManager
|
|
||||||
import androidx.loader.content.Loader
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.loader.app.LoaderManager
|
||||||
|
import androidx.loader.content.Loader
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivitySelectContactsBinding
|
import network.loki.messenger.databinding.ActivitySelectContactsBinding
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
@ -49,7 +49,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
|||||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_done, menu)
|
menuInflater.inflate(R.menu.menu_done, menu)
|
||||||
return members.isNotEmpty()
|
return members.isNotEmpty()
|
||||||
}
|
}
|
||||||
@ -70,7 +70,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
|||||||
|
|
||||||
private fun update(members: List<String>) {
|
private fun update(members: List<String>) {
|
||||||
this.members = members
|
this.members = members
|
||||||
binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
binding.recyclerView.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||||
binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.paging
|
||||||
|
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
|
||||||
|
private const val TIME_BUCKET = 600000L // bucket into 10 minute increments
|
||||||
|
|
||||||
|
private fun config() = PagingConfig(
|
||||||
|
pageSize = 25,
|
||||||
|
maxSize = 100,
|
||||||
|
enablePlaceholders = false
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Long.bucketed(): Long = (TIME_BUCKET - this % TIME_BUCKET) + this
|
||||||
|
|
||||||
|
fun conversationPager(threadId: Long, initialKey: PageLoad? = null, db: MmsSmsDatabase, contactDb: SessionContactDatabase) = Pager(config(), initialKey = initialKey) {
|
||||||
|
ConversationPagingSource(threadId, db, contactDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConversationPagerDiffCallback: DiffUtil.ItemCallback<MessageAndContact>() {
|
||||||
|
override fun areItemsTheSame(oldItem: MessageAndContact, newItem: MessageAndContact): Boolean =
|
||||||
|
oldItem.message.id == newItem.message.id && oldItem.message.isMms == newItem.message.isMms
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: MessageAndContact, newItem: MessageAndContact): Boolean =
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MessageAndContact(val message: MessageRecord,
|
||||||
|
val contact: Contact?)
|
||||||
|
|
||||||
|
data class PageLoad(val fromTime: Long, val toTime: Long? = null)
|
||||||
|
|
||||||
|
class ConversationPagingSource(
|
||||||
|
private val threadId: Long,
|
||||||
|
private val messageDb: MmsSmsDatabase,
|
||||||
|
private val contactDb: SessionContactDatabase
|
||||||
|
): PagingSource<PageLoad, MessageAndContact>() {
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<PageLoad, MessageAndContact>): PageLoad? {
|
||||||
|
val anchorPosition = state.anchorPosition ?: return null
|
||||||
|
val anchorPage = state.closestPageToPosition(anchorPosition) ?: return null
|
||||||
|
val next = anchorPage.nextKey?.fromTime
|
||||||
|
val previous = anchorPage.prevKey?.fromTime ?: anchorPage.data.firstOrNull()?.message?.dateSent ?: return null
|
||||||
|
return PageLoad(previous, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val contactCache = mutableMapOf<String, Contact>()
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun getContact(sessionId: String): Contact? {
|
||||||
|
contactCache[sessionId]?.let { contact ->
|
||||||
|
return contact
|
||||||
|
} ?: run {
|
||||||
|
contactDb.getContactWithSessionID(sessionId)?.let { contact ->
|
||||||
|
contactCache[sessionId] = contact
|
||||||
|
return contact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<PageLoad>): LoadResult<PageLoad, MessageAndContact> {
|
||||||
|
val pageLoad = params.key ?: withContext(Dispatchers.IO) {
|
||||||
|
messageDb.getConversationSnippet(threadId).use {
|
||||||
|
val reader = messageDb.readerFor(it)
|
||||||
|
var record: MessageRecord? = null
|
||||||
|
if (reader != null) {
|
||||||
|
record = reader.next
|
||||||
|
while (record != null && record.isDeleted) {
|
||||||
|
record = reader.next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
record?.dateSent?.let { fromTime ->
|
||||||
|
PageLoad(fromTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: return LoadResult.Page(emptyList(), null, null)
|
||||||
|
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
val cursor = messageDb.getConversationPage(
|
||||||
|
threadId,
|
||||||
|
pageLoad.fromTime,
|
||||||
|
pageLoad.toTime ?: -1L,
|
||||||
|
params.loadSize
|
||||||
|
)
|
||||||
|
val processedList = mutableListOf<MessageAndContact>()
|
||||||
|
val reader = messageDb.readerFor(cursor)
|
||||||
|
while (reader.next != null && !invalid) {
|
||||||
|
reader.current?.let { item ->
|
||||||
|
val contact = getContact(item.individualRecipient.address.serialize())
|
||||||
|
processedList += MessageAndContact(item, contact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.close()
|
||||||
|
processedList.toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNext = withContext(Dispatchers.IO) {
|
||||||
|
if (result.isEmpty()) return@withContext false
|
||||||
|
val lastTime = result.last().message.dateSent
|
||||||
|
messageDb.hasNextPage(threadId, lastTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
val nextCheckTime = if (hasNext) {
|
||||||
|
val lastSent = result.last().message.dateSent
|
||||||
|
if (lastSent == pageLoad.fromTime) null else lastSent
|
||||||
|
} else null
|
||||||
|
|
||||||
|
val hasPrevious = withContext(Dispatchers.IO) { messageDb.hasPreviousPage(threadId, pageLoad.fromTime) }
|
||||||
|
val nextKey = if (!hasNext) null else nextCheckTime
|
||||||
|
val prevKey = if (!hasPrevious) null else messageDb.getPreviousPage(threadId, pageLoad.fromTime, params.loadSize)
|
||||||
|
|
||||||
|
return LoadResult.Page(
|
||||||
|
data = result, // next check time is not null if drop is true
|
||||||
|
prevKey = prevKey?.let { PageLoad(it, pageLoad.fromTime) },
|
||||||
|
nextKey = nextKey?.let { PageLoad(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -3,31 +3,18 @@ package org.thoughtcrime.securesms.conversation.v2
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.animation.FloatEvaluator
|
import android.animation.FloatEvaluator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.ClipData
|
import android.content.*
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.AsyncTask
|
import android.os.*
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Pair
|
import android.util.Pair
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.ActionMode
|
import android.view.*
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@ -53,6 +40,8 @@ import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
|||||||
import nl.komponents.kovenant.ui.successUi
|
import nl.komponents.kovenant.ui.successUi
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||||
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
import org.session.libsession.messaging.mentions.Mention
|
import org.session.libsession.messaging.mentions.Mention
|
||||||
import org.session.libsession.messaging.mentions.MentionsManager
|
import org.session.libsession.messaging.mentions.MentionsManager
|
||||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification
|
import org.session.libsession.messaging.messages.control.DataExtractionNotification
|
||||||
@ -68,12 +57,8 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
|||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||||
import org.session.libsession.messaging.utilities.SessionId
|
import org.session.libsession.messaging.utilities.SessionId
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.*
|
||||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||||
import org.session.libsession.utilities.GroupUtil
|
|
||||||
import org.session.libsession.utilities.MediaTypes
|
|
||||||
import org.session.libsession.utilities.Stub
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsession.utilities.concurrent.SimpleTask
|
import org.session.libsession.utilities.concurrent.SimpleTask
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsession.utilities.recipients.RecipientModifiedListener
|
import org.session.libsession.utilities.recipients.RecipientModifiedListener
|
||||||
@ -106,25 +91,10 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
|
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
|
||||||
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
|
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
|
import org.thoughtcrime.securesms.conversation.v2.utilities.*
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
|
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
import org.thoughtcrime.securesms.database.*
|
||||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.ReactionDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageId
|
import org.thoughtcrime.securesms.database.model.MessageId
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
@ -137,25 +107,12 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
|||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
|
||||||
import org.thoughtcrime.securesms.mediasend.Media
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity
|
import org.thoughtcrime.securesms.mediasend.MediaSendActivity
|
||||||
import org.thoughtcrime.securesms.mms.AudioSlide
|
import org.thoughtcrime.securesms.mms.*
|
||||||
import org.thoughtcrime.securesms.mms.GifSlide
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide
|
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
|
||||||
import org.thoughtcrime.securesms.mms.VideoSlide
|
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
|
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
|
||||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
|
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
|
||||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
import org.thoughtcrime.securesms.util.*
|
||||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
import java.util.*
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
|
||||||
import org.thoughtcrime.securesms.util.push
|
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
@ -295,6 +252,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
onDeselect(message, position, it)
|
onDeselect(message, position, it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onAttachmentNeedsDownload = { attachmentId, mmsId ->
|
||||||
|
// Start download (on IO thread)
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId))
|
||||||
|
}
|
||||||
|
},
|
||||||
glide = glide,
|
glide = glide,
|
||||||
lifecycleCoroutineScope = lifecycleScope
|
lifecycleCoroutineScope = lifecycleScope
|
||||||
)
|
)
|
||||||
@ -352,13 +315,26 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
restoreDraftIfNeeded()
|
restoreDraftIfNeeded()
|
||||||
setUpUiStateObserver()
|
setUpUiStateObserver()
|
||||||
binding!!.scrollToBottomButton.setOnClickListener {
|
binding!!.scrollToBottomButton.setOnClickListener {
|
||||||
val layoutManager = binding?.conversationRecyclerView?.layoutManager ?: return@setOnClickListener
|
val layoutManager = (binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager) ?: return@setOnClickListener
|
||||||
|
|
||||||
if (layoutManager.isSmoothScrolling) {
|
if (layoutManager.isSmoothScrolling) {
|
||||||
binding?.conversationRecyclerView?.scrollToPosition(0)
|
binding?.conversationRecyclerView?.scrollToPosition(0)
|
||||||
} else {
|
} else {
|
||||||
|
// It looks like 'smoothScrollToPosition' will actually load all intermediate items in
|
||||||
|
// order to do the scroll, this can be very slow if there are a lot of messages so
|
||||||
|
// instead we check the current position and if there are more than 10 items to scroll
|
||||||
|
// we jump instantly to the 10th item and scroll from there (this should happen quick
|
||||||
|
// enough to give a similar scroll effect without having to load everything)
|
||||||
|
val position = layoutManager.findFirstVisibleItemPosition()
|
||||||
|
if (position > 10) {
|
||||||
|
binding?.conversationRecyclerView?.scrollToPosition(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding?.conversationRecyclerView?.post {
|
||||||
binding?.conversationRecyclerView?.smoothScrollToPosition(0)
|
binding?.conversationRecyclerView?.smoothScrollToPosition(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
|
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
|
||||||
updateUnreadCountIndicator()
|
updateUnreadCountIndicator()
|
||||||
setUpTypingObserver()
|
setUpTypingObserver()
|
||||||
@ -388,7 +364,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId)
|
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId)
|
||||||
val recipient = viewModel.recipient ?: return
|
val recipient = viewModel.recipient ?: return
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient)
|
threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient)
|
||||||
|
}
|
||||||
|
|
||||||
contentResolver.registerContentObserver(
|
contentResolver.registerContentObserver(
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
true,
|
true,
|
||||||
@ -481,6 +461,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
// called from onCreate
|
// called from onCreate
|
||||||
private fun setUpInputBar() {
|
private fun setUpInputBar() {
|
||||||
|
binding!!.inputBar.isVisible = viewModel.openGroup == null || viewModel.openGroup?.canWrite == true
|
||||||
binding!!.inputBar.delegate = this
|
binding!!.inputBar.delegate = this
|
||||||
binding!!.inputBarRecordingView.delegate = this
|
binding!!.inputBarRecordingView.delegate = this
|
||||||
// GIF button
|
// GIF button
|
||||||
@ -635,7 +616,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
this
|
this
|
||||||
) { onOptionsItemSelected(it) }
|
) { onOptionsItemSelected(it) }
|
||||||
}
|
}
|
||||||
super.onPrepareOptionsMenu(menu)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -660,7 +640,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
updateSubtitle()
|
updateSubtitle()
|
||||||
showOrHideInputIfNeeded()
|
showOrHideInputIfNeeded()
|
||||||
binding?.toolbarContent?.profilePictureView?.root?.update(threadRecipient)
|
binding?.toolbarContent?.profilePictureView?.root?.update(threadRecipient)
|
||||||
binding!!.toolbarContent.conversationTitleView.text = when {
|
binding?.toolbarContent?.conversationTitleView?.text = when {
|
||||||
threadRecipient.isLocalNumber -> getString(R.string.note_to_self)
|
threadRecipient.isLocalNumber -> getString(R.string.note_to_self)
|
||||||
else -> threadRecipient.toShortString()
|
else -> threadRecipient.toShortString()
|
||||||
}
|
}
|
||||||
@ -1789,6 +1769,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
endActionMode()
|
endActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun destroyActionMode() {
|
||||||
|
this.actionMode = null
|
||||||
|
}
|
||||||
|
|
||||||
private fun sendScreenshotNotification() {
|
private fun sendScreenshotNotification() {
|
||||||
val recipient = viewModel.recipient ?: return
|
val recipient = viewModel.recipient ?: return
|
||||||
if (recipient.isGroupRecipient) return
|
if (recipient.isGroupRecipient) return
|
||||||
@ -1834,7 +1818,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
if (result == null) return@Observer
|
if (result == null) return@Observer
|
||||||
if (result.getResults().isNotEmpty()) {
|
if (result.getResults().isNotEmpty()) {
|
||||||
result.getResults()[result.position]?.let {
|
result.getResults()[result.position]?.let {
|
||||||
jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs) {
|
jumpToMessage(it.messageRecipient.address, it.sentTimestampMs) {
|
||||||
searchViewModel.onMissingResult() }
|
searchViewModel.onMissingResult() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1900,7 +1884,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
ConversationReactionOverlay.Action.DELETE -> deleteMessages(selectedItems)
|
ConversationReactionOverlay.Action.DELETE -> deleteMessages(selectedItems)
|
||||||
ConversationReactionOverlay.Action.BAN_AND_DELETE_ALL -> banAndDeleteAll(selectedItems)
|
ConversationReactionOverlay.Action.BAN_AND_DELETE_ALL -> banAndDeleteAll(selectedItems)
|
||||||
ConversationReactionOverlay.Action.BAN_USER -> banUser(selectedItems)
|
ConversationReactionOverlay.Action.BAN_USER -> banUser(selectedItems)
|
||||||
ConversationReactionOverlay.Action.COPY_SESSION_ID -> TODO()
|
ConversationReactionOverlay.Action.COPY_SESSION_ID -> copySessionID(selectedItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,10 +39,10 @@ class ConversationAdapter(
|
|||||||
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit,
|
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit,
|
||||||
private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit,
|
private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit,
|
||||||
private val onDeselect: (MessageRecord, Int) -> Unit,
|
private val onDeselect: (MessageRecord, Int) -> Unit,
|
||||||
|
private val onAttachmentNeedsDownload: (Long, Long) -> Unit,
|
||||||
private val glide: GlideRequests,
|
private val glide: GlideRequests,
|
||||||
lifecycleCoroutineScope: LifecycleCoroutineScope
|
lifecycleCoroutineScope: LifecycleCoroutineScope
|
||||||
)
|
) : CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
|
||||||
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
|
|
||||||
private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() }
|
private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() }
|
||||||
private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() }
|
private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() }
|
||||||
var selectedItems = mutableSetOf<MessageRecord>()
|
var selectedItems = mutableSetOf<MessageRecord>()
|
||||||
@ -120,7 +120,18 @@ class ConversationAdapter(
|
|||||||
}
|
}
|
||||||
val contact = contactCache[senderIdHash]
|
val contact = contactCache[senderIdHash]
|
||||||
|
|
||||||
visibleMessageView.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery, contact, senderId, visibleMessageViewDelegate)
|
visibleMessageView.bind(
|
||||||
|
message,
|
||||||
|
messageBefore,
|
||||||
|
getMessageAfter(position, cursor),
|
||||||
|
glide,
|
||||||
|
searchQuery,
|
||||||
|
contact,
|
||||||
|
senderId,
|
||||||
|
visibleMessageViewDelegate,
|
||||||
|
onAttachmentNeedsDownload
|
||||||
|
)
|
||||||
|
|
||||||
if (!message.isDeleted) {
|
if (!message.isDeleted) {
|
||||||
visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) }
|
visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) }
|
||||||
visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
||||||
|
@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.goterl.lazysodium.utils.KeyPair
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
@ -48,11 +50,19 @@ class ConversationViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveDraft(text: String) {
|
fun saveDraft(text: String) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
repository.saveDraft(threadId, text)
|
repository.saveDraft(threadId, text)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getDraft(): String? {
|
fun getDraft(): String? {
|
||||||
return repository.getDraft(threadId)
|
val draft: String? = repository.getDraft(threadId)
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
repository.clearDrafts(threadId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return draft
|
||||||
}
|
}
|
||||||
|
|
||||||
fun inviteContacts(contacts: List<Recipient>) {
|
fun inviteContacts(contacts: List<Recipient>) {
|
||||||
@ -182,7 +192,6 @@ class ConversationViewModel(
|
|||||||
data class UiMessage(val id: Long, val message: String)
|
data class UiMessage(val id: Long, val message: String)
|
||||||
|
|
||||||
data class ConversationUiState(
|
data class ConversationUiState(
|
||||||
val isOxenHostedOpenGroup: Boolean = false,
|
|
||||||
val uiMessages: List<UiMessage> = emptyList(),
|
val uiMessages: List<UiMessage> = emptyList(),
|
||||||
val isMessageRequestAccepted: Boolean? = null
|
val isMessageRequestAccepted: Boolean? = null
|
||||||
)
|
)
|
||||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityMessageDetailBinding
|
import network.loki.messenger.databinding.ActivityMessageDetailBinding
|
||||||
@ -20,8 +21,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
|||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.*
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@ -48,7 +48,10 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
// We only show this screen for messages fail to send,
|
// We only show this screen for messages fail to send,
|
||||||
// so the author of the messages must be the current user.
|
// so the author of the messages must be the current user.
|
||||||
val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
|
val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
|
||||||
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author)
|
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) ?: run {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
val threadId = messageRecord!!.threadId
|
val threadId = messageRecord!!.threadId
|
||||||
val openGroup = storage.getOpenGroup(threadId)
|
val openGroup = storage.getOpenGroup(threadId)
|
||||||
val blindedKey = openGroup?.let { group ->
|
val blindedKey = openGroup?.let { group ->
|
||||||
@ -71,8 +74,15 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
|
|||||||
val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
|
val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
|
||||||
binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
|
binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
|
||||||
|
|
||||||
val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) ?: "Message failed to send."
|
val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId())
|
||||||
|
if (errorMessage != null) {
|
||||||
binding.errorMessage.text = errorMessage
|
binding.errorMessage.text = errorMessage
|
||||||
|
binding.resendContainer.isVisible = true
|
||||||
|
binding.errorContainer.isVisible = true
|
||||||
|
} else {
|
||||||
|
binding.errorContainer.isVisible = false
|
||||||
|
binding.resendContainer.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
|
if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
|
||||||
binding.expiresContainer.visibility = View.GONE
|
binding.expiresContainer.visibility = View.GONE
|
||||||
|
@ -8,53 +8,39 @@ import android.view.LayoutInflater
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.RelativeLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
|
import network.loki.messenger.databinding.AlbumThumbnailViewBinding
|
||||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.MediaPreviewActivity
|
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||||
import org.thoughtcrime.securesms.components.CornerMask
|
import org.thoughtcrime.securesms.components.CornerMask
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.mms.Slide
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||||
|
|
||||||
class AlbumThumbnailView : FrameLayout {
|
class AlbumThumbnailView : RelativeLayout {
|
||||||
|
|
||||||
private lateinit var binding: AlbumThumbnailViewBinding
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MAX_ALBUM_DISPLAY_SIZE = 3
|
const val MAX_ALBUM_DISPLAY_SIZE = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val binding: AlbumThumbnailViewBinding by lazy { AlbumThumbnailViewBinding.bind(this) }
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) {
|
constructor(context: Context) : super(context)
|
||||||
initialize()
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
}
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val cornerMask by lazy { CornerMask(this) }
|
private val cornerMask by lazy { CornerMask(this) }
|
||||||
private var slides: List<Slide> = listOf()
|
private var slides: List<Slide> = listOf()
|
||||||
private var slideSize: Int = 0
|
private var slideSize: Int = 0
|
||||||
|
|
||||||
private fun initialize() {
|
|
||||||
binding = AlbumThumbnailViewBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispatchDraw(canvas: Canvas?) {
|
override fun dispatchDraw(canvas: Canvas?) {
|
||||||
super.dispatchDraw(canvas)
|
super.dispatchDraw(canvas)
|
||||||
cornerMask.mask(canvas)
|
cornerMask.mask(canvas)
|
||||||
@ -63,26 +49,25 @@ class AlbumThumbnailView : FrameLayout {
|
|||||||
|
|
||||||
// region Interaction
|
// region Interaction
|
||||||
|
|
||||||
fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient) {
|
fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient, onAttachmentNeedsDownload: (Long, Long) -> Unit) {
|
||||||
val rawXInt = event.rawX.toInt()
|
val rawXInt = event.rawX.toInt()
|
||||||
val rawYInt = event.rawY.toInt()
|
val rawYInt = event.rawY.toInt()
|
||||||
val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||||
val testRect = Rect()
|
val testRect = Rect()
|
||||||
// test each album child
|
// test each album child
|
||||||
binding.albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
|
binding.albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed forEach@{ index, child ->
|
||||||
child.getGlobalVisibleRect(testRect)
|
child.getGlobalVisibleRect(testRect)
|
||||||
if (testRect.contains(eventRect)) {
|
if (testRect.contains(eventRect)) {
|
||||||
// hit intersects with this particular child
|
// hit intersects with this particular child
|
||||||
val slide = slides.getOrNull(index) ?: return
|
val slide = slides.getOrNull(index) ?: return@forEach
|
||||||
// only open to downloaded images
|
// only open to downloaded images
|
||||||
if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) {
|
if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) {
|
||||||
// restart download here
|
// Restart download here (on IO thread)
|
||||||
(slide.asAttachment() as? DatabaseAttachment)?.let { attachment ->
|
(slide.asAttachment() as? DatabaseAttachment)?.let { attachment ->
|
||||||
val attachmentId = attachment.attachmentId.rowId
|
onAttachmentNeedsDownload(attachment.attachmentId.rowId, mms.getId())
|
||||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mms.getId()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (slide.isInProgress) return
|
if (slide.isInProgress) return@forEach
|
||||||
|
|
||||||
ActivityDispatcher.get(context)?.dispatchIntent { context ->
|
ActivityDispatcher.get(context)?.dispatchIntent { context ->
|
||||||
MediaPreviewActivity.getPreviewIntent(context, slide, mms, threadRecipient)
|
MediaPreviewActivity.getPreviewIntent(context, slide, mms, threadRecipient)
|
||||||
@ -133,7 +118,7 @@ class AlbumThumbnailView : FrameLayout {
|
|||||||
else -> R.layout.album_thumbnail_3 // three stacked with additional text
|
else -> R.layout.album_thumbnail_3 // three stacked with additional text
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getThumbnailView(position: Int): KThumbnailView = when (position) {
|
fun getThumbnailView(position: Int): ThumbnailView = when (position) {
|
||||||
0 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
|
0 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
|
||||||
1 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
|
1 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
|
||||||
2 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
|
2 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
|
||||||
|
@ -23,7 +23,7 @@ class LinkPreviewDraftView : LinearLayout {
|
|||||||
// Start out with the loader showing and the content view hidden
|
// Start out with the loader showing and the content view hidden
|
||||||
binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true)
|
binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
binding.linkPreviewDraftContainer.isVisible = false
|
binding.linkPreviewDraftContainer.isVisible = false
|
||||||
binding.thumbnailImageView.clipToOutline = true
|
binding.thumbnailImageView.root.clipToOutline = true
|
||||||
binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,10 +31,10 @@ class LinkPreviewDraftView : LinearLayout {
|
|||||||
// Hide the loader and show the content view
|
// Hide the loader and show the content view
|
||||||
binding.linkPreviewDraftContainer.isVisible = true
|
binding.linkPreviewDraftContainer.isVisible = true
|
||||||
binding.linkPreviewDraftLoader.isVisible = false
|
binding.linkPreviewDraftLoader.isVisible = false
|
||||||
binding.thumbnailImageView.radius = toPx(4, resources)
|
binding.thumbnailImageView.root.radius = toPx(4, resources)
|
||||||
if (linkPreview.getThumbnail().isPresent) {
|
if (linkPreview.getThumbnail().isPresent) {
|
||||||
// This internally fetches the thumbnail
|
// This internally fetches the thumbnail
|
||||||
binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false)
|
binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, null)
|
||||||
}
|
}
|
||||||
binding.linkPreviewDraftTitleTextView.text = linkPreview.title
|
binding.linkPreviewDraftTitleTextView.text = linkPreview.title
|
||||||
}
|
}
|
||||||
|
@ -1,118 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class TypingIndicatorView extends LinearLayout {
|
|
||||||
private boolean isActive;
|
|
||||||
private long startTime;
|
|
||||||
|
|
||||||
private static final long CYCLE_DURATION = 1500;
|
|
||||||
private static final long DOT_DURATION = 600;
|
|
||||||
private static final float MIN_ALPHA = 0.4f;
|
|
||||||
private static final float MIN_SCALE = 0.75f;
|
|
||||||
|
|
||||||
private View dot1;
|
|
||||||
private View dot2;
|
|
||||||
private View dot3;
|
|
||||||
|
|
||||||
public TypingIndicatorView(Context context) {
|
|
||||||
super(context);
|
|
||||||
initialize(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TypingIndicatorView(Context context, @Nullable AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
initialize(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize(@Nullable AttributeSet attrs) {
|
|
||||||
inflate(getContext(), R.layout.view_typing_indicator, this);
|
|
||||||
|
|
||||||
setWillNotDraw(false);
|
|
||||||
|
|
||||||
dot1 = findViewById(R.id.typing_dot1);
|
|
||||||
dot2 = findViewById(R.id.typing_dot2);
|
|
||||||
dot3 = findViewById(R.id.typing_dot3);
|
|
||||||
|
|
||||||
if (attrs != null) {
|
|
||||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.TypingIndicatorView, 0, 0);
|
|
||||||
int tint = typedArray.getColor(R.styleable.TypingIndicatorView_typingIndicator_tint, Color.WHITE);
|
|
||||||
typedArray.recycle();
|
|
||||||
|
|
||||||
dot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
|
||||||
dot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
|
||||||
dot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDraw(Canvas canvas) {
|
|
||||||
if (!isActive) {
|
|
||||||
super.onDraw(canvas);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long timeInCycle = (System.currentTimeMillis() - startTime) % CYCLE_DURATION;
|
|
||||||
|
|
||||||
render(dot1, timeInCycle, 0);
|
|
||||||
render(dot2, timeInCycle, 150);
|
|
||||||
render(dot3, timeInCycle, 300);
|
|
||||||
|
|
||||||
super.onDraw(canvas);
|
|
||||||
postInvalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void render(View dot, long timeInCycle, long start) {
|
|
||||||
long end = start + DOT_DURATION;
|
|
||||||
long peak = start + (DOT_DURATION / 2);
|
|
||||||
|
|
||||||
if (timeInCycle < start || timeInCycle > end) {
|
|
||||||
renderDefault(dot);
|
|
||||||
} else if (timeInCycle < peak) {
|
|
||||||
renderFadeIn(dot, timeInCycle, start);
|
|
||||||
} else {
|
|
||||||
renderFadeOut(dot, timeInCycle, peak);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void renderDefault(View dot) {
|
|
||||||
dot.setAlpha(MIN_ALPHA);
|
|
||||||
dot.setScaleX(MIN_SCALE);
|
|
||||||
dot.setScaleY(MIN_SCALE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void renderFadeIn(View dot, long timeInCycle, long fadeInStart) {
|
|
||||||
float percent = (float) (timeInCycle - fadeInStart) / 300;
|
|
||||||
dot.setAlpha(MIN_ALPHA + (1 - MIN_ALPHA) * percent);
|
|
||||||
dot.setScaleX(MIN_SCALE + (1 - MIN_SCALE) * percent);
|
|
||||||
dot.setScaleY(MIN_SCALE + (1 - MIN_SCALE) * percent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void renderFadeOut(View dot, long timeInCycle, long fadeOutStart) {
|
|
||||||
float percent = (float) (timeInCycle - fadeOutStart) / 300;
|
|
||||||
dot.setAlpha(1 - (1 - MIN_ALPHA) * percent);
|
|
||||||
dot.setScaleX(1 - (1 - MIN_SCALE) * percent);
|
|
||||||
dot.setScaleY(1 - (1 - MIN_SCALE) * percent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startAnimation() {
|
|
||||||
isActive = true;
|
|
||||||
startTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
postInvalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopAnimation() {
|
|
||||||
isActive = false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,105 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewTypingIndicatorBinding
|
||||||
|
|
||||||
|
class TypingIndicatorView : LinearLayout {
|
||||||
|
companion object {
|
||||||
|
private const val CYCLE_DURATION: Long = 1500
|
||||||
|
private const val DOT_DURATION: Long = 600
|
||||||
|
private const val MIN_ALPHA = 0.4f
|
||||||
|
private const val MIN_SCALE = 0.75f
|
||||||
|
}
|
||||||
|
|
||||||
|
private val binding: ViewTypingIndicatorBinding by lazy {
|
||||||
|
val binding = ViewTypingIndicatorBinding.bind(this)
|
||||||
|
|
||||||
|
if (tint != -1) {
|
||||||
|
binding.typingDot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
|
||||||
|
binding.typingDot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
|
||||||
|
binding.typingDot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@lazy binding
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isActive = false
|
||||||
|
private var startTime: Long = 0
|
||||||
|
private var tint: Int = -1
|
||||||
|
|
||||||
|
constructor(context: Context) : super(context) { initialize(null) }
|
||||||
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
|
||||||
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
||||||
|
|
||||||
|
private fun initialize(attrs: AttributeSet?) {
|
||||||
|
setWillNotDraw(false)
|
||||||
|
|
||||||
|
if (attrs != null) {
|
||||||
|
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.TypingIndicatorView, 0, 0)
|
||||||
|
this.tint = typedArray.getColor(R.styleable.TypingIndicatorView_typingIndicator_tint, Color.WHITE)
|
||||||
|
typedArray.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
if (!isActive) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val timeInCycle = (System.currentTimeMillis() - startTime) % CYCLE_DURATION
|
||||||
|
render(binding.typingDot1, timeInCycle, 0)
|
||||||
|
render(binding.typingDot2, timeInCycle, 150)
|
||||||
|
render(binding.typingDot3, timeInCycle, 300)
|
||||||
|
super.onDraw(canvas)
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun render(dot: View?, timeInCycle: Long, start: Long) {
|
||||||
|
val end = start + DOT_DURATION
|
||||||
|
val peak = start + DOT_DURATION / 2
|
||||||
|
if (timeInCycle < start || timeInCycle > end) {
|
||||||
|
renderDefault(dot)
|
||||||
|
} else if (timeInCycle < peak) {
|
||||||
|
renderFadeIn(dot, timeInCycle, start)
|
||||||
|
} else {
|
||||||
|
renderFadeOut(dot, timeInCycle, peak)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderDefault(dot: View?) {
|
||||||
|
dot!!.alpha = MIN_ALPHA
|
||||||
|
dot.scaleX = MIN_SCALE
|
||||||
|
dot.scaleY = MIN_SCALE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderFadeIn(dot: View?, timeInCycle: Long, fadeInStart: Long) {
|
||||||
|
val percent = (timeInCycle - fadeInStart).toFloat() / 300
|
||||||
|
dot!!.alpha = MIN_ALPHA + (1 - MIN_ALPHA) * percent
|
||||||
|
dot.scaleX = MIN_SCALE + (1 - MIN_SCALE) * percent
|
||||||
|
dot.scaleY = MIN_SCALE + (1 - MIN_SCALE) * percent
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderFadeOut(dot: View?, timeInCycle: Long, fadeOutStart: Long) {
|
||||||
|
val percent = (timeInCycle - fadeOutStart).toFloat() / 300
|
||||||
|
dot!!.alpha = 1 - (1 - MIN_ALPHA) * percent
|
||||||
|
dot.scaleX = 1 - (1 - MIN_SCALE) * percent
|
||||||
|
dot.scaleY = 1 - (1 - MIN_SCALE) * percent
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startAnimation() {
|
||||||
|
isActive = true
|
||||||
|
startTime = System.currentTimeMillis()
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopAnimation() {
|
||||||
|
isActive = false
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,7 @@ class TypingIndicatorViewContainer : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setTypists(typists: List<Recipient>) {
|
fun setTypists(typists: List<Recipient>) {
|
||||||
if (typists.isEmpty()) { binding.typingIndicator.stopAnimation(); return }
|
if (typists.isEmpty()) { binding.typingIndicator.root.stopAnimation(); return }
|
||||||
binding.typingIndicator.startAnimation()
|
binding.typingIndicator.root.startAnimation()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -65,9 +65,9 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
|
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
|
||||||
// Copy Session ID
|
// Copy Session ID
|
||||||
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
|
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
|
||||||
(thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.recipient.address.toString() != userPublicKey)
|
(thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
|
||||||
// Message detail
|
// Message detail
|
||||||
menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
|
menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isOutgoing)
|
||||||
// Resend
|
// Resend
|
||||||
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
|
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
|
||||||
// Save media
|
// Save media
|
||||||
@ -101,6 +101,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
override fun onDestroyActionMode(mode: ActionMode) {
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
adapter.selectedItems.clear()
|
adapter.selectedItems.clear()
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
|
delegate?.destroyActionMode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,4 +117,5 @@ interface ConversationActionModeCallbackDelegate {
|
|||||||
fun showMessageDetail(messages: Set<MessageRecord>)
|
fun showMessageDetail(messages: Set<MessageRecord>)
|
||||||
fun saveAttachment(messages: Set<MessageRecord>)
|
fun saveAttachment(messages: Set<MessageRecord>)
|
||||||
fun reply(messages: Set<MessageRecord>)
|
fun reply(messages: Set<MessageRecord>)
|
||||||
|
fun destroyActionMode()
|
||||||
}
|
}
|
@ -1,346 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.messages;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.HapticFeedbackConstants;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.constraintlayout.widget.Group;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import com.google.android.flexbox.FlexboxLayout;
|
|
||||||
import com.google.android.flexbox.JustifyContent;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
import org.session.libsession.utilities.ThemeUtil;
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ViewUtil;
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
|
||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
|
||||||
import org.thoughtcrime.securesms.util.NumberUtil;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class EmojiReactionsView extends LinearLayout implements View.OnTouchListener {
|
|
||||||
|
|
||||||
// Normally 6dp, but we have 1dp left+right margin on the pills themselves
|
|
||||||
private final int OUTER_MARGIN = ViewUtil.dpToPx(2);
|
|
||||||
private static final int DEFAULT_THRESHOLD = 5;
|
|
||||||
|
|
||||||
private List<ReactionRecord> records;
|
|
||||||
private long messageId;
|
|
||||||
private ViewGroup container;
|
|
||||||
private Group showLess;
|
|
||||||
private VisibleMessageViewDelegate delegate;
|
|
||||||
private Handler gestureHandler = new Handler(Looper.getMainLooper());
|
|
||||||
private Runnable pressCallback;
|
|
||||||
private Runnable longPressCallback;
|
|
||||||
private long onDownTimestamp = 0;
|
|
||||||
private static long longPressDurationThreshold = 250;
|
|
||||||
private static long maxDoubleTapInterval = 200;
|
|
||||||
private boolean extended = false;
|
|
||||||
|
|
||||||
public EmojiReactionsView(Context context) {
|
|
||||||
super(context);
|
|
||||||
init(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EmojiReactionsView(Context context, @Nullable AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
init(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init(@Nullable AttributeSet attrs) {
|
|
||||||
inflate(getContext(), R.layout.view_emoji_reactions, this);
|
|
||||||
|
|
||||||
this.container = findViewById(R.id.layout_emoji_container);
|
|
||||||
this.showLess = findViewById(R.id.group_show_less);
|
|
||||||
|
|
||||||
records = new ArrayList<>();
|
|
||||||
|
|
||||||
if (attrs != null) {
|
|
||||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiReactionsView, 0, 0);
|
|
||||||
typedArray.recycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
this.records.clear();
|
|
||||||
container.removeAllViews();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReactions(long messageId, @NonNull List<ReactionRecord> records, boolean outgoing, VisibleMessageViewDelegate delegate) {
|
|
||||||
this.delegate = delegate;
|
|
||||||
if (records.equals(this.records)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FlexboxLayout containerLayout = (FlexboxLayout) this.container;
|
|
||||||
containerLayout.setJustifyContent(outgoing ? JustifyContent.FLEX_END : JustifyContent.FLEX_START);
|
|
||||||
this.records.clear();
|
|
||||||
this.records.addAll(records);
|
|
||||||
if (this.messageId != messageId) {
|
|
||||||
extended = false;
|
|
||||||
}
|
|
||||||
this.messageId = messageId;
|
|
||||||
|
|
||||||
displayReactions(extended ? Integer.MAX_VALUE : DEFAULT_THRESHOLD);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTouch(View v, MotionEvent event) {
|
|
||||||
if (v.getTag() == null) return false;
|
|
||||||
|
|
||||||
Reaction reaction = (Reaction) v.getTag();
|
|
||||||
int action = event.getAction();
|
|
||||||
if (action == MotionEvent.ACTION_DOWN) onDown(new MessageId(reaction.messageId, reaction.isMms));
|
|
||||||
else if (action == MotionEvent.ACTION_CANCEL) removeLongPressCallback();
|
|
||||||
else if (action == MotionEvent.ACTION_UP) onUp(reaction);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayReactions(int threshold) {
|
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
|
||||||
List<Reaction> reactions = buildSortedReactionsList(records, userPublicKey, threshold);
|
|
||||||
|
|
||||||
container.removeAllViews();
|
|
||||||
LinearLayout overflowContainer = new LinearLayout(getContext());
|
|
||||||
overflowContainer.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
int innerPadding = ViewUtil.dpToPx(4);
|
|
||||||
overflowContainer.setPaddingRelative(innerPadding,innerPadding,innerPadding,innerPadding);
|
|
||||||
|
|
||||||
int pixelSize = ViewUtil.dpToPx(1);
|
|
||||||
|
|
||||||
for (Reaction reaction : reactions) {
|
|
||||||
if (container.getChildCount() + 1 >= DEFAULT_THRESHOLD && threshold != Integer.MAX_VALUE && reactions.size() > threshold) {
|
|
||||||
if (overflowContainer.getParent() == null) {
|
|
||||||
container.addView(overflowContainer);
|
|
||||||
MarginLayoutParams overflowParams = (MarginLayoutParams) overflowContainer.getLayoutParams();
|
|
||||||
overflowParams.height = ViewUtil.dpToPx(26);
|
|
||||||
overflowParams.setMargins(pixelSize, pixelSize, pixelSize, pixelSize);
|
|
||||||
overflowContainer.setLayoutParams(overflowParams);
|
|
||||||
overflowContainer.setBackground(ContextCompat.getDrawable(getContext(), R.drawable.reaction_pill_background));
|
|
||||||
}
|
|
||||||
View pill = buildPill(getContext(), this, reaction, true);
|
|
||||||
pill.setOnClickListener(v -> {
|
|
||||||
extended = true;
|
|
||||||
displayReactions(Integer.MAX_VALUE);
|
|
||||||
});
|
|
||||||
pill.findViewById(R.id.reactions_pill_count).setVisibility(View.GONE);
|
|
||||||
pill.findViewById(R.id.reactions_pill_spacer).setVisibility(View.GONE);
|
|
||||||
overflowContainer.addView(pill);
|
|
||||||
} else {
|
|
||||||
View pill = buildPill(getContext(), this, reaction, false);
|
|
||||||
pill.setTag(reaction);
|
|
||||||
pill.setOnTouchListener(this);
|
|
||||||
MarginLayoutParams params = (MarginLayoutParams) pill.getLayoutParams();
|
|
||||||
params.setMargins(pixelSize, pixelSize, pixelSize, pixelSize);
|
|
||||||
pill.setLayoutParams(params);
|
|
||||||
container.addView(pill);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int overflowChildren = overflowContainer.getChildCount();
|
|
||||||
int negativeMargin = ViewUtil.dpToPx(-8);
|
|
||||||
for (int i = 0; i < overflowChildren; i++) {
|
|
||||||
View child = overflowContainer.getChildAt(i);
|
|
||||||
MarginLayoutParams childParams = (MarginLayoutParams) child.getLayoutParams();
|
|
||||||
if ((i == 0 && overflowChildren > 1) || i + 1 < overflowChildren) {
|
|
||||||
// if first and there is more than one child, or we are not the last child then set negative right margin
|
|
||||||
childParams.setMargins(0,0, negativeMargin, 0);
|
|
||||||
child.setLayoutParams(childParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (threshold == Integer.MAX_VALUE) {
|
|
||||||
showLess.setVisibility(VISIBLE);
|
|
||||||
for (int id : showLess.getReferencedIds()) {
|
|
||||||
findViewById(id).setOnClickListener(view -> {
|
|
||||||
extended = false;
|
|
||||||
displayReactions(DEFAULT_THRESHOLD);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showLess.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onReactionClicked(Reaction reaction) {
|
|
||||||
if (reaction.messageId != 0) {
|
|
||||||
MessageId messageId = new MessageId(reaction.messageId, reaction.isMms);
|
|
||||||
delegate.onReactionClicked(reaction.emoji, messageId, reaction.userWasSender);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @NonNull List<Reaction> buildSortedReactionsList(@NonNull List<ReactionRecord> records, String userPublicKey, int threshold) {
|
|
||||||
Map<String, Reaction> counters = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
for (ReactionRecord record : records) {
|
|
||||||
String baseEmoji = EmojiUtil.getCanonicalRepresentation(record.getEmoji());
|
|
||||||
Reaction info = counters.get(baseEmoji);
|
|
||||||
|
|
||||||
if (info == null) {
|
|
||||||
info = new Reaction(record.getMessageId(), record.isMms(), record.getEmoji(), record.getCount(), record.getSortId(), record.getDateReceived(), userPublicKey.equals(record.getAuthor()));
|
|
||||||
} else {
|
|
||||||
info.update(record.getEmoji(), record.getCount(), record.getDateReceived(), userPublicKey.equals(record.getAuthor()));
|
|
||||||
}
|
|
||||||
|
|
||||||
counters.put(baseEmoji, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Reaction> reactions = new ArrayList<>(counters.values());
|
|
||||||
|
|
||||||
Collections.sort(reactions, Collections.reverseOrder());
|
|
||||||
|
|
||||||
if (reactions.size() >= threshold + 2 && threshold != Integer.MAX_VALUE) {
|
|
||||||
List<Reaction> shortened = new ArrayList<>(threshold + 2);
|
|
||||||
shortened.addAll(reactions.subList(0, threshold + 2));
|
|
||||||
return shortened;
|
|
||||||
} else {
|
|
||||||
return reactions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static View buildPill(@NonNull Context context, @NonNull ViewGroup parent, @NonNull Reaction reaction, boolean isCompact) {
|
|
||||||
View root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false);
|
|
||||||
EmojiImageView emojiView = root.findViewById(R.id.reactions_pill_emoji);
|
|
||||||
TextView countView = root.findViewById(R.id.reactions_pill_count);
|
|
||||||
View spacer = root.findViewById(R.id.reactions_pill_spacer);
|
|
||||||
|
|
||||||
if (isCompact) {
|
|
||||||
root.setPaddingRelative(1,1,1,1);
|
|
||||||
ViewGroup.LayoutParams layoutParams = root.getLayoutParams();
|
|
||||||
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
|
||||||
root.setLayoutParams(layoutParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reaction.emoji != null) {
|
|
||||||
emojiView.setImageEmoji(reaction.emoji);
|
|
||||||
|
|
||||||
if (reaction.count >= 1) {
|
|
||||||
countView.setText(NumberUtil.getFormattedNumber(reaction.count));
|
|
||||||
} else {
|
|
||||||
countView.setVisibility(GONE);
|
|
||||||
spacer.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
emojiView.setVisibility(GONE);
|
|
||||||
spacer.setVisibility(GONE);
|
|
||||||
countView.setText(context.getString(R.string.ReactionsConversationView_plus, reaction.count));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reaction.userWasSender && !isCompact) {
|
|
||||||
root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected));
|
|
||||||
countView.setTextColor(ThemeUtil.getThemedColor(context, R.attr.reactionsPillSelectedTextColor));
|
|
||||||
} else {
|
|
||||||
if (!isCompact) {
|
|
||||||
root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onDown(MessageId messageId) {
|
|
||||||
removeLongPressCallback();
|
|
||||||
Runnable newLongPressCallback = () -> {
|
|
||||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
|
||||||
if (delegate != null) {
|
|
||||||
delegate.onReactionLongClicked(messageId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.longPressCallback = newLongPressCallback;
|
|
||||||
gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold);
|
|
||||||
onDownTimestamp = new Date().getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeLongPressCallback() {
|
|
||||||
if (longPressCallback != null) {
|
|
||||||
gestureHandler.removeCallbacks(longPressCallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onUp(Reaction reaction) {
|
|
||||||
if ((new Date().getTime() - onDownTimestamp) < longPressDurationThreshold) {
|
|
||||||
removeLongPressCallback();
|
|
||||||
if (pressCallback != null) {
|
|
||||||
gestureHandler.removeCallbacks(pressCallback);
|
|
||||||
this.pressCallback = null;
|
|
||||||
} else {
|
|
||||||
Runnable newPressCallback = () -> {
|
|
||||||
onReactionClicked(reaction);
|
|
||||||
pressCallback = null;
|
|
||||||
};
|
|
||||||
this.pressCallback = newPressCallback;
|
|
||||||
gestureHandler.postDelayed(newPressCallback, maxDoubleTapInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Reaction implements Comparable<Reaction> {
|
|
||||||
private final long messageId;
|
|
||||||
private final boolean isMms;
|
|
||||||
private String emoji;
|
|
||||||
private long count;
|
|
||||||
private long sortIndex;
|
|
||||||
private long lastSeen;
|
|
||||||
private boolean userWasSender;
|
|
||||||
|
|
||||||
Reaction(long messageId, boolean isMms, @Nullable String emoji, long count, long sortIndex, long lastSeen, boolean userWasSender) {
|
|
||||||
this.messageId = messageId;
|
|
||||||
this.isMms = isMms;
|
|
||||||
this.emoji = emoji;
|
|
||||||
this.count = count;
|
|
||||||
this.sortIndex = sortIndex;
|
|
||||||
this.lastSeen = lastSeen;
|
|
||||||
this.userWasSender = userWasSender;
|
|
||||||
}
|
|
||||||
|
|
||||||
void update(@NonNull String emoji, long count, long lastSeen, boolean userWasSender) {
|
|
||||||
if (!this.userWasSender) {
|
|
||||||
if (userWasSender || lastSeen > this.lastSeen) {
|
|
||||||
this.emoji = emoji;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.count = this.count + count;
|
|
||||||
this.lastSeen = Math.max(this.lastSeen, lastSeen);
|
|
||||||
this.userWasSender = this.userWasSender || userWasSender;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull Reaction merge(@NonNull Reaction other) {
|
|
||||||
this.count = this.count + other.count;
|
|
||||||
this.lastSeen = Math.max(this.lastSeen, other.lastSeen);
|
|
||||||
this.userWasSender = this.userWasSender || other.userWasSender;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(Reaction rhs) {
|
|
||||||
Reaction lhs = this;
|
|
||||||
if (lhs.count == rhs.count ) {
|
|
||||||
return Long.compare(lhs.sortIndex, rhs.sortIndex);
|
|
||||||
} else {
|
|
||||||
return Long.compare(lhs.count, rhs.count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,291 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.*
|
||||||
|
import android.view.View.OnTouchListener
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.google.android.flexbox.JustifyContent
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import network.loki.messenger.databinding.ViewEmojiReactionsBinding
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
|
||||||
|
import org.session.libsession.utilities.ThemeUtil
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiUtil
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.ViewUtil
|
||||||
|
import org.thoughtcrime.securesms.database.model.MessageId
|
||||||
|
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||||
|
import org.thoughtcrime.securesms.util.NumberUtil.getFormattedNumber
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class EmojiReactionsView : ConstraintLayout, OnTouchListener {
|
||||||
|
companion object {
|
||||||
|
private const val DEFAULT_THRESHOLD = 5
|
||||||
|
private const val longPressDurationThreshold: Long = 250
|
||||||
|
private const val maxDoubleTapInterval: Long = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
private val binding: ViewEmojiReactionsBinding by lazy { ViewEmojiReactionsBinding.bind(this) }
|
||||||
|
|
||||||
|
// Normally 6dp, but we have 1dp left+right margin on the pills themselves
|
||||||
|
private val OUTER_MARGIN = ViewUtil.dpToPx(2)
|
||||||
|
private var records: MutableList<ReactionRecord>? = null
|
||||||
|
private var messageId: Long = 0
|
||||||
|
private var delegate: VisibleMessageViewDelegate? = null
|
||||||
|
private val gestureHandler = Handler(Looper.getMainLooper())
|
||||||
|
private var pressCallback: Runnable? = null
|
||||||
|
private var longPressCallback: Runnable? = null
|
||||||
|
private var onDownTimestamp: Long = 0
|
||||||
|
private var extended = false
|
||||||
|
|
||||||
|
constructor(context: Context) : super(context) { init(null) }
|
||||||
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(attrs) }
|
||||||
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(attrs) }
|
||||||
|
|
||||||
|
private fun init(attrs: AttributeSet?) {
|
||||||
|
records = ArrayList()
|
||||||
|
|
||||||
|
if (attrs != null) {
|
||||||
|
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.EmojiReactionsView, 0, 0)
|
||||||
|
typedArray.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
records!!.clear()
|
||||||
|
binding.layoutEmojiContainer.removeAllViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setReactions(messageId: Long, records: List<ReactionRecord>, outgoing: Boolean, delegate: VisibleMessageViewDelegate?) {
|
||||||
|
this.delegate = delegate
|
||||||
|
if (records == this.records) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.layoutEmojiContainer.justifyContent = if (outgoing) JustifyContent.FLEX_END else JustifyContent.FLEX_START
|
||||||
|
this.records!!.clear()
|
||||||
|
this.records!!.addAll(records)
|
||||||
|
if (this.messageId != messageId) {
|
||||||
|
extended = false
|
||||||
|
}
|
||||||
|
this.messageId = messageId
|
||||||
|
displayReactions(if (extended) Int.MAX_VALUE else DEFAULT_THRESHOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||||
|
if (v.tag == null) return false
|
||||||
|
val reaction = v.tag as Reaction
|
||||||
|
val action = event.action
|
||||||
|
if (action == MotionEvent.ACTION_DOWN) onDown(MessageId(reaction.messageId, reaction.isMms)) else if (action == MotionEvent.ACTION_CANCEL) removeLongPressCallback() else if (action == MotionEvent.ACTION_UP) onUp(reaction)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayReactions(threshold: Int) {
|
||||||
|
val userPublicKey = getLocalNumber(context)
|
||||||
|
val reactions = buildSortedReactionsList(records!!, userPublicKey, threshold)
|
||||||
|
binding.layoutEmojiContainer.removeAllViews()
|
||||||
|
val overflowContainer = LinearLayout(context)
|
||||||
|
overflowContainer.orientation = LinearLayout.HORIZONTAL
|
||||||
|
val innerPadding = ViewUtil.dpToPx(4)
|
||||||
|
overflowContainer.setPaddingRelative(innerPadding, innerPadding, innerPadding, innerPadding)
|
||||||
|
val pixelSize = ViewUtil.dpToPx(1)
|
||||||
|
for (reaction in reactions) {
|
||||||
|
if (binding.layoutEmojiContainer.childCount + 1 >= DEFAULT_THRESHOLD && threshold != Int.MAX_VALUE && reactions.size > threshold) {
|
||||||
|
if (overflowContainer.parent == null) {
|
||||||
|
binding.layoutEmojiContainer.addView(overflowContainer)
|
||||||
|
val overflowParams = overflowContainer.layoutParams as MarginLayoutParams
|
||||||
|
overflowParams.height = ViewUtil.dpToPx(26)
|
||||||
|
overflowParams.setMargins(pixelSize, pixelSize, pixelSize, pixelSize)
|
||||||
|
overflowContainer.layoutParams = overflowParams
|
||||||
|
overflowContainer.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)
|
||||||
|
}
|
||||||
|
val pill = buildPill(context, this, reaction, true)
|
||||||
|
pill.setOnClickListener { v: View? ->
|
||||||
|
extended = true
|
||||||
|
displayReactions(Int.MAX_VALUE)
|
||||||
|
}
|
||||||
|
pill.findViewById<View>(R.id.reactions_pill_count).visibility = GONE
|
||||||
|
pill.findViewById<View>(R.id.reactions_pill_spacer).visibility = GONE
|
||||||
|
overflowContainer.addView(pill)
|
||||||
|
} else {
|
||||||
|
val pill = buildPill(context, this, reaction, false)
|
||||||
|
pill.tag = reaction
|
||||||
|
pill.setOnTouchListener(this)
|
||||||
|
val params = pill.layoutParams as MarginLayoutParams
|
||||||
|
params.setMargins(pixelSize, pixelSize, pixelSize, pixelSize)
|
||||||
|
pill.layoutParams = params
|
||||||
|
binding.layoutEmojiContainer.addView(pill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val overflowChildren = overflowContainer.childCount
|
||||||
|
val negativeMargin = ViewUtil.dpToPx(-8)
|
||||||
|
for (i in 0 until overflowChildren) {
|
||||||
|
val child = overflowContainer.getChildAt(i)
|
||||||
|
val childParams = child.layoutParams as MarginLayoutParams
|
||||||
|
if (i == 0 && overflowChildren > 1 || i + 1 < overflowChildren) {
|
||||||
|
// if first and there is more than one child, or we are not the last child then set negative right margin
|
||||||
|
childParams.setMargins(0, 0, negativeMargin, 0)
|
||||||
|
child.layoutParams = childParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (threshold == Int.MAX_VALUE) {
|
||||||
|
binding.groupShowLess.visibility = VISIBLE
|
||||||
|
for (id in binding.groupShowLess.referencedIds) {
|
||||||
|
findViewById<View>(id).setOnClickListener { view: View? ->
|
||||||
|
extended = false
|
||||||
|
displayReactions(DEFAULT_THRESHOLD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.groupShowLess.visibility = GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSortedReactionsList(records: List<ReactionRecord>, userPublicKey: String?, threshold: Int): List<Reaction> {
|
||||||
|
val counters: MutableMap<String, Reaction> = LinkedHashMap()
|
||||||
|
|
||||||
|
records.forEach {
|
||||||
|
val baseEmoji = EmojiUtil.getCanonicalRepresentation(it.emoji)
|
||||||
|
val info = counters[baseEmoji]
|
||||||
|
|
||||||
|
if (info == null) {
|
||||||
|
counters[baseEmoji] = Reaction(messageId, it.isMms, it.emoji, it.count, it.sortId, it.dateReceived, userPublicKey == it.author)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
info.update(it.emoji, it.count, it.dateReceived, userPublicKey == it.author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val reactions: List<Reaction> = ArrayList(counters.values)
|
||||||
|
Collections.sort(reactions, Collections.reverseOrder())
|
||||||
|
|
||||||
|
return if (reactions.size >= threshold + 2 && threshold != Int.MAX_VALUE) {
|
||||||
|
val shortened: MutableList<Reaction> = ArrayList(threshold + 2)
|
||||||
|
shortened.addAll(reactions.subList(0, threshold + 2))
|
||||||
|
shortened
|
||||||
|
} else {
|
||||||
|
reactions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildPill(context: Context, parent: ViewGroup, reaction: Reaction, isCompact: Boolean): View {
|
||||||
|
val root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false)
|
||||||
|
val emojiView = root.findViewById<EmojiImageView>(R.id.reactions_pill_emoji)
|
||||||
|
val countView = root.findViewById<TextView>(R.id.reactions_pill_count)
|
||||||
|
val spacer = root.findViewById<View>(R.id.reactions_pill_spacer)
|
||||||
|
if (isCompact) {
|
||||||
|
root.setPaddingRelative(1, 1, 1, 1)
|
||||||
|
val layoutParams = root.layoutParams
|
||||||
|
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
root.layoutParams = layoutParams
|
||||||
|
}
|
||||||
|
if (reaction.emoji != null) {
|
||||||
|
emojiView.setImageEmoji(reaction.emoji)
|
||||||
|
if (reaction.count >= 1) {
|
||||||
|
countView.text = getFormattedNumber(reaction.count)
|
||||||
|
} else {
|
||||||
|
countView.visibility = GONE
|
||||||
|
spacer.visibility = GONE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emojiView.visibility = GONE
|
||||||
|
spacer.visibility = GONE
|
||||||
|
countView.text = context.getString(R.string.ReactionsConversationView_plus, reaction.count)
|
||||||
|
}
|
||||||
|
if (reaction.userWasSender && !isCompact) {
|
||||||
|
root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)
|
||||||
|
countView.setTextColor(ThemeUtil.getThemedColor(context, R.attr.reactionsPillSelectedTextColor))
|
||||||
|
} else {
|
||||||
|
if (!isCompact) {
|
||||||
|
root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onReactionClicked(reaction: Reaction) {
|
||||||
|
if (reaction.messageId != 0L) {
|
||||||
|
val messageId = MessageId(reaction.messageId, reaction.isMms)
|
||||||
|
delegate!!.onReactionClicked(reaction.emoji!!, messageId, reaction.userWasSender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDown(messageId: MessageId) {
|
||||||
|
removeLongPressCallback()
|
||||||
|
val newLongPressCallback = Runnable {
|
||||||
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
if (delegate != null) {
|
||||||
|
delegate!!.onReactionLongClicked(messageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
longPressCallback = newLongPressCallback
|
||||||
|
gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold)
|
||||||
|
onDownTimestamp = Date().time
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeLongPressCallback() {
|
||||||
|
if (longPressCallback != null) {
|
||||||
|
gestureHandler.removeCallbacks(longPressCallback!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onUp(reaction: Reaction) {
|
||||||
|
if (Date().time - onDownTimestamp < longPressDurationThreshold) {
|
||||||
|
removeLongPressCallback()
|
||||||
|
if (pressCallback != null) {
|
||||||
|
gestureHandler.removeCallbacks(pressCallback!!)
|
||||||
|
pressCallback = null
|
||||||
|
} else {
|
||||||
|
val newPressCallback = Runnable {
|
||||||
|
onReactionClicked(reaction)
|
||||||
|
pressCallback = null
|
||||||
|
}
|
||||||
|
pressCallback = newPressCallback
|
||||||
|
gestureHandler.postDelayed(newPressCallback, maxDoubleTapInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Reaction(
|
||||||
|
internal val messageId: Long,
|
||||||
|
internal val isMms: Boolean,
|
||||||
|
internal var emoji: String?,
|
||||||
|
internal var count: Long,
|
||||||
|
internal val sortIndex: Long,
|
||||||
|
internal var lastSeen: Long,
|
||||||
|
internal var userWasSender: Boolean
|
||||||
|
) : Comparable<Reaction?> {
|
||||||
|
fun update(emoji: String, count: Long, lastSeen: Long, userWasSender: Boolean) {
|
||||||
|
if (!this.userWasSender) {
|
||||||
|
if (userWasSender || lastSeen > this.lastSeen) {
|
||||||
|
this.emoji = emoji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.count = this.count + count
|
||||||
|
this.lastSeen = Math.max(this.lastSeen, lastSeen)
|
||||||
|
this.userWasSender = this.userWasSender || userWasSender
|
||||||
|
}
|
||||||
|
|
||||||
|
fun merge(other: Reaction): Reaction {
|
||||||
|
count = count + other.count
|
||||||
|
lastSeen = Math.max(lastSeen, other.lastSeen)
|
||||||
|
userWasSender = userWasSender || other.userWasSender
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: Reaction?): Int {
|
||||||
|
if (other == null) { return -1 }
|
||||||
|
|
||||||
|
if (this.count == other.count) {
|
||||||
|
return this.sortIndex.compareTo(other.sortIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.count.compareTo(other.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,11 +4,9 @@ import android.content.Context
|
|||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewLinkPreviewBinding
|
import network.loki.messenger.databinding.ViewLinkPreviewBinding
|
||||||
@ -19,21 +17,16 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtiliti
|
|||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
|
||||||
|
|
||||||
class LinkPreviewView : LinearLayout {
|
class LinkPreviewView : LinearLayout {
|
||||||
private lateinit var binding: ViewLinkPreviewBinding
|
private val binding: ViewLinkPreviewBinding by lazy { ViewLinkPreviewBinding.bind(this) }
|
||||||
private val cornerMask by lazy { CornerMask(this) }
|
private val cornerMask by lazy { CornerMask(this) }
|
||||||
private var url: String? = null
|
private var url: String? = null
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
private fun initialize() {
|
|
||||||
binding = ViewLinkPreviewBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
}
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
@ -48,8 +41,8 @@ class LinkPreviewView : LinearLayout {
|
|||||||
// Thumbnail
|
// Thumbnail
|
||||||
if (linkPreview.getThumbnail().isPresent) {
|
if (linkPreview.getThumbnail().isPresent) {
|
||||||
// This internally fetches the thumbnail
|
// This internally fetches the thumbnail
|
||||||
binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
|
binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
|
||||||
binding.thumbnailImageView.loadIndicator.isVisible = false
|
binding.thumbnailImageView.root.loadIndicator.isVisible = false
|
||||||
}
|
}
|
||||||
// Title
|
// Title
|
||||||
binding.titleTextView.text = linkPreview.title
|
binding.titleTextView.text = linkPreview.title
|
||||||
@ -80,7 +73,7 @@ class LinkPreviewView : LinearLayout {
|
|||||||
val rawYInt = event.rawY.toInt()
|
val rawYInt = event.rawY.toInt()
|
||||||
val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
|
||||||
val previewRect = Rect()
|
val previewRect = Rect()
|
||||||
binding.mainLinkPreviewParent.getGlobalVisibleRect(previewRect)
|
binding.mainLinkPreviewContainer.getGlobalVisibleRect(previewRect)
|
||||||
if (previewRect.contains(hitRect)) {
|
if (previewRect.contains(hitRect)) {
|
||||||
openURL()
|
openURL()
|
||||||
return
|
return
|
||||||
|
@ -93,7 +93,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
val backgroundColor = context.getAccentColor()
|
val backgroundColor = context.getAccentColor()
|
||||||
binding.quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
|
binding.quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
|
||||||
binding.quoteViewAttachmentPreviewImageView.isVisible = false
|
binding.quoteViewAttachmentPreviewImageView.isVisible = false
|
||||||
binding.quoteViewAttachmentThumbnailImageView.isVisible = false
|
binding.quoteViewAttachmentThumbnailImageView.root.isVisible = false
|
||||||
when {
|
when {
|
||||||
attachments.audioSlide != null -> {
|
attachments.audioSlide != null -> {
|
||||||
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
|
||||||
@ -108,9 +108,9 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
attachments.thumbnailSlide != null -> {
|
attachments.thumbnailSlide != null -> {
|
||||||
val slide = attachments.thumbnailSlide!!
|
val slide = attachments.thumbnailSlide!!
|
||||||
// This internally fetches the thumbnail
|
// This internally fetches the thumbnail
|
||||||
binding.quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources)
|
binding.quoteViewAttachmentThumbnailImageView.root.radius = toPx(4, resources)
|
||||||
binding.quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false)
|
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false, null)
|
||||||
binding.quoteViewAttachmentThumbnailImageView.isVisible = true
|
binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true
|
||||||
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
|
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,6 @@ import network.loki.messenger.R
|
|||||||
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
@ -44,29 +42,33 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord
|
|||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil
|
import org.thoughtcrime.securesms.util.SearchUtil
|
||||||
import org.thoughtcrime.securesms.util.getAccentColor
|
import org.thoughtcrime.securesms.util.getAccentColor
|
||||||
import java.util.Locale
|
import java.util.*
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class VisibleMessageContentView : LinearLayout {
|
class VisibleMessageContentView : ConstraintLayout {
|
||||||
private lateinit var binding: ViewVisibleMessageContentBinding
|
private val binding: ViewVisibleMessageContentBinding by lazy { ViewVisibleMessageContentBinding.bind(this) }
|
||||||
var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
|
var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
|
||||||
var onContentDoubleTap: (() -> Unit)? = null
|
var onContentDoubleTap: (() -> Unit)? = null
|
||||||
var delegate: VisibleMessageViewDelegate? = null
|
var delegate: VisibleMessageViewDelegate? = null
|
||||||
var indexInAdapter: Int = -1
|
var indexInAdapter: Int = -1
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize() }
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
private fun initialize() {
|
|
||||||
binding = ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
}
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(message: MessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean,
|
fun bind(
|
||||||
glide: GlideRequests, thread: Recipient, searchQuery: String?, contactIsTrusted: Boolean) {
|
message: MessageRecord,
|
||||||
|
isStartOfMessageCluster: Boolean,
|
||||||
|
isEndOfMessageCluster: Boolean,
|
||||||
|
glide: GlideRequests,
|
||||||
|
thread: Recipient,
|
||||||
|
searchQuery: String?,
|
||||||
|
contactIsTrusted: Boolean,
|
||||||
|
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
||||||
|
) {
|
||||||
// Background
|
// Background
|
||||||
val background = getBackground(message.isOutgoing)
|
val background = getBackground(message.isOutgoing)
|
||||||
val color = if (message.isOutgoing) context.getAccentColor()
|
val color = if (message.isOutgoing) context.getAccentColor()
|
||||||
@ -80,28 +82,31 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
|
|
||||||
// reset visibilities / containers
|
// reset visibilities / containers
|
||||||
onContentClick.clear()
|
onContentClick.clear()
|
||||||
binding.albumThumbnailView.clearViews()
|
binding.albumThumbnailView.root.clearViews()
|
||||||
onContentDoubleTap = null
|
onContentDoubleTap = null
|
||||||
|
|
||||||
if (message.isDeleted) {
|
if (message.isDeleted) {
|
||||||
binding.deletedMessageView.root.isVisible = true
|
binding.deletedMessageView.root.isVisible = true
|
||||||
binding.deletedMessageView.root.bind(message, getTextColor(context, message))
|
binding.deletedMessageView.root.bind(message, getTextColor(context, message))
|
||||||
|
binding.bodyTextView.isVisible = false
|
||||||
|
binding.quoteView.root.isVisible = false
|
||||||
|
binding.linkPreviewView.root.isVisible = false
|
||||||
|
binding.untrustedView.root.isVisible = false
|
||||||
|
binding.voiceMessageView.root.isVisible = false
|
||||||
|
binding.documentView.root.isVisible = false
|
||||||
|
binding.albumThumbnailView.root.isVisible = false
|
||||||
|
binding.openGroupInvitationView.root.isVisible = false
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
binding.deletedMessageView.root.isVisible = false
|
binding.deletedMessageView.root.isVisible = false
|
||||||
}
|
}
|
||||||
// clear the
|
|
||||||
binding.bodyTextView.text = null
|
|
||||||
|
|
||||||
|
|
||||||
binding.quoteView.root.isVisible = message is MmsMessageRecord && message.quote != null
|
binding.quoteView.root.isVisible = message is MmsMessageRecord && message.quote != null
|
||||||
|
binding.linkPreviewView.root.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty()
|
||||||
binding.linkPreviewView.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty()
|
|
||||||
|
|
||||||
binding.untrustedView.root.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty()
|
binding.untrustedView.root.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty()
|
||||||
binding.voiceMessageView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
|
binding.voiceMessageView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
|
||||||
binding.documentView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
|
binding.documentView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
|
||||||
binding.albumThumbnailView.isVisible = mediaThumbnailMessage
|
binding.albumThumbnailView.root.isVisible = mediaThumbnailMessage
|
||||||
binding.openGroupInvitationView.root.isVisible = message.isOpenGroupInvitation
|
binding.openGroupInvitationView.root.isVisible = message.isOpenGroupInvitation
|
||||||
|
|
||||||
var hideBody = false
|
var hideBody = false
|
||||||
@ -133,8 +138,7 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
val attachmentId = dbAttachment.attachmentId.rowId
|
val attachmentId = dbAttachment.attachmentId.rowId
|
||||||
if (attach.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING
|
if (attach.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING
|
||||||
&& MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) {
|
&& MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) {
|
||||||
// start download
|
onAttachmentNeedsDownload(attachmentId, dbAttachment.mmsId)
|
||||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, dbAttachment.mmsId))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message.linkPreviews.forEach { preview ->
|
message.linkPreviews.forEach { preview ->
|
||||||
@ -142,15 +146,15 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
val attachmentId = previewThumbnail.attachmentId.rowId
|
val attachmentId = previewThumbnail.attachmentId.rowId
|
||||||
if (previewThumbnail.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING
|
if (previewThumbnail.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING
|
||||||
&& MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) {
|
&& MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) {
|
||||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, previewThumbnail.mmsId))
|
onAttachmentNeedsDownload(attachmentId, previewThumbnail.mmsId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
message is MmsMessageRecord && message.linkPreviews.isNotEmpty() -> {
|
message is MmsMessageRecord && message.linkPreviews.isNotEmpty() -> {
|
||||||
binding.linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
|
binding.linkPreviewView.root.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||||
onContentClick.add { event -> binding.linkPreviewView.calculateHit(event) }
|
onContentClick.add { event -> binding.linkPreviewView.root.calculateHit(event) }
|
||||||
// Body text view is inside the link preview for layout convenience
|
// Body text view is inside the link preview for layout convenience
|
||||||
}
|
}
|
||||||
message is MmsMessageRecord && message.slideDeck.audioSlide != null -> {
|
message is MmsMessageRecord && message.slideDeck.audioSlide != null -> {
|
||||||
@ -187,21 +191,21 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
if (contactIsTrusted || message.isOutgoing) {
|
if (contactIsTrusted || message.isOutgoing) {
|
||||||
// isStart and isEnd of cluster needed for calculating the mask for full bubble image groups
|
// isStart and isEnd of cluster needed for calculating the mask for full bubble image groups
|
||||||
// bind after add view because views are inflated and calculated during bind
|
// bind after add view because views are inflated and calculated during bind
|
||||||
binding.albumThumbnailView.bind(
|
binding.albumThumbnailView.root.bind(
|
||||||
glideRequests = glide,
|
glideRequests = glide,
|
||||||
message = message,
|
message = message,
|
||||||
isStart = isStartOfMessageCluster,
|
isStart = isStartOfMessageCluster,
|
||||||
isEnd = isEndOfMessageCluster
|
isEnd = isEndOfMessageCluster
|
||||||
)
|
)
|
||||||
val layoutParams = binding.albumThumbnailView.layoutParams as ConstraintLayout.LayoutParams
|
val layoutParams = binding.albumThumbnailView.root.layoutParams as ConstraintLayout.LayoutParams
|
||||||
layoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
layoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
||||||
binding.albumThumbnailView.layoutParams = layoutParams
|
binding.albumThumbnailView.root.layoutParams = layoutParams
|
||||||
onContentClick.add { event ->
|
onContentClick.add { event ->
|
||||||
binding.albumThumbnailView.calculateHitObject(event, message, thread)
|
binding.albumThumbnailView.root.calculateHitObject(event, message, thread, onAttachmentNeedsDownload)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hideBody = true
|
hideBody = true
|
||||||
binding.albumThumbnailView.clearViews()
|
binding.albumThumbnailView.root.clearViews()
|
||||||
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
|
||||||
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
||||||
}
|
}
|
||||||
@ -233,7 +237,7 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
|
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
|
||||||
listOf<View>(albumThumbnailView, linkPreviewView, voiceMessageView.root, quoteView.root).none { it.isVisible }
|
listOf<View>(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible }
|
||||||
|
|
||||||
private fun getBackground(isOutgoing: Boolean): Drawable {
|
private fun getBackground(isOutgoing: Boolean): Drawable {
|
||||||
val backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
|
val backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
|
||||||
@ -248,8 +252,8 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
binding.openGroupInvitationView.root,
|
binding.openGroupInvitationView.root,
|
||||||
binding.documentView.root,
|
binding.documentView.root,
|
||||||
binding.quoteView.root,
|
binding.quoteView.root,
|
||||||
binding.linkPreviewView,
|
binding.linkPreviewView.root,
|
||||||
binding.albumThumbnailView,
|
binding.albumThumbnailView.root,
|
||||||
binding.bodyTextView
|
binding.bodyTextView
|
||||||
).forEach { view: View -> view.isVisible = false }
|
).forEach { view: View -> view.isVisible = false }
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
var onPress: ((event: MotionEvent) -> Unit)? = null
|
var onPress: ((event: MotionEvent) -> Unit)? = null
|
||||||
var onSwipeToReply: (() -> Unit)? = null
|
var onSwipeToReply: (() -> Unit)? = null
|
||||||
var onLongPress: (() -> Unit)? = null
|
var onLongPress: (() -> Unit)? = null
|
||||||
val messageContentView: VisibleMessageContentView by lazy { binding.messageContentView }
|
val messageContentView: VisibleMessageContentView by lazy { binding.messageContentView.root }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val swipeToReplyThreshold = 64.0f // dp
|
const val swipeToReplyThreshold = 64.0f // dp
|
||||||
@ -108,7 +108,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
isHapticFeedbackEnabled = true
|
isHapticFeedbackEnabled = true
|
||||||
setWillNotDraw(false)
|
setWillNotDraw(false)
|
||||||
binding.messageInnerContainer.disableClipping()
|
binding.messageInnerContainer.disableClipping()
|
||||||
binding.messageContentView.disableClipping()
|
binding.messageContentView.root.disableClipping()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -122,6 +122,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
contact: Contact?,
|
contact: Contact?,
|
||||||
senderSessionID: String,
|
senderSessionID: String,
|
||||||
delegate: VisibleMessageViewDelegate?,
|
delegate: VisibleMessageViewDelegate?,
|
||||||
|
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
||||||
) {
|
) {
|
||||||
val threadID = message.threadId
|
val threadID = message.threadId
|
||||||
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
val thread = threadDb.getRecipientForThreadId(threadID) ?: return
|
||||||
@ -157,7 +158,8 @@ class VisibleMessageView : LinearLayout {
|
|||||||
binding.profilePictureView.root.update(message.individualRecipient)
|
binding.profilePictureView.root.update(message.individualRecipient)
|
||||||
binding.profilePictureView.root.setOnClickListener {
|
binding.profilePictureView.root.setOnClickListener {
|
||||||
if (thread.isOpenGroupRecipient) {
|
if (thread.isOpenGroupRecipient) {
|
||||||
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED) {
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
||||||
|
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
|
||||||
val intent = Intent(context, ConversationActivityV2::class.java)
|
val intent = Intent(context, ConversationActivityV2::class.java)
|
||||||
intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID)
|
intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID)
|
||||||
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderSessionID))
|
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderSessionID))
|
||||||
@ -190,7 +192,15 @@ class VisibleMessageView : LinearLayout {
|
|||||||
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
|
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
|
||||||
binding.dateBreakTextView.isVisible = showDateBreak
|
binding.dateBreakTextView.isVisible = showDateBreak
|
||||||
// Message status indicator
|
// Message status indicator
|
||||||
val (iconID, iconColor) = getMessageStatusImage(message)
|
if (message.isOutgoing) {
|
||||||
|
val (iconID, iconColor, textId) = getMessageStatusImage(message)
|
||||||
|
if (textId != null) {
|
||||||
|
binding.messageStatusTextView.setText(textId)
|
||||||
|
|
||||||
|
if (iconColor != null) {
|
||||||
|
binding.messageStatusTextView.setTextColor(iconColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (iconID != null) {
|
if (iconID != null) {
|
||||||
val drawable = ContextCompat.getDrawable(context, iconID)?.mutate()
|
val drawable = ContextCompat.getDrawable(context, iconID)?.mutate()
|
||||||
if (iconColor != null) {
|
if (iconColor != null) {
|
||||||
@ -198,42 +208,58 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
binding.messageStatusImageView.setImageDrawable(drawable)
|
binding.messageStatusImageView.setImageDrawable(drawable)
|
||||||
}
|
}
|
||||||
if (message.isOutgoing) {
|
|
||||||
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId)
|
||||||
binding.messageStatusImageView.isVisible =
|
binding.messageStatusTextView.isVisible = (
|
||||||
!message.isSent || message.id == lastMessageID
|
textId != null && (
|
||||||
|
!message.isSent ||
|
||||||
|
message.id == lastMessageID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
binding.messageStatusImageView.isVisible = (
|
||||||
|
iconID != null && (
|
||||||
|
!message.isSent ||
|
||||||
|
message.id == lastMessageID
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
|
binding.messageStatusTextView.isVisible = false
|
||||||
binding.messageStatusImageView.isVisible = false
|
binding.messageStatusImageView.isVisible = false
|
||||||
}
|
}
|
||||||
// Expiration timer
|
// Expiration timer
|
||||||
updateExpirationTimer(message)
|
updateExpirationTimer(message)
|
||||||
// Emoji Reactions
|
// Emoji Reactions
|
||||||
val emojiLayoutParams = binding.emojiReactionsView.layoutParams as ConstraintLayout.LayoutParams
|
val emojiLayoutParams = binding.emojiReactionsView.root.layoutParams as ConstraintLayout.LayoutParams
|
||||||
emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
|
||||||
binding.emojiReactionsView.layoutParams = emojiLayoutParams
|
binding.emojiReactionsView.root.layoutParams = emojiLayoutParams
|
||||||
|
|
||||||
|
if (message.reactions.isNotEmpty()) {
|
||||||
val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) }
|
val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) }
|
||||||
if (message.reactions.isNotEmpty() &&
|
if (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) {
|
||||||
(capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase()))
|
binding.emojiReactionsView.root.setReactions(message.id, message.reactions, message.isOutgoing, delegate)
|
||||||
) {
|
binding.emojiReactionsView.root.isVisible = true
|
||||||
binding.emojiReactionsView.setReactions(message.id, message.reactions, message.isOutgoing, delegate)
|
|
||||||
binding.emojiReactionsView.isVisible = true
|
|
||||||
} else {
|
} else {
|
||||||
binding.emojiReactionsView.isVisible = false
|
binding.emojiReactionsView.root.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
binding.emojiReactionsView.root.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate content view
|
// Populate content view
|
||||||
binding.messageContentView.indexInAdapter = indexInAdapter
|
binding.messageContentView.root.indexInAdapter = indexInAdapter
|
||||||
binding.messageContentView.bind(
|
binding.messageContentView.root.bind(
|
||||||
message,
|
message,
|
||||||
isStartOfMessageCluster,
|
isStartOfMessageCluster,
|
||||||
isEndOfMessageCluster,
|
isEndOfMessageCluster,
|
||||||
glide,
|
glide,
|
||||||
thread,
|
thread,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false)
|
message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false),
|
||||||
|
onAttachmentNeedsDownload
|
||||||
)
|
)
|
||||||
binding.messageContentView.delegate = delegate
|
binding.messageContentView.root.delegate = delegate
|
||||||
onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() }
|
onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean {
|
private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean {
|
||||||
@ -256,19 +282,23 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMessageStatusImage(message: MessageRecord): Pair<Int?,Int?> {
|
private fun getMessageStatusImage(message: MessageRecord): Triple<Int?,Int?,Int?> {
|
||||||
return when {
|
return when {
|
||||||
!message.isOutgoing -> null to null
|
!message.isOutgoing -> Triple(null, null, null)
|
||||||
message.isFailed -> R.drawable.ic_error to resources.getColor(R.color.destructive, context.theme)
|
message.isFailed ->
|
||||||
message.isPending -> R.drawable.ic_circle_dot_dot_dot to null
|
Triple(R.drawable.ic_delivery_status_failed, resources.getColor(R.color.destructive, context.theme), R.string.delivery_status_failed)
|
||||||
message.isRead -> R.drawable.ic_filled_circle_check to null
|
message.isPending ->
|
||||||
else -> R.drawable.ic_circle_check to null
|
Triple(R.drawable.ic_delivery_status_sending, context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sending)
|
||||||
|
message.isRead ->
|
||||||
|
Triple(R.drawable.ic_delivery_status_read, context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_read)
|
||||||
|
else ->
|
||||||
|
Triple(R.drawable.ic_delivery_status_sent, context.getColorFromAttr(R.attr.message_status_color), R.string.delivery_status_sent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateExpirationTimer(message: MessageRecord) {
|
private fun updateExpirationTimer(message: MessageRecord) {
|
||||||
val container = binding.messageInnerContainer
|
val container = binding.messageInnerContainer
|
||||||
val content = binding.messageContentView
|
val content = binding.messageContentView.root
|
||||||
val expiration = binding.expirationTimerView
|
val expiration = binding.expirationTimerView
|
||||||
val spacing = binding.messageContentSpacing
|
val spacing = binding.messageContentSpacing
|
||||||
container.removeAllViewsInLayout()
|
container.removeAllViewsInLayout()
|
||||||
@ -319,7 +349,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
|
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
|
||||||
val iconSize = toPx(24, context.resources)
|
val iconSize = toPx(24, context.resources)
|
||||||
val left = binding.messageInnerContainer.left + binding.messageContentView.right + spacing
|
val left = binding.messageInnerContainer.left + binding.messageContentView.root.right + spacing
|
||||||
val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2)
|
val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2)
|
||||||
val right = left + iconSize
|
val right = left + iconSize
|
||||||
val bottom = top + iconSize
|
val bottom = top + iconSize
|
||||||
@ -341,7 +371,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
binding.profilePictureView.root.recycle()
|
binding.profilePictureView.root.recycle()
|
||||||
binding.messageContentView.recycle()
|
binding.messageContentView.root.recycle()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -437,7 +467,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onContentClick(event: MotionEvent) {
|
fun onContentClick(event: MotionEvent) {
|
||||||
binding.messageContentView.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) }
|
binding.messageContentView.root.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPress(event: MotionEvent) {
|
private fun onPress(event: MotionEvent) {
|
||||||
@ -457,7 +487,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun playVoiceMessage() {
|
fun playVoiceMessage() {
|
||||||
binding.messageContentView.playVoiceMessage()
|
binding.messageContentView.root.playVoiceMessage()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener {
|
|||||||
binding.voiceMessageViewDurationTextView.visibility = View.VISIBLE
|
binding.voiceMessageViewDurationTextView.visibility = View.VISIBLE
|
||||||
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||||
TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
|
TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
|
||||||
TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs))
|
TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs) % 60)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener {
|
|||||||
this.progress = progress
|
this.progress = progress
|
||||||
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
|
||||||
TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()),
|
TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()),
|
||||||
TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong()))
|
TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong()) % 60)
|
||||||
val layoutParams = binding.progressView.layoutParams as RelativeLayout.LayoutParams
|
val layoutParams = binding.progressView.layoutParams as RelativeLayout.LayoutParams
|
||||||
layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt()
|
layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt()
|
||||||
binding.progressView.layoutParams = layoutParams
|
binding.progressView.layoutParams = layoutParams
|
||||||
|
@ -1,425 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.utilities;
|
|
||||||
|
|
||||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.UiThread;
|
|
||||||
|
|
||||||
import com.bumptech.glide.RequestBuilder;
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
|
|
||||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
|
||||||
import com.bumptech.glide.load.resource.bitmap.FitCenter;
|
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.session.libsignal.utilities.ListenableFuture;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.session.libsignal.utilities.SettableFuture;
|
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
|
||||||
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget;
|
|
||||||
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget;
|
|
||||||
import org.thoughtcrime.securesms.components.TransferControlView;
|
|
||||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class ThumbnailView extends FrameLayout {
|
|
||||||
|
|
||||||
private static final String TAG = ThumbnailView.class.getSimpleName();
|
|
||||||
private static final int WIDTH = 0;
|
|
||||||
private static final int HEIGHT = 1;
|
|
||||||
private static final int MIN_WIDTH = 0;
|
|
||||||
private static final int MAX_WIDTH = 1;
|
|
||||||
private static final int MIN_HEIGHT = 2;
|
|
||||||
private static final int MAX_HEIGHT = 3;
|
|
||||||
|
|
||||||
private ImageView image;
|
|
||||||
private View playOverlay;
|
|
||||||
private View loadIndicator;
|
|
||||||
private OnClickListener parentClickListener;
|
|
||||||
|
|
||||||
private final int[] dimens = new int[2];
|
|
||||||
private final int[] bounds = new int[4];
|
|
||||||
private final int[] measureDimens = new int[2];
|
|
||||||
|
|
||||||
private Optional<TransferControlView> transferControls = Optional.absent();
|
|
||||||
private SlideClickListener thumbnailClickListener = null;
|
|
||||||
private SlidesClickedListener downloadClickListener = null;
|
|
||||||
private Slide slide = null;
|
|
||||||
|
|
||||||
public int radius;
|
|
||||||
|
|
||||||
public ThumbnailView(Context context) {
|
|
||||||
this(context, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ThumbnailView(Context context, AttributeSet attrs) {
|
|
||||||
this(context, attrs, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ThumbnailView(final Context context, AttributeSet attrs, int defStyle) {
|
|
||||||
super(context, attrs, defStyle);
|
|
||||||
|
|
||||||
inflate(context, R.layout.thumbnail_view, this);
|
|
||||||
|
|
||||||
this.image = findViewById(R.id.thumbnail_image);
|
|
||||||
this.playOverlay = findViewById(R.id.play_overlay);
|
|
||||||
this.loadIndicator = findViewById(R.id.thumbnail_load_indicator);
|
|
||||||
super.setOnClickListener(new ThumbnailClickDispatcher());
|
|
||||||
|
|
||||||
if (attrs != null) {
|
|
||||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0);
|
|
||||||
bounds[MIN_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0);
|
|
||||||
bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0);
|
|
||||||
bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0);
|
|
||||||
bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0);
|
|
||||||
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0);
|
|
||||||
typedArray.recycle();
|
|
||||||
} else {
|
|
||||||
radius = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onMeasure(int originalWidthMeasureSpec, int originalHeightMeasureSpec) {
|
|
||||||
fillTargetDimensions(measureDimens, dimens, bounds);
|
|
||||||
if (measureDimens[WIDTH] == 0 && measureDimens[HEIGHT] == 0) {
|
|
||||||
super.onMeasure(originalWidthMeasureSpec, originalHeightMeasureSpec);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int finalWidth = measureDimens[WIDTH] + getPaddingLeft() + getPaddingRight();
|
|
||||||
int finalHeight = measureDimens[HEIGHT] + getPaddingTop() + getPaddingBottom();
|
|
||||||
|
|
||||||
super.onMeasure(MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
|
|
||||||
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("SuspiciousNameCombination")
|
|
||||||
private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) {
|
|
||||||
int dimensFilledCount = getNonZeroCount(dimens);
|
|
||||||
int boundsFilledCount = getNonZeroCount(bounds);
|
|
||||||
|
|
||||||
if (dimensFilledCount == 0 || boundsFilledCount == 0) {
|
|
||||||
targetDimens[WIDTH] = 0;
|
|
||||||
targetDimens[HEIGHT] = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
double naturalWidth = dimens[WIDTH];
|
|
||||||
double naturalHeight = dimens[HEIGHT];
|
|
||||||
|
|
||||||
int minWidth = bounds[MIN_WIDTH];
|
|
||||||
int maxWidth = bounds[MAX_WIDTH];
|
|
||||||
int minHeight = bounds[MIN_HEIGHT];
|
|
||||||
int maxHeight = bounds[MAX_HEIGHT];
|
|
||||||
|
|
||||||
if (dimensFilledCount > 0 && dimensFilledCount < dimens.length) {
|
|
||||||
throw new IllegalStateException(String.format(Locale.ENGLISH, "Width or height has been specified, but not both. Dimens: %f x %f",
|
|
||||||
naturalWidth, naturalHeight));
|
|
||||||
}
|
|
||||||
if (boundsFilledCount > 0 && boundsFilledCount < bounds.length) {
|
|
||||||
throw new IllegalStateException(String.format(Locale.ENGLISH, "One or more min/max dimensions have been specified, but not all. Bounds: [%d, %d, %d, %d]",
|
|
||||||
minWidth, maxWidth, minHeight, maxHeight));
|
|
||||||
}
|
|
||||||
|
|
||||||
double measuredWidth = naturalWidth;
|
|
||||||
double measuredHeight = naturalHeight;
|
|
||||||
|
|
||||||
boolean widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth;
|
|
||||||
boolean heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight;
|
|
||||||
|
|
||||||
if (!widthInBounds || !heightInBounds) {
|
|
||||||
double minWidthRatio = naturalWidth / minWidth;
|
|
||||||
double maxWidthRatio = naturalWidth / maxWidth;
|
|
||||||
double minHeightRatio = naturalHeight / minHeight;
|
|
||||||
double maxHeightRatio = naturalHeight / maxHeight;
|
|
||||||
|
|
||||||
if (maxWidthRatio > 1 || maxHeightRatio > 1) {
|
|
||||||
if (maxWidthRatio >= maxHeightRatio) {
|
|
||||||
measuredWidth /= maxWidthRatio;
|
|
||||||
measuredHeight /= maxWidthRatio;
|
|
||||||
} else {
|
|
||||||
measuredWidth /= maxHeightRatio;
|
|
||||||
measuredHeight /= maxHeightRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
measuredWidth = Math.max(measuredWidth, minWidth);
|
|
||||||
measuredHeight = Math.max(measuredHeight, minHeight);
|
|
||||||
|
|
||||||
} else if (minWidthRatio < 1 || minHeightRatio < 1) {
|
|
||||||
if (minWidthRatio <= minHeightRatio) {
|
|
||||||
measuredWidth /= minWidthRatio;
|
|
||||||
measuredHeight /= minWidthRatio;
|
|
||||||
} else {
|
|
||||||
measuredWidth /= minHeightRatio;
|
|
||||||
measuredHeight /= minHeightRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
measuredWidth = Math.min(measuredWidth, maxWidth);
|
|
||||||
measuredHeight = Math.min(measuredHeight, maxHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
targetDimens[WIDTH] = (int) measuredWidth;
|
|
||||||
targetDimens[HEIGHT] = (int) measuredHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getNonZeroCount(int[] vals) {
|
|
||||||
int count = 0;
|
|
||||||
for (int val : vals) {
|
|
||||||
if (val > 0) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setOnClickListener(OnClickListener l) {
|
|
||||||
parentClickListener = l;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setFocusable(boolean focusable) {
|
|
||||||
super.setFocusable(focusable);
|
|
||||||
if (transferControls.isPresent()) transferControls.get().setFocusable(focusable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setClickable(boolean clickable) {
|
|
||||||
super.setClickable(clickable);
|
|
||||||
if (transferControls.isPresent()) transferControls.get().setClickable(clickable);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TransferControlView getTransferControls() {
|
|
||||||
if (!transferControls.isPresent()) {
|
|
||||||
transferControls = Optional.of(ViewUtil.inflateStub(this, R.id.transfer_controls_stub));
|
|
||||||
}
|
|
||||||
return transferControls.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBounds(int minWidth, int maxWidth, int minHeight, int maxHeight) {
|
|
||||||
bounds[MIN_WIDTH] = minWidth;
|
|
||||||
bounds[MAX_WIDTH] = maxWidth;
|
|
||||||
bounds[MIN_HEIGHT] = minHeight;
|
|
||||||
bounds[MAX_HEIGHT] = maxHeight;
|
|
||||||
|
|
||||||
forceLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
|
|
||||||
boolean showControls, boolean isPreview)
|
|
||||||
{
|
|
||||||
return setImageResource(glideRequests, slide, showControls, isPreview, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
|
|
||||||
boolean showControls, boolean isPreview,
|
|
||||||
int naturalWidth, int naturalHeight)
|
|
||||||
{
|
|
||||||
if (showControls) {
|
|
||||||
getTransferControls().setSlide(slide);
|
|
||||||
getTransferControls().setDownloadClickListener(new DownloadClickDispatcher());
|
|
||||||
} else if (transferControls.isPresent()) {
|
|
||||||
getTransferControls().setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slide.getThumbnailUri() != null && slide.hasPlayOverlay() &&
|
|
||||||
(slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
|
|
||||||
{
|
|
||||||
this.playOverlay.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
this.playOverlay.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Util.equals(slide, this.slide)) {
|
|
||||||
Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri());
|
|
||||||
return new SettableFuture<>(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.slide != null && this.slide.getFastPreflightId() != null &&
|
|
||||||
this.slide.getFastPreflightId().equals(slide.getFastPreflightId()))
|
|
||||||
{
|
|
||||||
Log.i(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId());
|
|
||||||
this.slide = slide;
|
|
||||||
return new SettableFuture<>(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "loading part with id " + slide.asAttachment().getDataUri()
|
|
||||||
+ ", progress " + slide.getTransferState() + ", fast preflight id: " +
|
|
||||||
slide.asAttachment().getFastPreflightId());
|
|
||||||
|
|
||||||
this.slide = slide;
|
|
||||||
|
|
||||||
dimens[WIDTH] = naturalWidth;
|
|
||||||
dimens[HEIGHT] = naturalHeight;
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
SettableFuture<Boolean> result = new SettableFuture<>();
|
|
||||||
|
|
||||||
if (slide.getThumbnailUri() != null) {
|
|
||||||
buildThumbnailGlideRequest(glideRequests, slide).into(new GlideDrawableListeningTarget(image, result));
|
|
||||||
} else if (slide.hasPlaceholder()) {
|
|
||||||
buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(image, result));
|
|
||||||
} else {
|
|
||||||
glideRequests.load(R.drawable.ic_image_white_24dp).centerInside().into(image);
|
|
||||||
result.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
|
|
||||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
|
||||||
|
|
||||||
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
|
|
||||||
|
|
||||||
GlideRequest request = glideRequests.load(new DecryptableUri(uri))
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.transition(withCrossFade());
|
|
||||||
|
|
||||||
if (radius > 0) {
|
|
||||||
request = request.transforms(new CenterCrop(), new RoundedCorners(radius));
|
|
||||||
} else {
|
|
||||||
request = request.transforms(new CenterCrop());
|
|
||||||
}
|
|
||||||
|
|
||||||
request.into(new GlideDrawableListeningTarget(image, future));
|
|
||||||
|
|
||||||
return future;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setThumbnailClickListener(SlideClickListener listener) {
|
|
||||||
this.thumbnailClickListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDownloadClickListener(SlidesClickedListener listener) {
|
|
||||||
this.downloadClickListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear(GlideRequests glideRequests) {
|
|
||||||
glideRequests.clear(image);
|
|
||||||
|
|
||||||
if (transferControls.isPresent()) {
|
|
||||||
getTransferControls().clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
slide = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showDownloadText(boolean showDownloadText) {
|
|
||||||
getTransferControls().setShowDownloadText(showDownloadText);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showProgressSpinner() {
|
|
||||||
getTransferControls().showProgressSpinner();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLoadIndicatorVisibile(boolean visible) {
|
|
||||||
this.loadIndicator.setVisibility(visible ? VISIBLE : GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setRadius(int radius) {
|
|
||||||
this.radius = radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
|
||||||
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.transition(withCrossFade()), new CenterCrop());
|
|
||||||
|
|
||||||
if (slide.isInProgress()) return request;
|
|
||||||
else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
|
|
||||||
}
|
|
||||||
|
|
||||||
private RequestBuilder buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
|
||||||
return applySizing(glideRequests.asBitmap()
|
|
||||||
.load(slide.getPlaceholderRes(getContext().getTheme()))
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE), new FitCenter());
|
|
||||||
}
|
|
||||||
|
|
||||||
private GlideRequest applySizing(@NonNull GlideRequest request, @NonNull BitmapTransformation fitting) {
|
|
||||||
int[] size = new int[2];
|
|
||||||
fillTargetDimensions(size, dimens, bounds);
|
|
||||||
if (size[WIDTH] == 0 && size[HEIGHT] == 0) {
|
|
||||||
size[WIDTH] = getDefaultWidth();
|
|
||||||
size[HEIGHT] = getDefaultHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
request = request.override(size[WIDTH], size[HEIGHT]);
|
|
||||||
|
|
||||||
if (radius > 0) {
|
|
||||||
return request.transforms(fitting, new RoundedCorners(radius));
|
|
||||||
} else {
|
|
||||||
return request.transforms(fitting);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getDefaultWidth() {
|
|
||||||
ViewGroup.LayoutParams params = getLayoutParams();
|
|
||||||
if (params != null) {
|
|
||||||
return Math.max(params.width, 0);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getDefaultHeight() {
|
|
||||||
ViewGroup.LayoutParams params = getLayoutParams();
|
|
||||||
if (params != null) {
|
|
||||||
return Math.max(params.height, 0);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ThumbnailClickDispatcher implements View.OnClickListener {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
if (thumbnailClickListener != null &&
|
|
||||||
slide != null &&
|
|
||||||
slide.asAttachment().getDataUri() != null &&
|
|
||||||
slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE)
|
|
||||||
{
|
|
||||||
thumbnailClickListener.onClick(view, slide);
|
|
||||||
} else if (parentClickListener != null) {
|
|
||||||
parentClickListener.onClick(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DownloadClickDispatcher implements View.OnClickListener {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
if (downloadClickListener != null && slide != null) {
|
|
||||||
downloadClickListener.onClick(view, Collections.singletonList(slide));
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + String.valueOf(slide) + " downloadClickListener: " + String.valueOf(downloadClickListener));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,14 +2,11 @@ package org.thoughtcrime.securesms.conversation.v2.utilities
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||||
@ -29,31 +26,33 @@ import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
|||||||
import org.thoughtcrime.securesms.mms.GlideRequest
|
import org.thoughtcrime.securesms.mms.GlideRequest
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.mms.Slide
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
|
import kotlin.Boolean
|
||||||
|
import kotlin.Int
|
||||||
|
import kotlin.getValue
|
||||||
|
import kotlin.lazy
|
||||||
|
import kotlin.let
|
||||||
|
|
||||||
open class KThumbnailView: FrameLayout {
|
open class ThumbnailView: FrameLayout {
|
||||||
private lateinit var binding: ThumbnailViewBinding
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val WIDTH = 0
|
private const val WIDTH = 0
|
||||||
private const val HEIGHT = 1
|
private const val HEIGHT = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val binding: ThumbnailViewBinding by lazy { ThumbnailViewBinding.bind(this) }
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) { initialize(null) }
|
constructor(context: Context) : super(context) { initialize(null) }
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
||||||
|
|
||||||
private val image by lazy { binding.thumbnailImage }
|
|
||||||
private val playOverlay by lazy { binding.playOverlay }
|
|
||||||
val loadIndicator: View by lazy { binding.thumbnailLoadIndicator }
|
val loadIndicator: View by lazy { binding.thumbnailLoadIndicator }
|
||||||
val downloadIndicator: View by lazy { binding.thumbnailDownloadIcon }
|
|
||||||
|
|
||||||
private val dimensDelegate = ThumbnailDimensDelegate()
|
private val dimensDelegate = ThumbnailDimensDelegate()
|
||||||
|
|
||||||
private var slide: Slide? = null
|
private var slide: Slide? = null
|
||||||
private var radius: Int = 0
|
var radius: Int = 0
|
||||||
|
|
||||||
private fun initialize(attrs: AttributeSet?) {
|
private fun initialize(attrs: AttributeSet?) {
|
||||||
binding = ThumbnailViewBinding.inflate(LayoutInflater.from(context), this)
|
|
||||||
if (attrs != null) {
|
if (attrs != null) {
|
||||||
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
|
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
|
||||||
|
|
||||||
@ -66,8 +65,6 @@ open class KThumbnailView: FrameLayout {
|
|||||||
|
|
||||||
typedArray.recycle()
|
typedArray.recycle()
|
||||||
}
|
}
|
||||||
val background = ContextCompat.getColor(context, R.color.transparent_black_6)
|
|
||||||
binding.root.background = ColorDrawable(background)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
@ -90,17 +87,17 @@ open class KThumbnailView: FrameLayout {
|
|||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Interaction
|
// region Interaction
|
||||||
fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord): ListenableFuture<Boolean> {
|
fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord?): ListenableFuture<Boolean> {
|
||||||
return setImageResource(glide, slide, isPreview, 0, 0, mms)
|
return setImageResource(glide, slide, isPreview, 0, 0, mms)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setImageResource(glide: GlideRequests, slide: Slide,
|
fun setImageResource(glide: GlideRequests, slide: Slide,
|
||||||
isPreview: Boolean, naturalWidth: Int,
|
isPreview: Boolean, naturalWidth: Int,
|
||||||
naturalHeight: Int, mms: MmsMessageRecord): ListenableFuture<Boolean> {
|
naturalHeight: Int, mms: MmsMessageRecord?): ListenableFuture<Boolean> {
|
||||||
|
|
||||||
val currentSlide = this.slide
|
val currentSlide = this.slide
|
||||||
|
|
||||||
playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
|
binding.playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
|
||||||
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
|
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
|
||||||
|
|
||||||
if (equals(currentSlide, slide)) {
|
if (equals(currentSlide, slide)) {
|
||||||
@ -116,8 +113,8 @@ open class KThumbnailView: FrameLayout {
|
|||||||
|
|
||||||
this.slide = slide
|
this.slide = slide
|
||||||
|
|
||||||
loadIndicator.isVisible = slide.isInProgress
|
binding.thumbnailLoadIndicator.isVisible = slide.isInProgress
|
||||||
downloadIndicator.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED
|
binding.thumbnailDownloadIcon.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED
|
||||||
|
|
||||||
dimensDelegate.setDimens(naturalWidth, naturalHeight)
|
dimensDelegate.setDimens(naturalWidth, naturalHeight)
|
||||||
invalidate()
|
invalidate()
|
||||||
@ -126,13 +123,13 @@ open class KThumbnailView: FrameLayout {
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
slide.thumbnailUri != null -> {
|
slide.thumbnailUri != null -> {
|
||||||
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(image, result))
|
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, result))
|
||||||
}
|
}
|
||||||
slide.hasPlaceholder() -> {
|
slide.hasPlaceholder() -> {
|
||||||
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(image, result))
|
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, result))
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
glide.clear(image)
|
glide.clear(binding.thumbnailImage)
|
||||||
result.set(false)
|
result.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,7 +173,7 @@ open class KThumbnailView: FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open fun clear(glideRequests: GlideRequests) {
|
open fun clear(glideRequests: GlideRequests) {
|
||||||
glideRequests.clear(image)
|
glideRequests.clear(binding.thumbnailImage)
|
||||||
slide = null
|
slide = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,11 +190,8 @@ open class KThumbnailView: FrameLayout {
|
|||||||
request.transforms(CenterCrop())
|
request.transforms(CenterCrop())
|
||||||
}
|
}
|
||||||
|
|
||||||
request.into(GlideDrawableListeningTarget(image, future))
|
request.into(GlideDrawableListeningTarget(binding.thumbnailImage, future))
|
||||||
|
|
||||||
return future
|
return future
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
}
|
}
|
@ -33,8 +33,9 @@ import androidx.annotation.VisibleForTesting;
|
|||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||||
@ -318,6 +319,28 @@ public class AttachmentDatabase extends Database {
|
|||||||
notifyAttachmentListeners();
|
notifyAttachmentListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
|
void deleteAttachmentsForMessages(long[] mmsIds) {
|
||||||
|
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||||
|
Cursor cursor = null;
|
||||||
|
String mmsIdString = StringUtils.join(mmsIds, ',');
|
||||||
|
|
||||||
|
try {
|
||||||
|
cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL, CONTENT_TYPE}, MMS_ID + " IN (?)",
|
||||||
|
new String[] {mmsIdString}, null, null, null);
|
||||||
|
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
deleteAttachmentOnDisk(cursor.getString(0), cursor.getString(1), cursor.getString(2));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (cursor != null)
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
database.delete(TABLE_NAME, MMS_ID + " IN (?)", new String[] {mmsIdString});
|
||||||
|
notifyAttachmentListeners();
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteAttachment(@NonNull AttachmentId id) {
|
public void deleteAttachment(@NonNull AttachmentId id) {
|
||||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ import android.database.Cursor;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.session.libsession.utilities.WindowDebouncer;
|
import org.session.libsession.utilities.WindowDebouncer;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
|
@ -19,7 +19,7 @@ package org.thoughtcrime.securesms.database;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package org.thoughtcrime.securesms.database
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import net.sqlcipher.Cursor
|
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||||
import net.sqlcipher.database.SQLiteDatabase
|
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
|
|
||||||
fun <T> SQLiteDatabase.get(table: String, query: String?, arguments: Array<String>?, get: (Cursor) -> T): T? {
|
fun <T> SQLiteDatabase.get(table: String, query: String?, arguments: Array<String>?, get: (Cursor) -> T): T? {
|
||||||
|
@ -6,7 +6,7 @@ import android.database.Cursor;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.thoughtcrime.securesms.database;
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -12,7 +11,7 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
@ -319,6 +318,19 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
|||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasDownloadedProfilePicture(String groupId) {
|
||||||
|
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[]{AVATAR}, GROUP_ID + " = ?",
|
||||||
|
new String[] {groupId},
|
||||||
|
null, null, null))
|
||||||
|
{
|
||||||
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
|
return !cursor.isNull(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updateMembers(String groupId, List<Address> members) {
|
public void updateMembers(String groupId, List<Address> members) {
|
||||||
Collections.sort(members);
|
Collections.sort(members);
|
||||||
|
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package org.thoughtcrime.securesms.database;
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
@ -110,6 +110,11 @@ public class GroupReceiptDatabase extends Database {
|
|||||||
db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)});
|
db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void deleteRowsForMessages(long[] mmsIds) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
db.delete(TABLE_NAME, MMS_ID + " IN (?)", new String[] {StringUtils.join(mmsIds, ',')});
|
||||||
|
}
|
||||||
|
|
||||||
void deleteAllRows() {
|
void deleteAllRows() {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
db.delete(TABLE_NAME, null, null);
|
db.delete(TABLE_NAME, null, null);
|
||||||
|
@ -5,7 +5,7 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||||
|
@ -300,6 +300,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
val lastHash = database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey, namespace.toString() ))
|
val lastHash = database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey, namespace.toString() ))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun clearAllLastMessageHashes() {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
database.delete(lastMessageHashValueTable2, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getReceivedMessageHashValues(publicKey: String, namespace: Int): Set<String>? {
|
override fun getReceivedMessageHashValues(publicKey: String, namespace: Int): Set<String>? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
val query = "${Companion.publicKey} = ? AND ${Companion.receivedMessageHashNamespace} = ?"
|
val query = "${Companion.publicKey} = ? AND ${Companion.receivedMessageHashNamespace} = ?"
|
||||||
@ -321,6 +326,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
database.insertOrUpdate(receivedMessageHashValuesTable, row, query, arrayOf( publicKey, namespace.toString() ))
|
database.insertOrUpdate(receivedMessageHashValuesTable, row, query, arrayOf( publicKey, namespace.toString() ))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun clearReceivedMessageHashValues() {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
database.delete(receivedMessageHashValuesTable, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAuthToken(server: String): String? {
|
override fun getAuthToken(server: String): String? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor ->
|
return database.get(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor ->
|
||||||
@ -339,7 +349,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getLastMessageServerID(room: String, server: String): Long? {
|
override fun getLastMessageServerID(room: String, server: String): Long? {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
val index = "$server.$room"
|
val index = "$server.$room"
|
||||||
return database.get(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) { cursor ->
|
return database.get(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) { cursor ->
|
||||||
cursor.getInt(lastMessageServerID)
|
cursor.getInt(lastMessageServerID)
|
||||||
@ -510,7 +520,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getServerCapabilities(serverName: String): List<String> {
|
fun getServerCapabilities(serverName: String): List<String> {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(serverCapabilitiesTable, "$server = ?", wrap(serverName)) { cursor ->
|
return database.get(serverCapabilitiesTable, "$server = ?", wrap(serverName)) { cursor ->
|
||||||
cursor.getString(capabilities)
|
cursor.getString(capabilities)
|
||||||
}?.split(",") ?: emptyList()
|
}?.split(",") ?: emptyList()
|
||||||
@ -523,7 +533,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getLastInboxMessageId(serverName: String): Long? {
|
fun getLastInboxMessageId(serverName: String): Long? {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(lastInboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor ->
|
return database.get(lastInboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor ->
|
||||||
cursor.getInt(lastInboxMessageServerId)
|
cursor.getInt(lastInboxMessageServerId)
|
||||||
}?.toLong()
|
}?.toLong()
|
||||||
@ -540,7 +550,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getLastOutboxMessageId(serverName: String): Long? {
|
fun getLastOutboxMessageId(serverName: String): Long? {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(lastOutboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor ->
|
return database.get(lastOutboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor ->
|
||||||
cursor.getInt(lastOutboxMessageServerId)
|
cursor.getInt(lastOutboxMessageServerId)
|
||||||
}?.toLong()
|
}?.toLong()
|
||||||
|
@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database
|
|||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE
|
import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE
|
||||||
import org.session.libsignal.database.LokiMessageDatabaseProtocol
|
import org.session.libsignal.database.LokiMessageDatabaseProtocol
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
|
|
||||||
@ -77,6 +77,25 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
database.endTransaction()
|
database.endTransaction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteMessages(messageIDs: List<Long>) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
database.beginTransaction()
|
||||||
|
|
||||||
|
database.delete(
|
||||||
|
messageIDTable,
|
||||||
|
"${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})",
|
||||||
|
messageIDs.map { "$it" }.toTypedArray()
|
||||||
|
)
|
||||||
|
database.delete(
|
||||||
|
messageThreadMappingTable,
|
||||||
|
"${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})",
|
||||||
|
messageIDs.map { "$it" }.toTypedArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
database.setTransactionSuccessful()
|
||||||
|
database.endTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return pair of sms or mms table-specific ID and whether it is in SMS table
|
* @return pair of sms or mms table-specific ID and whether it is in SMS table
|
||||||
*/
|
*/
|
||||||
@ -96,6 +115,37 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>> {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
|
||||||
|
// Retrieve the message ids
|
||||||
|
val messageIdCursor = database
|
||||||
|
.rawQuery(
|
||||||
|
"""
|
||||||
|
SELECT ${messageThreadMappingTable}.${messageID}, ${messageIDTable}.${messageType}
|
||||||
|
FROM ${messageThreadMappingTable}
|
||||||
|
JOIN ${messageIDTable} ON ${messageIDTable}.message_id = ${messageThreadMappingTable}.${messageID}
|
||||||
|
WHERE (
|
||||||
|
${messageThreadMappingTable}.${Companion.threadID} = $threadID AND
|
||||||
|
${messageThreadMappingTable}.${Companion.serverID} IN (${serverIDs.joinToString(",")})
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
val smsMessageIds: MutableList<Long> = mutableListOf()
|
||||||
|
val mmsMessageIds: MutableList<Long> = mutableListOf()
|
||||||
|
while (messageIdCursor.moveToNext()) {
|
||||||
|
if (messageIdCursor.getInt(1) == SMS_TYPE) {
|
||||||
|
smsMessageIds.add(messageIdCursor.getLong(0))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
mmsMessageIds.add(messageIdCursor.getLong(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(smsMessageIds, mmsMessageIds)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) {
|
override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(3)
|
val contentValues = ContentValues(3)
|
||||||
@ -136,6 +186,11 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
database.insertOrUpdate(errorMessageTable, contentValues, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
database.insertOrUpdate(errorMessageTable, contentValues, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearErrorMessage(messageID: Long) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
database.delete(errorMessageTable, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
fun deleteThread(threadId: Long) {
|
fun deleteThread(threadId: Long) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
try {
|
try {
|
||||||
@ -178,6 +233,15 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
database.delete(messageHashTable, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
database.delete(messageHashTable, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteMessageServerHashes(messageIDs: List<Long>) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
database.delete(
|
||||||
|
messageHashTable,
|
||||||
|
"${Companion.messageID} IN (${messageIDs.map { "?" }.joinToString(",")})",
|
||||||
|
messageIDs.map { "$it" }.toTypedArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun migrateThreadId(legacyThreadId: Long, newThreadId: Long) {
|
fun migrateThreadId(legacyThreadId: Long, newThreadId: Long) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(1)
|
val contentValues = ContentValues(1)
|
||||||
|
@ -7,7 +7,7 @@ import android.database.Cursor;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
|
@ -5,7 +5,7 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.Document;
|
import org.session.libsession.utilities.Document;
|
||||||
@ -42,6 +42,7 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
|||||||
public abstract void markAsDeleted(long messageId, boolean read);
|
public abstract void markAsDeleted(long messageId, boolean read);
|
||||||
|
|
||||||
public abstract boolean deleteMessage(long messageId);
|
public abstract boolean deleteMessage(long messageId);
|
||||||
|
public abstract boolean deleteMessages(long[] messageId, long threadId);
|
||||||
|
|
||||||
public abstract void updateThreadId(long fromId, long toId);
|
public abstract void updateThreadId(long fromId, long toId);
|
||||||
|
|
||||||
|
@ -995,6 +995,23 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
return threadDeleted
|
return threadDeleted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun deleteMessages(messageIds: LongArray, threadId: Long): Boolean {
|
||||||
|
val attachmentDatabase = get(context).attachmentDatabase()
|
||||||
|
val groupReceiptDatabase = get(context).groupReceiptDatabase()
|
||||||
|
|
||||||
|
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessages(messageIds) })
|
||||||
|
groupReceiptDatabase.deleteRowsForMessages(messageIds)
|
||||||
|
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
database!!.delete(TABLE_NAME, ID_IN, arrayOf(messageIds.joinToString(",")))
|
||||||
|
|
||||||
|
val threadDeleted = get(context).threadDatabase().update(threadId, false)
|
||||||
|
notifyConversationListeners(threadId)
|
||||||
|
notifyStickerListeners()
|
||||||
|
notifyStickerPackListeners()
|
||||||
|
return threadDeleted
|
||||||
|
}
|
||||||
|
|
||||||
override fun updateThreadId(fromId: Long, toId: Long) {
|
override fun updateThreadId(fromId: Long, toId: Long) {
|
||||||
val contentValues = ContentValues(1)
|
val contentValues = ContentValues(1)
|
||||||
contentValues.put(THREAD_ID, toId)
|
contentValues.put(THREAD_ID, toId)
|
||||||
|
@ -22,8 +22,8 @@ import android.database.Cursor;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
import net.sqlcipher.database.SQLiteQueryBuilder;
|
import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
|
||||||
|
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
@ -112,6 +112,64 @@ public class MmsSmsDatabase extends Database {
|
|||||||
return getMessageFor(timestamp, author.serialize());
|
return getMessageFor(timestamp, author.serialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getPreviousPage(long threadId, long fromTime, int limit) {
|
||||||
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT+" ASC";
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID+" = "+threadId
|
||||||
|
+ " AND "+MmsSmsColumns.NORMALIZED_DATE_SENT+" > "+fromTime;
|
||||||
|
String limitStr = ""+limit;
|
||||||
|
long sent = -1;
|
||||||
|
Cursor cursor = queryTables(PROJECTION, selection, order, limitStr);
|
||||||
|
if (cursor == null) return sent;
|
||||||
|
Reader reader = readerFor(cursor);
|
||||||
|
if (!cursor.move(limit)) {
|
||||||
|
cursor.moveToLast();
|
||||||
|
}
|
||||||
|
MessageRecord record = reader.getCurrent();
|
||||||
|
sent = record.getDateSent();
|
||||||
|
reader.close();
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cursor getConversationPage(long threadId, long fromTime, long toTime, int limit) {
|
||||||
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT+" DESC";
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID + " = "+threadId
|
||||||
|
+ " AND "+MmsSmsColumns.NORMALIZED_DATE_SENT+" <= " + fromTime;
|
||||||
|
String limitStr = null;
|
||||||
|
if (toTime != -1L) {
|
||||||
|
selection += " AND "+MmsSmsColumns.NORMALIZED_DATE_SENT+" > "+toTime;
|
||||||
|
} else {
|
||||||
|
limitStr = ""+limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryTables(PROJECTION, selection, order, limitStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasNextPage(long threadId, long toTime) {
|
||||||
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT+" DESC";
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID + " = "+threadId
|
||||||
|
+ " AND "+MmsSmsColumns.NORMALIZED_DATE_SENT+" < " + toTime; // check if there's at least one message before the `toTime`
|
||||||
|
Cursor cursor = queryTables(PROJECTION, selection, order, null);
|
||||||
|
boolean hasNext = false;
|
||||||
|
if (cursor != null) {
|
||||||
|
hasNext = cursor.getCount() > 0;
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
return hasNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPreviousPage(long threadId, long fromTime) {
|
||||||
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT+" DESC";
|
||||||
|
String selection = MmsSmsColumns.THREAD_ID + " = "+threadId
|
||||||
|
+ " AND "+MmsSmsColumns.NORMALIZED_DATE_SENT+" > " + fromTime; // check if there's at least one message after the `fromTime`
|
||||||
|
Cursor cursor = queryTables(PROJECTION, selection, order, null);
|
||||||
|
boolean hasNext = false;
|
||||||
|
if (cursor != null) {
|
||||||
|
hasNext = cursor.getCount() > 0;
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
return hasNext;
|
||||||
|
}
|
||||||
|
|
||||||
public Cursor getConversation(long threadId, boolean reverse, long offset, long limit) {
|
public Cursor getConversation(long threadId, boolean reverse, long offset, long limit) {
|
||||||
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + (reverse ? " DESC" : " ASC");
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + (reverse ? " DESC" : " ASC");
|
||||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||||
@ -199,16 +257,16 @@ public class MmsSmsDatabase extends Database {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getMessagePositionInConversation(long threadId, long receivedTimestamp, @NonNull Address address) {
|
public int getMessagePositionInConversation(long threadId, long sentTimestamp, @NonNull Address address) {
|
||||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||||
|
|
||||||
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.ADDRESS }, selection, order, null)) {
|
try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.ADDRESS }, selection, order, null)) {
|
||||||
String serializedAddress = address.serialize();
|
String serializedAddress = address.serialize();
|
||||||
boolean isOwnNumber = Util.isOwnNumber(context, address.serialize());
|
boolean isOwnNumber = Util.isOwnNumber(context, address.serialize());
|
||||||
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
boolean timestampMatches = cursor.getLong(0) == receivedTimestamp;
|
boolean timestampMatches = cursor.getLong(0) == sentTimestamp;
|
||||||
boolean addressMatches = serializedAddress.equals(cursor.getString(1));
|
boolean addressMatches = serializedAddress.equals(cursor.getString(1));
|
||||||
|
|
||||||
if (timestampMatches && (addressMatches || isOwnNumber)) {
|
if (timestampMatches && (addressMatches || isOwnNumber)) {
|
||||||
|
@ -6,7 +6,7 @@ import android.database.Cursor;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.session.libsignal.utilities.Base64;
|
import org.session.libsignal.utilities.Base64;
|
||||||
|
@ -48,6 +48,14 @@ class ReactionDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATE_INDEXS = arrayOf(
|
||||||
|
"CREATE INDEX IF NOT EXISTS reaction_message_id_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.MESSAGE_ID + ");",
|
||||||
|
"CREATE INDEX IF NOT EXISTS reaction_is_mms_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.IS_MMS + ");",
|
||||||
|
"CREATE INDEX IF NOT EXISTS reaction_message_id_is_mms_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.MESSAGE_ID + ", " + ReactionDatabase.IS_MMS + ");",
|
||||||
|
"CREATE INDEX IF NOT EXISTS reaction_sort_id_index ON " + ReactionDatabase.TABLE_NAME + " (" + ReactionDatabase.SORT_ID + ");",
|
||||||
|
)
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATE_REACTION_TRIGGERS = arrayOf(
|
val CREATE_REACTION_TRIGGERS = arrayOf(
|
||||||
"""
|
"""
|
||||||
|
@ -11,7 +11,7 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.MaterialColor;
|
import org.session.libsession.utilities.MaterialColor;
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
package org.thoughtcrime.securesms.database;
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import net.sqlcipher.Cursor;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
@ -63,7 +63,7 @@ public class SearchDatabase extends Database {
|
|||||||
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
|
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
|
||||||
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
|
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
|
||||||
"snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
|
"snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
|
||||||
SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " +
|
SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT + ", " +
|
||||||
SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
|
SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
|
||||||
"FROM " + SmsDatabase.TABLE_NAME + " " +
|
"FROM " + SmsDatabase.TABLE_NAME + " " +
|
||||||
"INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " +
|
"INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " +
|
||||||
@ -74,13 +74,13 @@ public class SearchDatabase extends Database {
|
|||||||
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
|
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
|
||||||
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
|
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
|
||||||
"snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
|
"snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
|
||||||
MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " +
|
MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT + ", " +
|
||||||
MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
|
MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
|
||||||
"FROM " + MmsDatabase.TABLE_NAME + " " +
|
"FROM " + MmsDatabase.TABLE_NAME + " " +
|
||||||
"INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " +
|
"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 + " " +
|
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
|
||||||
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " +
|
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " +
|
||||||
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
|
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC " +
|
||||||
"LIMIT ?";
|
"LIMIT ?";
|
||||||
|
|
||||||
private static final String MESSAGES_FOR_THREAD_QUERY =
|
private static final String MESSAGES_FOR_THREAD_QUERY =
|
||||||
@ -88,7 +88,7 @@ public class SearchDatabase extends Database {
|
|||||||
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
|
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
|
||||||
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
|
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
|
||||||
"snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
|
"snippet(" + SMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
|
||||||
SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " +
|
SmsDatabase.TABLE_NAME + "." + SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT + ", " +
|
||||||
SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
|
SMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
|
||||||
"FROM " + SmsDatabase.TABLE_NAME + " " +
|
"FROM " + SmsDatabase.TABLE_NAME + " " +
|
||||||
"INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " +
|
"INNER JOIN " + SMS_FTS_TABLE_NAME + " ON " + SMS_FTS_TABLE_NAME + "." + ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " " +
|
||||||
@ -99,13 +99,13 @@ public class SearchDatabase extends Database {
|
|||||||
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
|
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ADDRESS + " AS " + CONVERSATION_ADDRESS + ", " +
|
||||||
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
|
MmsSmsColumns.ADDRESS + " AS " + MESSAGE_ADDRESS + ", " +
|
||||||
"snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
|
"snippet(" + MMS_FTS_TABLE_NAME + ", -1, '', '', '...', 7) AS " + SNIPPET + ", " +
|
||||||
MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + ", " +
|
MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT + ", " +
|
||||||
MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
|
MMS_FTS_TABLE_NAME + "." + THREAD_ID + " " +
|
||||||
"FROM " + MmsDatabase.TABLE_NAME + " " +
|
"FROM " + MmsDatabase.TABLE_NAME + " " +
|
||||||
"INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " +
|
"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 + " " +
|
"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 + " = ? " +
|
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? AND " + MmsDatabase.TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? " +
|
||||||
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
|
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC " +
|
||||||
"LIMIT 500";
|
"LIMIT 500";
|
||||||
|
|
||||||
public SearchDatabase(@NonNull Context context, @NonNull SQLCipherOpenHelper databaseHelper) {
|
public SearchDatabase(@NonNull Context context, @NonNull SQLCipherOpenHelper databaseHelper) {
|
||||||
|
@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.database
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import net.sqlcipher.Cursor
|
import android.database.Cursor
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
@ -75,21 +75,6 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun contactFromCursor(cursor: Cursor): Contact {
|
fun contactFromCursor(cursor: Cursor): Contact {
|
||||||
val sessionID = cursor.getString(sessionID)
|
|
||||||
val contact = Contact(sessionID)
|
|
||||||
contact.name = cursor.getStringOrNull(name)
|
|
||||||
contact.nickname = cursor.getStringOrNull(nickname)
|
|
||||||
contact.profilePictureURL = cursor.getStringOrNull(profilePictureURL)
|
|
||||||
contact.profilePictureFileName = cursor.getStringOrNull(profilePictureFileName)
|
|
||||||
cursor.getStringOrNull(profilePictureEncryptionKey)?.let {
|
|
||||||
contact.profilePictureEncryptionKey = Base64.decode(it)
|
|
||||||
}
|
|
||||||
contact.threadID = cursor.getLong(threadID)
|
|
||||||
contact.isTrusted = cursor.getInt(isTrusted) != 0
|
|
||||||
return contact
|
|
||||||
}
|
|
||||||
|
|
||||||
fun contactFromCursor(cursor: android.database.Cursor): Contact {
|
|
||||||
val sessionID = cursor.getString(cursor.getColumnIndexOrThrow(sessionID))
|
val sessionID = cursor.getString(cursor.getColumnIndexOrThrow(sessionID))
|
||||||
val contact = Contact(sessionID)
|
val contact = Contact(sessionID)
|
||||||
contact.name = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(name))
|
contact.name = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(name))
|
||||||
|
@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database
|
|||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import net.sqlcipher.Cursor
|
import android.database.Cursor
|
||||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||||
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
||||||
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
||||||
|
@ -28,9 +28,10 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
import net.sqlcipher.database.SQLiteStatement;
|
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.session.libsession.messaging.calls.CallMessageType;
|
import org.session.libsession.messaging.calls.CallMessageType;
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
|
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
|
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
|
||||||
@ -52,6 +53,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -596,6 +598,30 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
return threadDeleted;
|
return threadDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteMessages(long[] messageIds, long threadId) {
|
||||||
|
String[] argsArray = new String[messageIds.length];
|
||||||
|
String[] argValues = new String[messageIds.length];
|
||||||
|
Arrays.fill(argsArray, "?");
|
||||||
|
|
||||||
|
for (int i = 0; i < messageIds.length; i++) {
|
||||||
|
argValues[i] = (messageIds[i] + "");
|
||||||
|
}
|
||||||
|
|
||||||
|
String combinedMessageIdArgss = StringUtils.join(messageIds, ',');
|
||||||
|
String combinedMessageIds = StringUtils.join(messageIds, ',');
|
||||||
|
Log.i("MessageDatabase", "Deleting: " + combinedMessageIds);
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
db.delete(
|
||||||
|
TABLE_NAME,
|
||||||
|
ID + " IN (" + StringUtils.join(argsArray, ',') + ")",
|
||||||
|
argValues
|
||||||
|
);
|
||||||
|
boolean threadDeleted = DatabaseComponent.get(context).threadDatabase().update(threadId, false);
|
||||||
|
notifyConversationListeners(threadId);
|
||||||
|
return threadDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateThreadId(long fromId, long toId) {
|
public void updateThreadId(long fromId, long toId) {
|
||||||
ContentValues contentValues = new ContentValues(1);
|
ContentValues contentValues = new ContentValues(1);
|
||||||
|
@ -2,31 +2,23 @@ package org.thoughtcrime.securesms.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import org.session.libsession.avatars.AvatarHelper
|
||||||
import org.session.libsession.database.StorageProtocol
|
import org.session.libsession.database.StorageProtocol
|
||||||
import org.session.libsession.messaging.BlindedIdMapping
|
import org.session.libsession.messaging.BlindedIdMapping
|
||||||
import org.session.libsession.messaging.calls.CallMessageType
|
import org.session.libsession.messaging.calls.CallMessageType
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
import org.session.libsession.messaging.jobs.*
|
||||||
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
|
||||||
import org.session.libsession.messaging.jobs.Job
|
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
|
||||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
|
||||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
|
||||||
import org.session.libsession.messaging.messages.Message
|
import org.session.libsession.messaging.messages.Message
|
||||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage
|
import org.session.libsession.messaging.messages.signal.*
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage
|
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
|
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
|
||||||
import org.session.libsession.messaging.messages.visible.Attachment
|
import org.session.libsession.messaging.messages.visible.Attachment
|
||||||
|
import org.session.libsession.messaging.messages.visible.Profile
|
||||||
import org.session.libsession.messaging.messages.visible.Reaction
|
import org.session.libsession.messaging.messages.visible.Reaction
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsession.messaging.open_groups.GroupMember
|
import org.session.libsession.messaging.open_groups.GroupMember
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
@ -36,11 +28,12 @@ import org.session.libsession.messaging.utilities.SessionId
|
|||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||||
import org.session.libsession.messaging.utilities.UpdateMessageData
|
import org.session.libsession.messaging.utilities.UpdateMessageData
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.*
|
||||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||||
import org.session.libsession.utilities.GroupRecord
|
import org.session.libsession.utilities.GroupRecord
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.ProfileKeyUtil
|
import org.session.libsession.utilities.ProfileKeyUtil
|
||||||
|
import org.session.libsession.utilities.SSKEnvironment
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
@ -58,6 +51,7 @@ import org.thoughtcrime.securesms.groups.OpenGroupManager
|
|||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol
|
import org.thoughtcrime.securesms.util.SessionMetaProtocol
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
|
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
|
||||||
|
|
||||||
@ -69,16 +63,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return DatabaseComponent.get(context).lokiAPIDatabase().getUserX25519KeyPair()
|
return DatabaseComponent.get(context).lokiAPIDatabase().getUserX25519KeyPair()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUserDisplayName(): String? {
|
override fun getUserProfile(): Profile {
|
||||||
return TextSecurePreferences.getProfileName(context)
|
val displayName = TextSecurePreferences.getProfileName(context)!!
|
||||||
}
|
val profileKey = ProfileKeyUtil.getProfileKey(context)
|
||||||
|
val profilePictureUrl = TextSecurePreferences.getProfilePictureURL(context)
|
||||||
override fun getUserProfileKey(): ByteArray? {
|
return Profile(displayName, profileKey, profilePictureUrl)
|
||||||
return ProfileKeyUtil.getProfileKey(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getUserProfilePictureURL(): String? {
|
|
||||||
return TextSecurePreferences.getProfilePictureURL(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setUserProfilePictureURL(newValue: String) {
|
override fun setUserProfilePictureURL(newValue: String) {
|
||||||
@ -335,6 +324,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseComponent.get(context).groupDatabase().updateProfilePicture(groupID, newValue)
|
DatabaseComponent.get(context).groupDatabase().updateProfilePicture(groupID, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hasDownloadedProfilePicture(groupID: String): Boolean {
|
||||||
|
return DatabaseComponent.get(context).groupDatabase().hasDownloadedProfilePicture(groupID)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getReceivedMessageTimestamps(): Set<Long> {
|
override fun getReceivedMessageTimestamps(): Set<Long> {
|
||||||
return SessionMetaProtocol.getTimestamps()
|
return SessionMetaProtocol.getTimestamps()
|
||||||
}
|
}
|
||||||
@ -428,6 +421,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun clearErrorMessage(messageID: Long) {
|
||||||
|
val db = DatabaseComponent.get(context).lokiMessageDatabase()
|
||||||
|
db.clearErrorMessage(messageID)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setMessageServerHash(messageID: Long, serverHash: String) {
|
override fun setMessageServerHash(messageID: Long, serverHash: String) {
|
||||||
DatabaseComponent.get(context).lokiMessageDatabase().setMessageServerHash(messageID, serverHash)
|
DatabaseComponent.get(context).lokiMessageDatabase().setMessageServerHash(messageID, serverHash)
|
||||||
}
|
}
|
||||||
@ -562,8 +560,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return DatabaseComponent.get(context).groupDatabase().allGroups
|
return DatabaseComponent.get(context).groupDatabase().allGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addOpenGroup(urlAsString: String) {
|
override fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? {
|
||||||
OpenGroupManager.addOpenGroup(urlAsString, context)
|
return OpenGroupManager.addOpenGroup(urlAsString, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenGroupAdded(server: String) {
|
override fun onOpenGroupAdded(server: String) {
|
||||||
@ -759,6 +757,25 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
val smsDb = DatabaseComponent.get(context).smsDatabase()
|
val smsDb = DatabaseComponent.get(context).smsDatabase()
|
||||||
val sender = Recipient.from(context, fromSerialized(senderPublicKey), false)
|
val sender = Recipient.from(context, fromSerialized(senderPublicKey), false)
|
||||||
val threadId = threadDB.getOrCreateThreadIdFor(sender)
|
val threadId = threadDB.getOrCreateThreadIdFor(sender)
|
||||||
|
val profile = response.profile
|
||||||
|
if (profile != null) {
|
||||||
|
val profileManager = SSKEnvironment.shared.profileManager
|
||||||
|
val name = profile.displayName!!
|
||||||
|
if (name.isNotEmpty()) {
|
||||||
|
profileManager.setName(context, sender, name)
|
||||||
|
}
|
||||||
|
val newProfileKey = profile.profileKey
|
||||||
|
|
||||||
|
val needsProfilePicture = !AvatarHelper.avatarFileExists(context, sender.address)
|
||||||
|
val profileKeyValid = newProfileKey?.isNotEmpty() == true && (newProfileKey.size == 16 || newProfileKey.size == 32) && profile.profilePictureURL?.isNotEmpty() == true
|
||||||
|
val profileKeyChanged = (sender.profileKey == null || !MessageDigest.isEqual(sender.profileKey, newProfileKey))
|
||||||
|
|
||||||
|
if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) {
|
||||||
|
profileManager.setProfileKey(context, sender, newProfileKey!!)
|
||||||
|
profileManager.setUnidentifiedAccessMode(context, sender, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
||||||
|
profileManager.setProfilePictureURL(context, sender, profile.profilePictureURL!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
threadDB.setHasSent(threadId, true)
|
threadDB.setHasSent(threadId, true)
|
||||||
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase()
|
||||||
val mappings = mutableMapOf<String, BlindedIdMapping>()
|
val mappings = mutableMapOf<String, BlindedIdMapping>()
|
||||||
|
@ -32,7 +32,7 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
@ -502,15 +502,23 @@ public class ThreadDatabase extends Database {
|
|||||||
return db.rawQuery(query, null);
|
return db.rawQuery(query, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLastSeen(long threadId) {
|
public void setLastSeen(long threadId, long timestamp) {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
ContentValues contentValues = new ContentValues(1);
|
ContentValues contentValues = new ContentValues(1);
|
||||||
|
if (timestamp == -1) {
|
||||||
contentValues.put(LAST_SEEN, System.currentTimeMillis());
|
contentValues.put(LAST_SEEN, System.currentTimeMillis());
|
||||||
|
} else {
|
||||||
|
contentValues.put(LAST_SEEN, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)});
|
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)});
|
||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setLastSeen(long threadId) {
|
||||||
|
setLastSeen(threadId, -1);
|
||||||
|
}
|
||||||
|
|
||||||
public Pair<Long, Boolean> getLastSeenAndHasSent(long threadId) {
|
public Pair<Long, Boolean> getLastSeenAndHasSent(long threadId) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN, HAS_SENT}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN, HAS_SENT}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
package org.thoughtcrime.securesms.database.helpers;
|
package org.thoughtcrime.securesms.database.helpers;
|
||||||
|
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteConnection;
|
||||||
import net.sqlcipher.database.SQLiteDatabaseHook;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
import net.sqlcipher.database.SQLiteOpenHelper;
|
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
|
||||||
|
import net.zetetic.database.sqlcipher.SQLiteException;
|
||||||
|
import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
|
||||||
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
@ -35,6 +39,11 @@ import org.thoughtcrime.securesms.database.SessionContactDatabase;
|
|||||||
import org.thoughtcrime.securesms.database.SessionJobDatabase;
|
import org.thoughtcrime.securesms.database.SessionJobDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
@ -75,40 +84,157 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
private static final int lokiV36 = 57;
|
private static final int lokiV36 = 57;
|
||||||
private static final int lokiV37 = 58;
|
private static final int lokiV37 = 58;
|
||||||
private static final int lokiV38 = 59;
|
private static final int lokiV38 = 59;
|
||||||
|
private static final int lokiV39 = 60;
|
||||||
|
|
||||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||||
private static final int DATABASE_VERSION = lokiV38;
|
private static final int DATABASE_VERSION = lokiV39;
|
||||||
private static final String DATABASE_NAME = "signal.db";
|
private static final int MIN_DATABASE_VERSION = lokiV7;
|
||||||
|
private static final String CIPHER3_DATABASE_NAME = "signal.db";
|
||||||
|
public static final String DATABASE_NAME = "signal_v4.db";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final DatabaseSecret databaseSecret;
|
private final DatabaseSecret databaseSecret;
|
||||||
|
|
||||||
public SQLCipherOpenHelper(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) {
|
public SQLCipherOpenHelper(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) {
|
||||||
super(context, DATABASE_NAME, null, DATABASE_VERSION, new SQLiteDatabaseHook() {
|
super(context, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, MIN_DATABASE_VERSION, null, new SQLiteDatabaseHook() {
|
||||||
@Override
|
@Override
|
||||||
public void preKey(SQLiteDatabase db) {
|
public void preKey(SQLiteConnection connection) {
|
||||||
db.rawExecSQL("PRAGMA cipher_default_kdf_iter = 1;");
|
SQLCipherOpenHelper.applySQLCipherPragmas(connection, true);
|
||||||
db.rawExecSQL("PRAGMA cipher_default_page_size = 4096;");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postKey(SQLiteDatabase db) {
|
public void postKey(SQLiteConnection connection) {
|
||||||
db.rawExecSQL("PRAGMA kdf_iter = '1';");
|
SQLCipherOpenHelper.applySQLCipherPragmas(connection, true);
|
||||||
db.rawExecSQL("PRAGMA cipher_page_size = 4096;");
|
|
||||||
// if not vacuumed in a while, perform that operation
|
// if not vacuumed in a while, perform that operation
|
||||||
long currentTime = System.currentTimeMillis();
|
long currentTime = System.currentTimeMillis();
|
||||||
// 7 days
|
// 7 days
|
||||||
if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) {
|
if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) {
|
||||||
db.rawExecSQL("VACUUM;");
|
connection.execute("VACUUM;", null, null);
|
||||||
TextSecurePreferences.setLastVacuumNow(context);
|
TextSecurePreferences.setLastVacuumNow(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}, true);
|
||||||
|
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
this.databaseSecret = databaseSecret;
|
this.databaseSecret = databaseSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void applySQLCipherPragmas(SQLiteConnection connection, boolean useSQLCipher4) {
|
||||||
|
if (useSQLCipher4) {
|
||||||
|
connection.execute("PRAGMA kdf_iter = '256000';", null, null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
connection.execute("PRAGMA cipher_compatibility = 3;", null, null);
|
||||||
|
connection.execute("PRAGMA kdf_iter = '1';", null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) throws SQLiteException {
|
||||||
|
return SQLiteDatabase.openDatabase(path, databaseSecret.asString(), null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() {
|
||||||
|
@Override
|
||||||
|
public void preKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void migrateSqlCipher3To4IfNeeded(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) throws Exception {
|
||||||
|
String oldDbPath = context.getDatabasePath(CIPHER3_DATABASE_NAME).getPath();
|
||||||
|
File oldDbFile = new File(oldDbPath);
|
||||||
|
|
||||||
|
// If the old SQLCipher3 database file doesn't exist then no need to do anything
|
||||||
|
if (!oldDbFile.exists()) { return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Define the location for the new database
|
||||||
|
String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath();
|
||||||
|
File newDbFile = new File(newDbPath);
|
||||||
|
|
||||||
|
// If the new database file already exists then check if it's valid first, if it's in an
|
||||||
|
// invalid state we should delete it and try to migrate again
|
||||||
|
if (newDbFile.exists()) {
|
||||||
|
// If the old database hasn't been modified since the new database was created, then we can
|
||||||
|
// assume the user hasn't downgraded for some reason and made changes to the old database and
|
||||||
|
// can remove the old database file (it won't be used anymore)
|
||||||
|
if (oldDbFile.lastModified() <= newDbFile.lastModified()) {
|
||||||
|
// TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past
|
||||||
|
// //noinspection ResultOfMethodCallIgnored
|
||||||
|
// oldDbFile.delete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the old database does have newer changes then the new database could have stale/invalid
|
||||||
|
// data and we should re-migrate to avoid losing any data or issues
|
||||||
|
if (!newDbFile.delete()) {
|
||||||
|
throw new Exception("Failed to remove invalid new database");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newDbFile.createNewFile()) {
|
||||||
|
throw new Exception("Failed to create new database");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the old database and extract it's version
|
||||||
|
SQLiteDatabase oldDb = SQLCipherOpenHelper.open(oldDbPath, databaseSecret, false);
|
||||||
|
int oldDbVersion = oldDb.getVersion();
|
||||||
|
|
||||||
|
// Export the old database to the new one (will have the default 'kdf_iter' and 'page_size' settings)
|
||||||
|
oldDb.rawExecSQL(
|
||||||
|
String.format("ATTACH DATABASE '%s' AS sqlcipher4 KEY '%s'", newDbPath, databaseSecret.asString())
|
||||||
|
);
|
||||||
|
Cursor cursor = oldDb.rawQuery("SELECT sqlcipher_export('sqlcipher4')");
|
||||||
|
cursor.moveToLast();
|
||||||
|
cursor.close();
|
||||||
|
oldDb.rawExecSQL("DETACH DATABASE sqlcipher4");
|
||||||
|
oldDb.close();
|
||||||
|
|
||||||
|
// Open the newly migrated database (to ensure it works) and set it's version so we don't try
|
||||||
|
// to run any of our custom migrations
|
||||||
|
SQLiteDatabase newDb = SQLCipherOpenHelper.open(newDbPath, databaseSecret, true);
|
||||||
|
newDb.setVersion(oldDbVersion);
|
||||||
|
newDb.close();
|
||||||
|
|
||||||
|
// TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past
|
||||||
|
// Remove the old database file since it will no longer be used
|
||||||
|
// //noinspection ResultOfMethodCallIgnored
|
||||||
|
// oldDbFile.delete();
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Log.e(TAG, "Migration from SQLCipher3 to SQLCipher4 failed", e);
|
||||||
|
|
||||||
|
// Notify the user of the issue so they know they can downgrade until the issue is fixed
|
||||||
|
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||||
|
String channelId = context.getString(R.string.NotificationChannel_failures);
|
||||||
|
|
||||||
|
if (NotificationChannels.supported()) {
|
||||||
|
NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH);
|
||||||
|
channel.enableVibration(true);
|
||||||
|
notificationManager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setColor(context.getResources().getColor(R.color.textsecure_primary))
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||||
|
.setContentTitle(context.getString(R.string.ErrorNotifier_migration))
|
||||||
|
.setContentText(context.getString(R.string.ErrorNotifier_migration_downgrade))
|
||||||
|
.setAutoCancel(true);
|
||||||
|
|
||||||
|
if (!NotificationChannels.supported()) {
|
||||||
|
builder.setPriority(NotificationCompat.PRIORITY_HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.notify(5874, builder.build());
|
||||||
|
|
||||||
|
// Throw the error (app will crash but there is nothing else we can do unfortunately)
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(SQLiteDatabase db) {
|
public void onCreate(SQLiteDatabase db) {
|
||||||
db.execSQL(SmsDatabase.CREATE_TABLE);
|
db.execSQL(SmsDatabase.CREATE_TABLE);
|
||||||
@ -188,6 +314,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
executeStatements(db, DraftDatabase.CREATE_INDEXS);
|
executeStatements(db, DraftDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, GroupDatabase.CREATE_INDEXS);
|
executeStatements(db, GroupDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
|
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
|
||||||
|
executeStatements(db, ReactionDatabase.CREATE_INDEXS);
|
||||||
|
|
||||||
executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS);
|
executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS);
|
||||||
}
|
}
|
||||||
@ -195,9 +322,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
@Override
|
@Override
|
||||||
public void onConfigure(SQLiteDatabase db) {
|
public void onConfigure(SQLiteDatabase db) {
|
||||||
super.onConfigure(db);
|
super.onConfigure(db);
|
||||||
// Loki - Enable write ahead logging mode and increase the cache size.
|
|
||||||
// This should be disabled if we ever run into serious race condition bugs.
|
|
||||||
db.enableWriteAheadLogging();
|
|
||||||
db.execSQL("PRAGMA cache_size = 10000");
|
db.execSQL("PRAGMA cache_size = 10000");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,20 +539,16 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.execSQL(EmojiSearchDatabase.CREATE_EMOJI_SEARCH_TABLE_COMMAND);
|
db.execSQL(EmojiSearchDatabase.CREATE_EMOJI_SEARCH_TABLE_COMMAND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < lokiV39) {
|
||||||
|
executeStatements(db, ReactionDatabase.CREATE_INDEXS);
|
||||||
|
}
|
||||||
|
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SQLiteDatabase getReadableDatabase() {
|
|
||||||
return getReadableDatabase(databaseSecret.asString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public SQLiteDatabase getWritableDatabase() {
|
|
||||||
return getWritableDatabase(databaseSecret.asString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void markCurrent(SQLiteDatabase db) {
|
public void markCurrent(SQLiteDatabase db) {
|
||||||
db.setVersion(DATABASE_VERSION);
|
db.setVersion(DATABASE_VERSION);
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ import org.session.libsession.utilities.NetworkFailure;
|
|||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base class for message record models that are displayed in
|
* The base class for message record models that are displayed in
|
||||||
@ -140,14 +141,16 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||||||
return spannable;
|
return spannable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean equals(Object other) {
|
public boolean equals(Object other) {
|
||||||
return other instanceof MessageRecord
|
return other instanceof MessageRecord
|
||||||
&& ((MessageRecord) other).getId() == getId()
|
&& ((MessageRecord) other).getId() == getId()
|
||||||
&& ((MessageRecord) other).isMms() == isMms();
|
&& ((MessageRecord) other).isMms() == isMms();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return (int)getId();
|
return Objects.hash(id, isMms());
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull List<ReactionRecord> getReactions() {
|
public @NonNull List<ReactionRecord> getReactions() {
|
||||||
|
@ -2,13 +2,15 @@ package org.thoughtcrime.securesms.database.model;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import org.session.libsession.utilities.Contact;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.Contact;
|
||||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||||
import org.session.libsession.utilities.NetworkFailure;
|
import org.session.libsession.utilities.NetworkFailure;
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -8,6 +8,8 @@ import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
|||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class Quote {
|
public class Quote {
|
||||||
|
|
||||||
private final long id;
|
private final long id;
|
||||||
@ -47,4 +49,17 @@ public class Quote {
|
|||||||
public QuoteModel getQuoteModel() {
|
public QuoteModel getQuoteModel() {
|
||||||
return new QuoteModel(id, author, text, missing, attachment.asAttachments());
|
return new QuoteModel(id, author, text, missing, attachment.asAttachments());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Quote quote = (Quote) o;
|
||||||
|
return id == quote.id && missing == quote.missing && Objects.equals(author, quote.author) && Objects.equals(text, quote.text) && Objects.equals(attachment, quote.attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(id, author, text, missing, attachment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,6 @@ public class ThreadRecord extends DisplayRecord {
|
|||||||
private final long expiresIn;
|
private final long expiresIn;
|
||||||
private final long lastSeen;
|
private final long lastSeen;
|
||||||
private final boolean pinned;
|
private final boolean pinned;
|
||||||
private final int recipientHash;
|
|
||||||
|
|
||||||
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
||||||
@NonNull Recipient recipient, long date, long count, int unreadCount,
|
@NonNull Recipient recipient, long date, long count, int unreadCount,
|
||||||
@ -67,17 +66,12 @@ public class ThreadRecord extends DisplayRecord {
|
|||||||
this.expiresIn = expiresIn;
|
this.expiresIn = expiresIn;
|
||||||
this.lastSeen = lastSeen;
|
this.lastSeen = lastSeen;
|
||||||
this.pinned = pinned;
|
this.pinned = pinned;
|
||||||
this.recipientHash = recipient.hashCode();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable Uri getSnippetUri() {
|
public @Nullable Uri getSnippetUri() {
|
||||||
return snippetUri;
|
return snippetUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getRecipientHash() {
|
|
||||||
return recipientHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||||
if (isGroupUpdateMessage()) {
|
if (isGroupUpdateMessage()) {
|
||||||
|
@ -6,7 +6,7 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import net.sqlcipher.database.SQLiteDatabase
|
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||||
import org.session.libsession.database.MessageDataProvider
|
import org.session.libsession.database.MessageDataProvider
|
||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||||
@ -22,7 +22,7 @@ object DatabaseModule {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
SQLiteDatabase.loadLibs(context)
|
System.loadLibrary("sqlcipher")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ -33,6 +33,7 @@ object DatabaseModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideOpenHelper(@ApplicationContext context: Context): SQLCipherOpenHelper {
|
fun provideOpenHelper(@ApplicationContext context: Context): SQLCipherOpenHelper {
|
||||||
val dbSecret = DatabaseSecretProvider(context).orCreateDatabaseSecret
|
val dbSecret = DatabaseSecretProvider(context).orCreateDatabaseSecret
|
||||||
|
SQLCipherOpenHelper.migrateSqlCipher3To4IfNeeded(context, dbSecret)
|
||||||
return SQLCipherOpenHelper(context, dbSecret)
|
return SQLCipherOpenHelper(context, dbSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,14 +58,14 @@ object OpenGroupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun add(server: String, room: String, publicKey: String, context: Context) {
|
fun add(server: String, room: String, publicKey: String, context: Context): OpenGroupApi.RoomInfo? {
|
||||||
val openGroupID = "$server.$room"
|
val openGroupID = "$server.$room"
|
||||||
var threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
var threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
val threadDB = DatabaseComponent.get(context).lokiThreadDatabase()
|
||||||
// Check it it's added already
|
// Check it it's added already
|
||||||
val existingOpenGroup = threadDB.getOpenGroupChat(threadID)
|
val existingOpenGroup = threadDB.getOpenGroupChat(threadID)
|
||||||
if (existingOpenGroup != null) { return }
|
if (existingOpenGroup != null) { return null }
|
||||||
// Clear any existing data if needed
|
// Clear any existing data if needed
|
||||||
storage.removeLastDeletionServerID(room, server)
|
storage.removeLastDeletionServerID(room, server)
|
||||||
storage.removeLastMessageServerID(room, server)
|
storage.removeLastMessageServerID(room, server)
|
||||||
@ -73,18 +73,17 @@ object OpenGroupManager {
|
|||||||
storage.removeLastOutboxMessageId(server)
|
storage.removeLastOutboxMessageId(server)
|
||||||
// Store the public key
|
// Store the public key
|
||||||
storage.setOpenGroupPublicKey(server, publicKey)
|
storage.setOpenGroupPublicKey(server, publicKey)
|
||||||
// Get capabilities
|
// Get capabilities & room info
|
||||||
val capabilities = OpenGroupApi.getCapabilities(server).get()
|
val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(room, server).get()
|
||||||
storage.setServerCapabilities(server, capabilities.capabilities)
|
storage.setServerCapabilities(server, capabilities.capabilities)
|
||||||
// Get room info
|
|
||||||
val info = OpenGroupApi.getRoomInfo(room, server).get()
|
|
||||||
storage.setUserCount(room, server, info.activeUsers)
|
storage.setUserCount(room, server, info.activeUsers)
|
||||||
// Create the group locally if not available already
|
// Create the group locally if not available already
|
||||||
if (threadID < 0) {
|
if (threadID < 0) {
|
||||||
threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId
|
threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId
|
||||||
}
|
}
|
||||||
val openGroup = OpenGroup(server, room, info.name, info.infoUpdates, publicKey)
|
val openGroup = OpenGroup(server = server, room = room, publicKey = publicKey, name = info.name, imageId = info.imageId, canWrite = info.write, infoUpdates = info.infoUpdates)
|
||||||
threadDB.setOpenGroupChat(openGroup, threadID)
|
threadDB.setOpenGroupChat(openGroup, threadID)
|
||||||
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restartPollerForServer(server: String) {
|
fun restartPollerForServer(server: String) {
|
||||||
@ -130,12 +129,13 @@ object OpenGroupManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addOpenGroup(urlAsString: String, context: Context) {
|
fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? {
|
||||||
val url = HttpUrl.parse(urlAsString) ?: return
|
val url = HttpUrl.parse(urlAsString) ?: return null
|
||||||
val server = OpenGroup.getServer(urlAsString)
|
val server = OpenGroup.getServer(urlAsString)
|
||||||
val room = url.pathSegments().firstOrNull() ?: return
|
val room = url.pathSegments().firstOrNull() ?: return null
|
||||||
val publicKey = url.queryParameter("public_key") ?: return
|
val publicKey = url.queryParameter("public_key") ?: return null
|
||||||
add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function
|
|
||||||
|
return add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateOpenGroup(openGroup: OpenGroup, context: Context) {
|
fun updateOpenGroup(openGroup: OpenGroup, context: Context) {
|
||||||
|
@ -99,11 +99,11 @@ class ConversationView : LinearLayout {
|
|||||||
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||||
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||||
if (isTyping) {
|
if (isTyping) {
|
||||||
binding.typingIndicatorView.startAnimation()
|
binding.typingIndicatorView.root.startAnimation()
|
||||||
} else {
|
} else {
|
||||||
binding.typingIndicatorView.stopAnimation()
|
binding.typingIndicatorView.root.stopAnimation()
|
||||||
}
|
}
|
||||||
binding.typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE
|
binding.typingIndicatorView.root.visibility = if (isTyping) View.VISIBLE else View.GONE
|
||||||
binding.statusIndicatorImageView.visibility = View.VISIBLE
|
binding.statusIndicatorImageView.visibility = View.VISIBLE
|
||||||
when {
|
when {
|
||||||
!thread.isOutgoing -> binding.statusIndicatorImageView.visibility = View.GONE
|
!thread.isOutgoing -> binding.statusIndicatorImageView.visibility = View.GONE
|
||||||
|
@ -102,7 +102,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
when (model) {
|
when (model) {
|
||||||
is GlobalSearchAdapter.Model.Message -> {
|
is GlobalSearchAdapter.Model.Message -> {
|
||||||
val threadId = model.messageResult.threadId
|
val threadId = model.messageResult.threadId
|
||||||
val timestamp = model.messageResult.receivedTimestampMs
|
val timestamp = model.messageResult.sentTimestampMs
|
||||||
val author = model.messageResult.messageRecipient.address
|
val author = model.messageResult.messageRecipient.address
|
||||||
|
|
||||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
val intent = Intent(this, ConversationActivityV2::class.java)
|
||||||
@ -202,7 +202,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
OpenGroupManager.startPolling()
|
OpenGroupManager.startPolling()
|
||||||
JobQueue.shared.resumePendingJobs()
|
JobQueue.shared.resumePendingJobs()
|
||||||
}
|
}
|
||||||
// Set up typing observer
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateProfileButton()
|
updateProfileButton()
|
||||||
TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect {
|
TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect {
|
||||||
@ -365,6 +365,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
setupMessageRequestsBanner()
|
setupMessageRequestsBanner()
|
||||||
updateEmptyState()
|
updateEmptyState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
|
||||||
|
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateEmptyState() {
|
private fun updateEmptyState() {
|
||||||
|
@ -63,6 +63,8 @@ class HomeAdapter(
|
|||||||
lateinit var glide: GlideRequests
|
lateinit var glide: GlideRequests
|
||||||
var typingThreadIDs = setOf<Long>()
|
var typingThreadIDs = setOf<Long>()
|
||||||
set(value) {
|
set(value) {
|
||||||
|
if (field == value) { return }
|
||||||
|
|
||||||
field = value
|
field = value
|
||||||
// TODO: replace this with a diffed update or a partial change set with payloads
|
// TODO: replace this with a diffed update or a partial change set with payloads
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
|
@ -22,22 +22,28 @@ class HomeDiffUtil(
|
|||||||
val newItem = new[newItemPosition]
|
val newItem = new[newItemPosition]
|
||||||
|
|
||||||
// return early to save getDisplayBody or expensive calls
|
// return early to save getDisplayBody or expensive calls
|
||||||
val sameCount = oldItem.count == newItem.count
|
var isSameItem = true
|
||||||
if (!sameCount) return false
|
|
||||||
val sameUnreads = oldItem.unreadCount == newItem.unreadCount
|
|
||||||
if (!sameUnreads) return false
|
|
||||||
val samePinned = oldItem.isPinned == newItem.isPinned
|
|
||||||
if (!samePinned) return false
|
|
||||||
val sameRecipientHash = oldItem.recipientHash == newItem.recipientHash
|
|
||||||
if (!sameRecipientHash) return false
|
|
||||||
val sameSnippet = oldItem.getDisplayBody(context) == newItem.getDisplayBody(context)
|
|
||||||
if (!sameSnippet) return false
|
|
||||||
val sameSendStatus = oldItem.isFailed == newItem.isFailed && oldItem.isDelivered == newItem.isDelivered
|
|
||||||
&& oldItem.isSent == newItem.isSent && oldItem.isPending == newItem.isPending
|
|
||||||
if (!sameSendStatus) return false
|
|
||||||
|
|
||||||
// all same
|
if (isSameItem) { isSameItem = (oldItem.count == newItem.count) }
|
||||||
return true
|
if (isSameItem) { isSameItem = (oldItem.unreadCount == newItem.unreadCount) }
|
||||||
|
if (isSameItem) { isSameItem = (oldItem.isPinned == newItem.isPinned) }
|
||||||
|
|
||||||
|
// Note: For some reason the 'hashCode' value can change after initialisation so we can't cache it
|
||||||
|
if (isSameItem) { isSameItem = (oldItem.recipient.hashCode() == newItem.recipient.hashCode()) }
|
||||||
|
|
||||||
|
// Note: Two instances of 'SpannableString' may not equate even though their content matches
|
||||||
|
if (isSameItem) { isSameItem = (oldItem.getDisplayBody(context).toString() == newItem.getDisplayBody(context).toString()) }
|
||||||
|
|
||||||
|
if (isSameItem) {
|
||||||
|
isSameItem = (
|
||||||
|
oldItem.isFailed == newItem.isFailed &&
|
||||||
|
oldItem.isDelivered == newItem.isDelivered &&
|
||||||
|
oldItem.isSent == newItem.isSent &&
|
||||||
|
oldItem.isPending == newItem.isPending
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSameItem
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -134,7 +134,7 @@ fun ContentView.bindModel(query: String?, model: Message) {
|
|||||||
// if (hasUnreads) {
|
// if (hasUnreads) {
|
||||||
// binding.unreadCountTextView.text = model.unread.toString()
|
// binding.unreadCountTextView.text = model.unread.toString()
|
||||||
// }
|
// }
|
||||||
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.receivedTimestampMs)
|
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
|
||||||
binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient)
|
binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient)
|
||||||
val textSpannable = SpannableStringBuilder()
|
val textSpannable = SpannableStringBuilder()
|
||||||
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
||||||
|
@ -6,17 +6,19 @@ import android.app.PendingIntent;
|
|||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
|
||||||
import network.loki.messenger.BuildConfig;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import network.loki.messenger.BuildConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedules tasks using the {@link AlarmManager}.
|
* Schedules tasks using the {@link AlarmManager}.
|
||||||
*
|
*
|
||||||
@ -51,7 +53,7 @@ public class AlarmManagerScheduler implements Scheduler {
|
|||||||
Intent intent = new Intent(context, RetryReceiver.class);
|
Intent intent = new Intent(context, RetryReceiver.class);
|
||||||
|
|
||||||
intent.setAction(BuildConfig.APPLICATION_ID + UUID.randomUUID().toString());
|
intent.setAction(BuildConfig.APPLICATION_ID + UUID.randomUUID().toString());
|
||||||
alarmManager.set(AlarmManager.RTC_WAKEUP, time, PendingIntent.getBroadcast(context, 0, intent, 0));
|
alarmManager.set(AlarmManager.RTC_WAKEUP, time, PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE));
|
||||||
|
|
||||||
Log.i(TAG, "Set an alarm to retry a job in " + (time - System.currentTimeMillis()) + " ms.");
|
Log.i(TAG, "Set an alarm to retry a job in " + (time - System.currentTimeMillis()) + " ms.");
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
|||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.forceShowIcon
|
|
||||||
|
|
||||||
class MessageRequestsAdapter(
|
class MessageRequestsAdapter(
|
||||||
context: Context,
|
context: Context,
|
||||||
@ -64,7 +63,7 @@ class MessageRequestsAdapter(
|
|||||||
item.iconTintList = ColorStateList.valueOf(context.getColor(R.color.destructive))
|
item.iconTintList = ColorStateList.valueOf(context.getColor(R.color.destructive))
|
||||||
item.title = s
|
item.title = s
|
||||||
}
|
}
|
||||||
popupMenu.forceShowIcon()
|
popupMenu.setForceShowIcon(true)
|
||||||
popupMenu.show()
|
popupMenu.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,17 +17,19 @@
|
|||||||
package org.thoughtcrime.securesms.mms;
|
package org.thoughtcrime.securesms.mms;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
import org.session.libsignal.utilities.guava.Optional;
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class SlideDeck {
|
public class SlideDeck {
|
||||||
|
|
||||||
@ -138,4 +140,17 @@ public class SlideDeck {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
SlideDeck slideDeck = (SlideDeck) o;
|
||||||
|
return Objects.equals(slides, slideDeck.slides);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(slides);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
if (TextSecurePreferences.getLocalNumber(context) == null) {
|
if (TextSecurePreferences.getLocalNumber(context) == null || !TextSecurePreferences.hasSeenWelcomeScreen(context)) {
|
||||||
Log.v(TAG, "User not registered yet.")
|
Log.v(TAG, "User not registered yet.")
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,6 @@ import com.annimon.stream.Optional;
|
|||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
import com.goterl.lazysodium.utils.KeyPair;
|
import com.goterl.lazysodium.utils.KeyPair;
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
import org.session.libsession.messaging.open_groups.OpenGroup;
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||||
import org.session.libsession.messaging.utilities.SessionId;
|
import org.session.libsession.messaging.utilities.SessionId;
|
||||||
@ -453,8 +452,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
NotificationState notificationState = new NotificationState();
|
NotificationState notificationState = new NotificationState();
|
||||||
MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor);
|
MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor);
|
||||||
ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase();
|
ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase();
|
||||||
LokiThreadDatabase lokiThreadDatabase= DatabaseComponent.get(context).lokiThreadDatabase();
|
|
||||||
KeyPair edKeyPair = MessagingModuleConfiguration.getShared().getGetUserED25519KeyPair().invoke();
|
|
||||||
MessageRecord record;
|
MessageRecord record;
|
||||||
Map<Long, String> cache = new HashMap<Long, String>();
|
Map<Long, String> cache = new HashMap<Long, String>();
|
||||||
|
|
||||||
@ -575,7 +573,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION);
|
Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION);
|
||||||
alarmIntent.putExtra("reminder_count", count);
|
alarmIntent.putExtra("reminder_count", count);
|
||||||
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||||
long timeout = TimeUnit.MINUTES.toMillis(2);
|
long timeout = TimeUnit.MINUTES.toMillis(2);
|
||||||
|
|
||||||
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent);
|
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent);
|
||||||
@ -584,7 +582,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
@Override
|
@Override
|
||||||
public void clearReminder(Context context) {
|
public void clearReminder(Context context) {
|
||||||
Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION);
|
Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION);
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||||
alarmManager.cancel(pendingIntent);
|
alarmManager.cancel(pendingIntent);
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,10 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.session.libsession.utilities.NotificationPrivacyPreference;
|
import org.session.libsession.utilities.NotificationPrivacyPreference;
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
|
|
||||||
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class FailedNotificationBuilder extends AbstractNotificationBuilder {
|
public class FailedNotificationBuilder extends AbstractNotificationBuilder {
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ public class FailedNotificationBuilder extends AbstractNotificationBuilder {
|
|||||||
setContentTitle(context.getString(R.string.MessageNotifier_message_delivery_failed));
|
setContentTitle(context.getString(R.string.MessageNotifier_message_delivery_failed));
|
||||||
setContentText(context.getString(R.string.MessageNotifier_failed_to_deliver_message));
|
setContentText(context.getString(R.string.MessageNotifier_failed_to_deliver_message));
|
||||||
setTicker(context.getString(R.string.MessageNotifier_error_delivering_message));
|
setTicker(context.getString(R.string.MessageNotifier_error_delivering_message));
|
||||||
setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
|
setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE));
|
||||||
setAutoCancel(true);
|
setAutoCancel(true);
|
||||||
setAlarms(null, Recipient.VibrateState.DEFAULT);
|
setAlarms(null, Recipient.VibrateState.DEFAULT);
|
||||||
setChannelId(NotificationChannels.FAILURES);
|
setChannelId(NotificationChannels.FAILURES);
|
||||||
|
@ -34,7 +34,7 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
|
|||||||
setColor(context.getResources().getColor(R.color.textsecure_primary));
|
setColor(context.getResources().getColor(R.color.textsecure_primary));
|
||||||
setSmallIcon(R.drawable.ic_notification);
|
setSmallIcon(R.drawable.ic_notification);
|
||||||
setContentTitle(context.getString(R.string.app_name));
|
setContentTitle(context.getString(R.string.app_name));
|
||||||
setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, HomeActivity.class), 0));
|
setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, HomeActivity.class), PendingIntent.FLAG_IMMUTABLE));
|
||||||
setCategory(NotificationCompat.CATEGORY_MESSAGE);
|
setCategory(NotificationCompat.CATEGORY_MESSAGE);
|
||||||
setGroupSummary(true);
|
setGroupSummary(true);
|
||||||
|
|
||||||
@ -52,8 +52,8 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
|
|||||||
|
|
||||||
public void setMostRecentSender(Recipient recipient, Recipient threadRecipient) {
|
public void setMostRecentSender(Recipient recipient, Recipient threadRecipient) {
|
||||||
String displayName = recipient.toShortString();
|
String displayName = recipient.toShortString();
|
||||||
if (threadRecipient.isOpenGroupRecipient()) {
|
if (threadRecipient.isGroupRecipient()) {
|
||||||
displayName = getOpenGroupDisplayName(recipient);
|
displayName = getGroupDisplayName(recipient, threadRecipient.isOpenGroupRecipient());
|
||||||
}
|
}
|
||||||
if (privacy.isDisplayContact()) {
|
if (privacy.isDisplayContact()) {
|
||||||
setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s, displayName));
|
setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s, displayName));
|
||||||
@ -78,8 +78,8 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
|
|||||||
|
|
||||||
public void addMessageBody(@NonNull Recipient sender, Recipient threadRecipient, @Nullable CharSequence body) {
|
public void addMessageBody(@NonNull Recipient sender, Recipient threadRecipient, @Nullable CharSequence body) {
|
||||||
String displayName = sender.toShortString();
|
String displayName = sender.toShortString();
|
||||||
if (threadRecipient.isOpenGroupRecipient()) {
|
if (threadRecipient.isGroupRecipient()) {
|
||||||
displayName = getOpenGroupDisplayName(sender);
|
displayName = getGroupDisplayName(sender, threadRecipient.isOpenGroupRecipient());
|
||||||
}
|
}
|
||||||
if (privacy.isDisplayMessage()) {
|
if (privacy.isDisplayMessage()) {
|
||||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||||
@ -113,14 +113,15 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param recipient the * individual * recipient for which to get the open group display name.
|
* @param recipient the * individual * recipient for which to get the display name.
|
||||||
|
* @param openGroupRecipient whether in an open group context
|
||||||
*/
|
*/
|
||||||
private String getOpenGroupDisplayName(Recipient recipient) {
|
private String getGroupDisplayName(Recipient recipient, boolean openGroupRecipient) {
|
||||||
SessionContactDatabase contactDB = DatabaseComponent.get(context).sessionContactDatabase();
|
SessionContactDatabase contactDB = DatabaseComponent.get(context).sessionContactDatabase();
|
||||||
String sessionID = recipient.getAddress().serialize();
|
String sessionID = recipient.getAddress().serialize();
|
||||||
Contact contact = contactDB.getContactWithSessionID(sessionID);
|
Contact contact = contactDB.getContactWithSessionID(sessionID);
|
||||||
if (contact == null) { return sessionID; }
|
if (contact == null) { return sessionID; }
|
||||||
String displayName = contact.displayName(Contact.ContactContext.OPEN_GROUP);
|
String displayName = contact.displayName(openGroupRecipient ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR);
|
||||||
if (displayName == null) { return sessionID; }
|
if (displayName == null) { return sessionID; }
|
||||||
return displayName;
|
return displayName;
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,15 @@ import android.app.PendingIntent;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.app.TaskStackBuilder;
|
import androidx.core.app.TaskStackBuilder;
|
||||||
|
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
|
|
||||||
public class NotificationItem {
|
public class NotificationItem {
|
||||||
|
|
||||||
@ -75,9 +76,14 @@ public class NotificationItem {
|
|||||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
|
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId);
|
||||||
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
|
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
|
||||||
|
|
||||||
|
int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
intentFlags |= PendingIntent.FLAG_MUTABLE;
|
||||||
|
}
|
||||||
|
|
||||||
return TaskStackBuilder.create(context)
|
return TaskStackBuilder.create(context)
|
||||||
.addNextIntentWithParentStack(intent)
|
.addNextIntentWithParentStack(intent)
|
||||||
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
|
.getPendingIntent(0, intentFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getId() {
|
public long getId() {
|
||||||
|
@ -4,12 +4,14 @@ import android.app.PendingIntent;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsession.utilities.recipients.Recipient.*;
|
import org.session.libsession.utilities.recipients.Recipient.VibrateState;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
|
||||||
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@ -114,7 +116,12 @@ public class NotificationState {
|
|||||||
intent.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, threadArray);
|
intent.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, threadArray);
|
||||||
intent.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId);
|
intent.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId);
|
||||||
|
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
intentFlags |= PendingIntent.FLAG_MUTABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PendingIntent getRemoteReplyIntent(Context context, Recipient recipient, ReplyMethod replyMethod) {
|
public PendingIntent getRemoteReplyIntent(Context context, Recipient recipient, ReplyMethod replyMethod) {
|
||||||
@ -127,7 +134,12 @@ public class NotificationState {
|
|||||||
intent.putExtra(RemoteReplyReceiver.REPLY_METHOD, replyMethod);
|
intent.putExtra(RemoteReplyReceiver.REPLY_METHOD, replyMethod);
|
||||||
intent.setPackage(context.getPackageName());
|
intent.setPackage(context.getPackageName());
|
||||||
|
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
intentFlags |= PendingIntent.FLAG_MUTABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PendingIntent getAndroidAutoReplyIntent(Context context, Recipient recipient) {
|
public PendingIntent getAndroidAutoReplyIntent(Context context, Recipient recipient) {
|
||||||
@ -141,7 +153,12 @@ public class NotificationState {
|
|||||||
intent.putExtra(AndroidAutoReplyReceiver.THREAD_ID_EXTRA, (long)threads.toArray()[0]);
|
intent.putExtra(AndroidAutoReplyReceiver.THREAD_ID_EXTRA, (long)threads.toArray()[0]);
|
||||||
intent.setPackage(context.getPackageName());
|
intent.setPackage(context.getPackageName());
|
||||||
|
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
intentFlags |= PendingIntent.FLAG_MUTABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PendingIntent getAndroidAutoHeardIntent(Context context, int notificationId) {
|
public PendingIntent getAndroidAutoHeardIntent(Context context, int notificationId) {
|
||||||
@ -160,7 +177,12 @@ public class NotificationState {
|
|||||||
intent.putExtra(AndroidAutoHeardReceiver.NOTIFICATION_ID_EXTRA, notificationId);
|
intent.putExtra(AndroidAutoHeardReceiver.NOTIFICATION_ID_EXTRA, notificationId);
|
||||||
intent.setPackage(context.getPackageName());
|
intent.setPackage(context.getPackageName());
|
||||||
|
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
intentFlags |= PendingIntent.FLAG_MUTABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PendingIntent getQuickReplyIntent(Context context, Recipient recipient) {
|
public PendingIntent getQuickReplyIntent(Context context, Recipient recipient) {
|
||||||
@ -171,7 +193,12 @@ public class NotificationState {
|
|||||||
intent.putExtra(ConversationActivityV2.THREAD_ID, (long)threads.toArray()[0]);
|
intent.putExtra(ConversationActivityV2.THREAD_ID, (long)threads.toArray()[0]);
|
||||||
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
|
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
|
||||||
|
|
||||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
intentFlags |= PendingIntent.FLAG_MUTABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PendingIntent.getActivity(context, 0, intent, intentFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PendingIntent getDeleteIntent(Context context) {
|
public PendingIntent getDeleteIntent(Context context) {
|
||||||
@ -190,7 +217,12 @@ public class NotificationState {
|
|||||||
intent.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms);
|
intent.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms);
|
||||||
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
|
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
|
||||||
|
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
intentFlags |= PendingIntent.FLAG_MUTABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,12 +4,13 @@ package org.thoughtcrime.securesms.notifications;
|
|||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
|
||||||
import org.session.libsession.utilities.NotificationPrivacyPreference;
|
import org.session.libsession.utilities.NotificationPrivacyPreference;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ public class PendingMessageNotificationBuilder extends AbstractNotificationBuild
|
|||||||
setContentText(context.getString(R.string.MessageNotifier_you_have_pending_signal_messages));
|
setContentText(context.getString(R.string.MessageNotifier_you_have_pending_signal_messages));
|
||||||
setTicker(context.getString(R.string.MessageNotifier_you_have_pending_signal_messages));
|
setTicker(context.getString(R.string.MessageNotifier_you_have_pending_signal_messages));
|
||||||
|
|
||||||
setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
|
setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE));
|
||||||
setAutoCancel(true);
|
setAutoCancel(true);
|
||||||
setAlarms(null, Recipient.VibrateState.DEFAULT);
|
setAlarms(null, Recipient.VibrateState.DEFAULT);
|
||||||
|
|
||||||
|
@ -117,15 +117,15 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||||||
setNumber(messageCount);
|
setNumber(messageCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPrimaryMessageBody(@NonNull Recipient threadRecipients,
|
public void setPrimaryMessageBody(@NonNull Recipient threadRecipient,
|
||||||
@NonNull Recipient individualRecipient,
|
@NonNull Recipient individualRecipient,
|
||||||
@NonNull CharSequence message,
|
@NonNull CharSequence message,
|
||||||
@Nullable SlideDeck slideDeck)
|
@Nullable SlideDeck slideDeck)
|
||||||
{
|
{
|
||||||
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
||||||
|
|
||||||
if (privacy.isDisplayContact() && threadRecipients.isOpenGroupRecipient()) {
|
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
|
||||||
String displayName = getOpenGroupDisplayName(individualRecipient);
|
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient());
|
||||||
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,8 +214,8 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||||||
{
|
{
|
||||||
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
|
||||||
|
|
||||||
if (privacy.isDisplayContact() && threadRecipient.isOpenGroupRecipient()) {
|
if (privacy.isDisplayContact() && threadRecipient.isGroupRecipient()) {
|
||||||
String displayName = getOpenGroupDisplayName(individualRecipient);
|
String displayName = getGroupDisplayName(individualRecipient, threadRecipient.isOpenGroupRecipient());
|
||||||
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
stringBuilder.append(Util.getBoldedString(displayName + ": "));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,14 +334,15 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param recipient the * individual * recipient for which to get the open group display name.
|
* @param recipient the * individual * recipient for which to get the display name.
|
||||||
|
* @param openGroupRecipient whether in an open group context
|
||||||
*/
|
*/
|
||||||
private String getOpenGroupDisplayName(Recipient recipient) {
|
private String getGroupDisplayName(Recipient recipient, boolean openGroupRecipient) {
|
||||||
SessionContactDatabase contactDB = DatabaseComponent.get(context).sessionContactDatabase();
|
SessionContactDatabase contactDB = DatabaseComponent.get(context).sessionContactDatabase();
|
||||||
String sessionID = recipient.getAddress().serialize();
|
String sessionID = recipient.getAddress().serialize();
|
||||||
Contact contact = contactDB.getContactWithSessionID(sessionID);
|
Contact contact = contactDB.getContactWithSessionID(sessionID);
|
||||||
if (contact == null) { return sessionID; }
|
if (contact == null) { return sessionID; }
|
||||||
String displayName = contact.displayName(Contact.ContactContext.OPEN_GROUP);
|
String displayName = contact.displayName(openGroupRecipient ? Contact.ContactContext.OPEN_GROUP : Contact.ContactContext.REGULAR);
|
||||||
if (displayName == null) { return sessionID; }
|
if (displayName == null) { return sessionID; }
|
||||||
return displayName;
|
return displayName;
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,10 @@ import kotlinx.coroutines.launch
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
|
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
|
||||||
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
|
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
|
||||||
|
import org.session.libsession.snode.SnodeModule
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.crypto.MnemonicCodec
|
import org.session.libsignal.crypto.MnemonicCodec
|
||||||
|
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
import org.session.libsignal.utilities.KeyHelper
|
import org.session.libsignal.utilities.KeyHelper
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
@ -39,6 +41,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
|||||||
|
|
||||||
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||||
private lateinit var binding: ActivityLinkDeviceBinding
|
private lateinit var binding: ActivityLinkDeviceBinding
|
||||||
|
internal val database: LokiAPIDatabaseProtocol
|
||||||
|
get() = SnodeModule.shared.storage
|
||||||
private val adapter = LinkDeviceActivityAdapter(this)
|
private val adapter = LinkDeviceActivityAdapter(this)
|
||||||
private var restoreJob: Job? = null
|
private var restoreJob: Job? = null
|
||||||
|
|
||||||
@ -99,6 +103,11 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
|||||||
if (restoreJob?.isActive == true) return
|
if (restoreJob?.isActive == true) return
|
||||||
|
|
||||||
restoreJob = lifecycleScope.launch {
|
restoreJob = lifecycleScope.launch {
|
||||||
|
// This is here to resolve a case where the app restarts before a user completes onboarding
|
||||||
|
// which can result in an invalid database state
|
||||||
|
database.clearAllLastMessageHashes()
|
||||||
|
database.clearReceivedMessageHashValues()
|
||||||
|
|
||||||
// RestoreActivity handles seed this way
|
// RestoreActivity handles seed this way
|
||||||
val keyPairGenerationResult = KeyPairUtilities.generate(seed)
|
val keyPairGenerationResult = KeyPairUtilities.generate(seed)
|
||||||
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
|
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
|
||||||
|
@ -52,7 +52,7 @@ class PNModeActivity : BaseActionBarActivity() {
|
|||||||
toggleFCM()
|
toggleFCM()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_pn_mode, menu)
|
menuInflater.inflate(R.menu.menu_pn_mode, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,10 @@ import android.view.View
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding
|
import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding
|
||||||
|
import org.session.libsession.snode.SnodeModule
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.crypto.MnemonicCodec
|
import org.session.libsignal.crypto.MnemonicCodec
|
||||||
|
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
import org.session.libsignal.utilities.KeyHelper
|
import org.session.libsignal.utilities.KeyHelper
|
||||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||||
@ -26,6 +28,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
|||||||
|
|
||||||
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||||
private lateinit var binding: ActivityRecoveryPhraseRestoreBinding
|
private lateinit var binding: ActivityRecoveryPhraseRestoreBinding
|
||||||
|
internal val database: LokiAPIDatabaseProtocol
|
||||||
|
get() = SnodeModule.shared.storage
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -64,6 +68,11 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
|||||||
private fun restore() {
|
private fun restore() {
|
||||||
val mnemonic = binding.mnemonicEditText.text.toString()
|
val mnemonic = binding.mnemonicEditText.text.toString()
|
||||||
try {
|
try {
|
||||||
|
// This is here to resolve a case where the app restarts before a user completes onboarding
|
||||||
|
// which can result in an invalid database state
|
||||||
|
database.clearAllLastMessageHashes()
|
||||||
|
database.clearReceivedMessageHashValues()
|
||||||
|
|
||||||
val loadFileContents: (String) -> String = { fileName ->
|
val loadFileContents: (String) -> String = { fileName ->
|
||||||
MnemonicUtilities.loadFileContents(this, fileName)
|
MnemonicUtilities.loadFileContents(this, fileName)
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,10 @@ import android.widget.Toast
|
|||||||
import com.goterl.lazysodium.utils.KeyPair
|
import com.goterl.lazysodium.utils.KeyPair
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityRegisterBinding
|
import network.loki.messenger.databinding.ActivityRegisterBinding
|
||||||
|
import org.session.libsession.snode.SnodeModule
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
|
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||||
import org.session.libsignal.utilities.KeyHelper
|
import org.session.libsignal.utilities.KeyHelper
|
||||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
@ -29,6 +31,8 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
|||||||
|
|
||||||
class RegisterActivity : BaseActionBarActivity() {
|
class RegisterActivity : BaseActionBarActivity() {
|
||||||
private lateinit var binding: ActivityRegisterBinding
|
private lateinit var binding: ActivityRegisterBinding
|
||||||
|
internal val database: LokiAPIDatabaseProtocol
|
||||||
|
get() = SnodeModule.shared.storage
|
||||||
private var seed: ByteArray? = null
|
private var seed: ByteArray? = null
|
||||||
private var ed25519KeyPair: KeyPair? = null
|
private var ed25519KeyPair: KeyPair? = null
|
||||||
private var x25519KeyPair: ECKeyPair? = null
|
private var x25519KeyPair: ECKeyPair? = null
|
||||||
@ -109,6 +113,11 @@ class RegisterActivity : BaseActionBarActivity() {
|
|||||||
|
|
||||||
// region Interaction
|
// region Interaction
|
||||||
private fun register() {
|
private fun register() {
|
||||||
|
// This is here to resolve a case where the app restarts before a user completes onboarding
|
||||||
|
// which can result in an invalid database state
|
||||||
|
database.clearAllLastMessageHashes()
|
||||||
|
database.clearReceivedMessageHashValues()
|
||||||
|
|
||||||
KeyPairUtilities.store(this, seed!!, ed25519KeyPair!!, x25519KeyPair!!)
|
KeyPairUtilities.store(this, seed!!, ed25519KeyPair!!, x25519KeyPair!!)
|
||||||
val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey
|
val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey
|
||||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||||
|
@ -41,8 +41,7 @@ class HelpSettingsFragment: CorrectedPreferenceFragment() {
|
|||||||
addPreferencesFromResource(R.xml.preferences_help)
|
addPreferencesFromResource(R.xml.preferences_help)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||||
preference ?: return false
|
|
||||||
return when (preference.key) {
|
return when (preference.key) {
|
||||||
EXPORT_LOGS -> {
|
EXPORT_LOGS -> {
|
||||||
shareLogs()
|
shareLogs()
|
||||||
|
@ -35,6 +35,7 @@ interface ConversationRepository {
|
|||||||
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
||||||
fun saveDraft(threadId: Long, text: String)
|
fun saveDraft(threadId: Long, text: String)
|
||||||
fun getDraft(threadId: Long): String?
|
fun getDraft(threadId: Long): String?
|
||||||
|
fun clearDrafts(threadId: Long)
|
||||||
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
||||||
fun setBlocked(recipient: Recipient, blocked: Boolean)
|
fun setBlocked(recipient: Recipient, blocked: Boolean)
|
||||||
fun deleteLocally(recipient: Recipient, message: MessageRecord)
|
fun deleteLocally(recipient: Recipient, message: MessageRecord)
|
||||||
@ -98,10 +99,13 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
|
|
||||||
override fun getDraft(threadId: Long): String? {
|
override fun getDraft(threadId: Long): String? {
|
||||||
val drafts = draftDb.getDrafts(threadId)
|
val drafts = draftDb.getDrafts(threadId)
|
||||||
draftDb.clearDrafts(threadId)
|
|
||||||
return drafts.find { it.type == DraftDatabase.Draft.TEXT }?.value
|
return drafts.find { it.type == DraftDatabase.Draft.TEXT }?.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun clearDrafts(threadId: Long) {
|
||||||
|
draftDb.clearDrafts(threadId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun inviteContacts(threadId: Long, contacts: List<Recipient>) {
|
override fun inviteContacts(threadId: Long, contacts: List<Recipient>) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return
|
||||||
for (contact in contacts) {
|
for (contact in contacts) {
|
||||||
|
@ -301,10 +301,10 @@ public class SearchRepository {
|
|||||||
Recipient conversationRecipient = Recipient.from(context, conversationAddress, false);
|
Recipient conversationRecipient = Recipient.from(context, conversationAddress, false);
|
||||||
Recipient messageRecipient = Recipient.from(context, messageAddress, false);
|
Recipient messageRecipient = Recipient.from(context, messageAddress, false);
|
||||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.SNIPPET));
|
String body = cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.SNIPPET));
|
||||||
long receivedMs = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED));
|
long sentMs = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT));
|
||||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.THREAD_ID));
|
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.THREAD_ID));
|
||||||
|
|
||||||
return new MessageResult(conversationRecipient, messageRecipient, body, threadId, receivedMs);
|
return new MessageResult(conversationRecipient, messageRecipient, body, threadId, sentMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,18 +13,18 @@ public class MessageResult {
|
|||||||
public final Recipient messageRecipient;
|
public final Recipient messageRecipient;
|
||||||
public final String bodySnippet;
|
public final String bodySnippet;
|
||||||
public final long threadId;
|
public final long threadId;
|
||||||
public final long receivedTimestampMs;
|
public final long sentTimestampMs;
|
||||||
|
|
||||||
public MessageResult(@NonNull Recipient conversationRecipient,
|
public MessageResult(@NonNull Recipient conversationRecipient,
|
||||||
@NonNull Recipient messageRecipient,
|
@NonNull Recipient messageRecipient,
|
||||||
@NonNull String bodySnippet,
|
@NonNull String bodySnippet,
|
||||||
long threadId,
|
long threadId,
|
||||||
long receivedTimestampMs)
|
long sentTimestampMs)
|
||||||
{
|
{
|
||||||
this.conversationRecipient = conversationRecipient;
|
this.conversationRecipient = conversationRecipient;
|
||||||
this.messageRecipient = messageRecipient;
|
this.messageRecipient = messageRecipient;
|
||||||
this.bodySnippet = bodySnippet;
|
this.bodySnippet = bodySnippet;
|
||||||
this.threadId = threadId;
|
this.threadId = threadId;
|
||||||
this.receivedTimestampMs = receivedTimestampMs;
|
this.sentTimestampMs = sentTimestampMs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,12 @@ import android.content.IntentFilter;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.Icon;
|
import android.graphics.drawable.Icon;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.service.chooser.ChooserTarget;
|
import android.service.chooser.ChooserTarget;
|
||||||
import android.service.chooser.ChooserTargetService;
|
import android.service.chooser.ChooserTargetService;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
@ -28,7 +26,6 @@ import java.util.LinkedList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
public class DirectShareService extends ChooserTargetService {
|
public class DirectShareService extends ChooserTargetService {
|
||||||
|
|
||||||
private static final String TAG = DirectShareService.class.getSimpleName();
|
private static final String TAG = DirectShareService.class.getSimpleName();
|
||||||
@ -40,9 +37,8 @@ public class DirectShareService extends ChooserTargetService {
|
|||||||
List<ChooserTarget> results = new LinkedList<>();
|
List<ChooserTarget> results = new LinkedList<>();
|
||||||
ComponentName componentName = new ComponentName(this, ShareActivity.class);
|
ComponentName componentName = new ComponentName(this, ShareActivity.class);
|
||||||
ThreadDatabase threadDatabase = DatabaseComponent.get(this).threadDatabase();
|
ThreadDatabase threadDatabase = DatabaseComponent.get(this).threadDatabase();
|
||||||
Cursor cursor = threadDatabase.getDirectShareList();
|
|
||||||
|
|
||||||
try {
|
try (Cursor cursor = threadDatabase.getDirectShareList()) {
|
||||||
ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor);
|
ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor);
|
||||||
ThreadRecord record;
|
ThreadRecord record;
|
||||||
|
|
||||||
@ -84,8 +80,6 @@ public class DirectShareService extends ChooserTargetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
} finally {
|
|
||||||
if (cursor != null) cursor.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ public class ExpirationListener extends BroadcastReceiver {
|
|||||||
|
|
||||||
public static void setAlarm(Context context, long waitTimeMillis) {
|
public static void setAlarm(Context context, long waitTimeMillis) {
|
||||||
Intent intent = new Intent(context, ExpirationListener.class);
|
Intent intent = new Intent(context, ExpirationListener.class);
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||||
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
|
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
|
||||||
alarmManager.cancel(pendingIntent);
|
alarmManager.cancel(pendingIntent);
|
||||||
|
@ -6,6 +6,7 @@ import android.app.Service;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -13,9 +14,9 @@ import androidx.core.app.NotificationCompat;
|
|||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.session.libsignal.utilities.guava.Preconditions;
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.session.libsignal.utilities.guava.Preconditions;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
@ -90,7 +91,7 @@ public class GenericForegroundService extends Service {
|
|||||||
startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this, channelId)
|
startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this, channelId)
|
||||||
.setSmallIcon(iconRes)
|
.setSmallIcon(iconRes)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, HomeActivity.class), 0))
|
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, HomeActivity.class), PendingIntent.FLAG_IMMUTABLE))
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,18 +29,19 @@ import android.os.AsyncTask;
|
|||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
|
import org.session.libsession.utilities.ServiceUtil;
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
|
import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
|
||||||
import org.thoughtcrime.securesms.DummyActivity;
|
import org.thoughtcrime.securesms.DummyActivity;
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.session.libsession.utilities.ServiceUtil;
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@ -255,18 +256,18 @@ public class KeyCachingService extends Service {
|
|||||||
private PendingIntent buildLockIntent() {
|
private PendingIntent buildLockIntent() {
|
||||||
Intent intent = new Intent(this, KeyCachingService.class);
|
Intent intent = new Intent(this, KeyCachingService.class);
|
||||||
intent.setAction(PASSPHRASE_EXPIRED_EVENT);
|
intent.setAction(PASSPHRASE_EXPIRED_EVENT);
|
||||||
return PendingIntent.getService(getApplicationContext(), 0, intent, 0);
|
return PendingIntent.getService(getApplicationContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PendingIntent buildLaunchIntent() {
|
private PendingIntent buildLaunchIntent() {
|
||||||
Intent intent = new Intent(this, HomeActivity.class);
|
Intent intent = new Intent(this, HomeActivity.class);
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
return PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
|
return PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PendingIntent buildExpirationPendingIntent(@NonNull Context context) {
|
private static PendingIntent buildExpirationPendingIntent(@NonNull Context context) {
|
||||||
Intent expirationIntent = new Intent(PASSPHRASE_EXPIRED_EVENT, null, context, KeyCachingService.class);
|
Intent expirationIntent = new Intent(PASSPHRASE_EXPIRED_EVENT, null, context, KeyCachingService.class);
|
||||||
return PendingIntent.getService(context, 0, expirationIntent, 0);
|
return PendingIntent.getService(context, 0, expirationIntent, PendingIntent.FLAG_IMMUTABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -6,6 +6,7 @@ import android.app.PendingIntent;
|
|||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
|
|
||||||
public abstract class PersistentAlarmManagerListener extends BroadcastReceiver {
|
public abstract class PersistentAlarmManagerListener extends BroadcastReceiver {
|
||||||
@ -21,7 +22,7 @@ public abstract class PersistentAlarmManagerListener extends BroadcastReceiver {
|
|||||||
long scheduledTime = getNextScheduledExecutionTime(context);
|
long scheduledTime = getNextScheduledExecutionTime(context);
|
||||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||||
Intent alarmIntent = new Intent(context, getClass());
|
Intent alarmIntent = new Intent(context, getClass());
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, 0);
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_IMMUTABLE);
|
||||||
|
|
||||||
if (System.currentTimeMillis() >= scheduledTime) {
|
if (System.currentTimeMillis() >= scheduledTime) {
|
||||||
scheduledTime = onAlarm(context, scheduledTime);
|
scheduledTime = onAlarm(context, scheduledTime);
|
||||||
|
@ -12,21 +12,22 @@ import android.net.Uri;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
|
||||||
import org.thoughtcrime.securesms.util.FileProviderUtil;
|
|
||||||
import org.session.libsession.utilities.FileUtils;
|
import org.session.libsession.utilities.FileUtils;
|
||||||
import org.session.libsignal.utilities.Hex;
|
|
||||||
import org.session.libsession.utilities.ServiceUtil;
|
import org.session.libsession.utilities.ServiceUtil;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
import org.session.libsignal.utilities.Hex;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
|
import org.thoughtcrime.securesms.util.FileProviderUtil;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
|
||||||
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class UpdateApkReadyListener extends BroadcastReceiver {
|
public class UpdateApkReadyListener extends BroadcastReceiver {
|
||||||
|
|
||||||
private static final String TAG = UpdateApkReadyListener.class.getSimpleName();
|
private static final String TAG = UpdateApkReadyListener.class.getSimpleName();
|
||||||
@ -61,7 +62,7 @@ public class UpdateApkReadyListener extends BroadcastReceiver {
|
|||||||
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
intent.setData(uri);
|
intent.setData(uri);
|
||||||
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||||
|
|
||||||
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.APP_UPDATES)
|
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.APP_UPDATES)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
@ -7,11 +7,15 @@ import android.content.Intent
|
|||||||
import android.content.Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
|
import android.content.Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.ResultReceiver
|
import android.os.ResultReceiver
|
||||||
import android.telephony.PhoneStateListener
|
import android.telephony.PhoneStateListener
|
||||||
|
import android.telephony.PhoneStateListener.LISTEN_NONE
|
||||||
import android.telephony.TelephonyManager
|
import android.telephony.TelephonyManager
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@ -27,30 +31,13 @@ import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_IN
|
|||||||
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_PRE_OFFER
|
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_PRE_OFFER
|
||||||
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING
|
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING
|
||||||
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING
|
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING
|
||||||
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand
|
import org.thoughtcrime.securesms.webrtc.*
|
||||||
import org.thoughtcrime.securesms.webrtc.CallManager
|
|
||||||
import org.thoughtcrime.securesms.webrtc.CallViewModel
|
|
||||||
import org.thoughtcrime.securesms.webrtc.HangUpRtcOnPstnCallAnsweredListener
|
|
||||||
import org.thoughtcrime.securesms.webrtc.IncomingPstnCallReceiver
|
|
||||||
import org.thoughtcrime.securesms.webrtc.NetworkChangeReceiver
|
|
||||||
import org.thoughtcrime.securesms.webrtc.PeerConnectionException
|
|
||||||
import org.thoughtcrime.securesms.webrtc.PowerButtonReceiver
|
|
||||||
import org.thoughtcrime.securesms.webrtc.ProximityLockRelease
|
|
||||||
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager
|
|
||||||
import org.thoughtcrime.securesms.webrtc.WiredHeadsetStateReceiver
|
|
||||||
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
|
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
|
||||||
import org.thoughtcrime.securesms.webrtc.data.Event
|
import org.thoughtcrime.securesms.webrtc.data.Event
|
||||||
import org.thoughtcrime.securesms.webrtc.locks.LockManager
|
import org.thoughtcrime.securesms.webrtc.locks.LockManager
|
||||||
import org.webrtc.DataChannel
|
import org.webrtc.*
|
||||||
import org.webrtc.IceCandidate
|
import org.webrtc.PeerConnection.IceConnectionState.*
|
||||||
import org.webrtc.MediaStream
|
import java.util.*
|
||||||
import org.webrtc.PeerConnection
|
|
||||||
import org.webrtc.PeerConnection.IceConnectionState.CONNECTED
|
|
||||||
import org.webrtc.PeerConnection.IceConnectionState.DISCONNECTED
|
|
||||||
import org.webrtc.PeerConnection.IceConnectionState.FAILED
|
|
||||||
import org.webrtc.RtpReceiver
|
|
||||||
import org.webrtc.SessionDescription
|
|
||||||
import java.util.UUID
|
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.ScheduledFuture
|
import java.util.concurrent.ScheduledFuture
|
||||||
@ -59,7 +46,7 @@ import javax.inject.Inject
|
|||||||
import org.thoughtcrime.securesms.webrtc.data.State as CallState
|
import org.thoughtcrime.securesms.webrtc.data.State as CallState
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
class WebRtcCallService : Service(), CallManager.WebRtcListener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@ -107,7 +94,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
private const val RECONNECT_SECONDS = 5L
|
private const val RECONNECT_SECONDS = 5L
|
||||||
private const val MAX_RECONNECTS = 5
|
private const val MAX_RECONNECTS = 5
|
||||||
|
|
||||||
fun cameraEnabled(context: Context, enabled: Boolean) = Intent(context, WebRtcCallService::class.java)
|
fun cameraEnabled(context: Context, enabled: Boolean) =
|
||||||
|
Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_SET_MUTE_VIDEO)
|
.setAction(ACTION_SET_MUTE_VIDEO)
|
||||||
.putExtra(EXTRA_MUTE, !enabled)
|
.putExtra(EXTRA_MUTE, !enabled)
|
||||||
|
|
||||||
@ -117,15 +105,23 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
fun acceptCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java)
|
fun acceptCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_ANSWER_CALL)
|
.setAction(ACTION_ANSWER_CALL)
|
||||||
|
|
||||||
fun microphoneIntent(context: Context, enabled: Boolean) = Intent(context, WebRtcCallService::class.java)
|
fun microphoneIntent(context: Context, enabled: Boolean) =
|
||||||
|
Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_SET_MUTE_AUDIO)
|
.setAction(ACTION_SET_MUTE_AUDIO)
|
||||||
.putExtra(EXTRA_MUTE, !enabled)
|
.putExtra(EXTRA_MUTE, !enabled)
|
||||||
|
|
||||||
fun createCall(context: Context, recipient: Recipient) = Intent(context, WebRtcCallService::class.java)
|
fun createCall(context: Context, recipient: Recipient) =
|
||||||
|
Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_OUTGOING_CALL)
|
.setAction(ACTION_OUTGOING_CALL)
|
||||||
.putExtra(EXTRA_RECIPIENT_ADDRESS, recipient.address)
|
.putExtra(EXTRA_RECIPIENT_ADDRESS, recipient.address)
|
||||||
|
|
||||||
fun incomingCall(context: Context, address: Address, sdp: String, callId: UUID, callTime: Long) =
|
fun incomingCall(
|
||||||
|
context: Context,
|
||||||
|
address: Address,
|
||||||
|
sdp: String,
|
||||||
|
callId: UUID,
|
||||||
|
callTime: Long
|
||||||
|
) =
|
||||||
Intent(context, WebRtcCallService::class.java)
|
Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_INCOMING_RING)
|
.setAction(ACTION_INCOMING_RING)
|
||||||
.putExtra(EXTRA_RECIPIENT_ADDRESS, address)
|
.putExtra(EXTRA_RECIPIENT_ADDRESS, address)
|
||||||
@ -147,22 +143,33 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
.putExtra(EXTRA_CALL_ID, callId)
|
.putExtra(EXTRA_CALL_ID, callId)
|
||||||
.putExtra(EXTRA_TIMESTAMP, callTime)
|
.putExtra(EXTRA_TIMESTAMP, callTime)
|
||||||
|
|
||||||
fun iceCandidates(context: Context, address: Address, iceCandidates: List<IceCandidate>, callId: UUID) =
|
fun iceCandidates(
|
||||||
|
context: Context,
|
||||||
|
address: Address,
|
||||||
|
iceCandidates: List<IceCandidate>,
|
||||||
|
callId: UUID
|
||||||
|
) =
|
||||||
Intent(context, WebRtcCallService::class.java)
|
Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_ICE_MESSAGE)
|
.setAction(ACTION_ICE_MESSAGE)
|
||||||
.putExtra(EXTRA_CALL_ID, callId)
|
.putExtra(EXTRA_CALL_ID, callId)
|
||||||
.putExtra(EXTRA_ICE_SDP, iceCandidates.map(IceCandidate::sdp).toTypedArray())
|
.putExtra(EXTRA_ICE_SDP, iceCandidates.map(IceCandidate::sdp).toTypedArray())
|
||||||
.putExtra(EXTRA_ICE_SDP_LINE_INDEX, iceCandidates.map(IceCandidate::sdpMLineIndex).toIntArray())
|
.putExtra(
|
||||||
|
EXTRA_ICE_SDP_LINE_INDEX,
|
||||||
|
iceCandidates.map(IceCandidate::sdpMLineIndex).toIntArray()
|
||||||
|
)
|
||||||
.putExtra(EXTRA_ICE_SDP_MID, iceCandidates.map(IceCandidate::sdpMid).toTypedArray())
|
.putExtra(EXTRA_ICE_SDP_MID, iceCandidates.map(IceCandidate::sdpMid).toTypedArray())
|
||||||
.putExtra(EXTRA_RECIPIENT_ADDRESS, address)
|
.putExtra(EXTRA_RECIPIENT_ADDRESS, address)
|
||||||
|
|
||||||
fun denyCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java).setAction(ACTION_DENY_CALL)
|
fun denyCallIntent(context: Context) =
|
||||||
|
Intent(context, WebRtcCallService::class.java).setAction(ACTION_DENY_CALL)
|
||||||
|
|
||||||
fun remoteHangupIntent(context: Context, callId: UUID) = Intent(context, WebRtcCallService::class.java)
|
fun remoteHangupIntent(context: Context, callId: UUID) =
|
||||||
|
Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_REMOTE_HANGUP)
|
.setAction(ACTION_REMOTE_HANGUP)
|
||||||
.putExtra(EXTRA_CALL_ID, callId)
|
.putExtra(EXTRA_CALL_ID, callId)
|
||||||
|
|
||||||
fun hangupIntent(context: Context) = Intent(context, WebRtcCallService::class.java).setAction(ACTION_LOCAL_HANGUP)
|
fun hangupIntent(context: Context) =
|
||||||
|
Intent(context, WebRtcCallService::class.java).setAction(ACTION_LOCAL_HANGUP)
|
||||||
|
|
||||||
fun sendAudioManagerCommand(context: Context, command: AudioManagerCommand) {
|
fun sendAudioManagerCommand(context: Context, command: AudioManagerCommand) {
|
||||||
val intent = Intent(context, WebRtcCallService::class.java)
|
val intent = Intent(context, WebRtcCallService::class.java)
|
||||||
@ -187,7 +194,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject lateinit var callManager: CallManager
|
@Inject
|
||||||
|
lateinit var callManager: CallManager
|
||||||
|
|
||||||
private var wantsToAnswer = false
|
private var wantsToAnswer = false
|
||||||
private var currentTimeouts = 0
|
private var currentTimeouts = 0
|
||||||
@ -198,8 +206,17 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
private val lockManager by lazy { LockManager(this) }
|
private val lockManager by lazy { LockManager(this) }
|
||||||
private val serviceExecutor = Executors.newSingleThreadExecutor()
|
private val serviceExecutor = Executors.newSingleThreadExecutor()
|
||||||
private val timeoutExecutor = Executors.newScheduledThreadPool(1)
|
private val timeoutExecutor = Executors.newScheduledThreadPool(1)
|
||||||
private val hangupOnCallAnswered = HangUpRtcOnPstnCallAnsweredListener {
|
|
||||||
startService(hangupIntent(this))
|
private val hangupOnCallAnswered by lazy {
|
||||||
|
HangUpRtcOnPstnCallAnsweredListener {
|
||||||
|
ContextCompat.startForegroundService(this, hangupIntent(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val hangupTelephonyCallback by lazy {
|
||||||
|
HangUpRtcTelephonyCallback {
|
||||||
|
ContextCompat.startForegroundService(this, hangupIntent(this))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var networkChangedReceiver: NetworkChangeReceiver? = null
|
private var networkChangedReceiver: NetworkChangeReceiver? = null
|
||||||
@ -257,7 +274,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
val action = intent.action
|
val action = intent.action
|
||||||
Log.i("Loki", "Handling ${intent.action}")
|
Log.i("Loki", "Handling ${intent.action}")
|
||||||
when {
|
when {
|
||||||
action == ACTION_INCOMING_RING && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> handleNewOffer(intent)
|
action == ACTION_INCOMING_RING && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> handleNewOffer(
|
||||||
|
intent
|
||||||
|
)
|
||||||
action == ACTION_PRE_OFFER && isIdle() -> handlePreOffer(intent)
|
action == ACTION_PRE_OFFER && isIdle() -> handlePreOffer(intent)
|
||||||
action == ACTION_INCOMING_RING && isBusy(intent) -> handleBusyCall(intent)
|
action == ACTION_INCOMING_RING && isBusy(intent) -> handleBusyCall(intent)
|
||||||
action == ACTION_INCOMING_RING && isPreOffer() -> handleIncomingRing(intent)
|
action == ACTION_INCOMING_RING && isPreOffer() -> handleIncomingRing(intent)
|
||||||
@ -271,7 +290,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
action == ACTION_FLIP_CAMERA -> handleSetCameraFlip(intent)
|
action == ACTION_FLIP_CAMERA -> handleSetCameraFlip(intent)
|
||||||
action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChanged(intent)
|
action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChanged(intent)
|
||||||
action == ACTION_SCREEN_OFF -> handleScreenOffChange(intent)
|
action == ACTION_SCREEN_OFF -> handleScreenOffChange(intent)
|
||||||
action == ACTION_RESPONSE_MESSAGE && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> handleResponseMessage(intent)
|
action == ACTION_RESPONSE_MESSAGE && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> handleResponseMessage(
|
||||||
|
intent
|
||||||
|
)
|
||||||
action == ACTION_RESPONSE_MESSAGE -> handleResponseMessage(intent)
|
action == ACTION_RESPONSE_MESSAGE -> handleResponseMessage(intent)
|
||||||
action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent)
|
action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent)
|
||||||
action == ACTION_ICE_CONNECTED -> handleIceConnected(intent)
|
action == ACTION_ICE_CONNECTED -> handleIceConnected(intent)
|
||||||
@ -292,8 +313,15 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
registerIncomingPstnCallReceiver()
|
registerIncomingPstnCallReceiver()
|
||||||
registerWiredHeadsetStateReceiver()
|
registerWiredHeadsetStateReceiver()
|
||||||
registerWantsToAnswerReceiver()
|
registerWantsToAnswerReceiver()
|
||||||
|
if (checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
getSystemService(TelephonyManager::class.java)
|
getSystemService(TelephonyManager::class.java)
|
||||||
.listen(hangupOnCallAnswered, PhoneStateListener.LISTEN_CALL_STATE)
|
.listen(hangupOnCallAnswered, PhoneStateListener.LISTEN_CALL_STATE)
|
||||||
|
} else {
|
||||||
|
getSystemService(TelephonyManager::class.java)
|
||||||
|
.registerTelephonyCallback(serviceExecutor, hangupTelephonyCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
registerUncaughtExceptionHandler()
|
registerUncaughtExceptionHandler()
|
||||||
networkChangedReceiver = NetworkChangeReceiver(::networkChange)
|
networkChangedReceiver = NetworkChangeReceiver(::networkChange)
|
||||||
networkChangedReceiver!!.register(this)
|
networkChangedReceiver!!.register(this)
|
||||||
@ -317,7 +345,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
wantsToAnswerReceiver = receiver
|
wantsToAnswerReceiver = receiver
|
||||||
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, IntentFilter(ACTION_WANTS_TO_ANSWER))
|
LocalBroadcastManager.getInstance(this)
|
||||||
|
.registerReceiver(receiver, IntentFilter(ACTION_WANTS_TO_ANSWER))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerWiredHeadsetStateReceiver() {
|
private fun registerWiredHeadsetStateReceiver() {
|
||||||
@ -338,7 +367,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
|
|
||||||
private fun handleUpdateAudio(intent: Intent) {
|
private fun handleUpdateAudio(intent: Intent) {
|
||||||
val audioCommand = intent.getParcelableExtra<AudioManagerCommand>(EXTRA_AUDIO_COMMAND)!!
|
val audioCommand = intent.getParcelableExtra<AudioManagerCommand>(EXTRA_AUDIO_COMMAND)!!
|
||||||
if (callManager.currentConnectionState !in arrayOf(CallState.Connected, *CallState.PENDING_CONNECTION_STATES)) {
|
if (callManager.currentConnectionState !in arrayOf(
|
||||||
|
CallState.Connected,
|
||||||
|
*CallState.PENDING_CONNECTION_STATES
|
||||||
|
)
|
||||||
|
) {
|
||||||
Log.w(TAG, "handling audio command not in call")
|
Log.w(TAG, "handling audio command not in call")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -418,8 +451,15 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
callManager.initializeAudioForCall()
|
callManager.initializeAudioForCall()
|
||||||
callManager.startOutgoingRinger(OutgoingRinger.Type.RINGING)
|
callManager.startOutgoingRinger(OutgoingRinger.Type.RINGING)
|
||||||
setCallInProgressNotification(TYPE_OUTGOING_RINGING, callManager.recipient)
|
setCallInProgressNotification(TYPE_OUTGOING_RINGING, callManager.recipient)
|
||||||
callManager.insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_OUTGOING)
|
callManager.insertCallMessage(
|
||||||
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
recipient.address.serialize(),
|
||||||
|
CallMessageType.CALL_OUTGOING
|
||||||
|
)
|
||||||
|
scheduledTimeout = timeoutExecutor.schedule(
|
||||||
|
TimeoutRunnable(callId, this),
|
||||||
|
TIMEOUT_SECONDS,
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
)
|
||||||
callManager.setAudioEnabled(true)
|
callManager.setAudioEnabled(true)
|
||||||
|
|
||||||
val expectedState = callManager.currentConnectionState
|
val expectedState = callManager.currentConnectionState
|
||||||
@ -428,15 +468,21 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
try {
|
try {
|
||||||
val offerFuture = callManager.onOutgoingCall(this)
|
val offerFuture = callManager.onOutgoingCall(this)
|
||||||
offerFuture.fail { e ->
|
offerFuture.fail { e ->
|
||||||
if (isConsistentState(expectedState, expectedCallId, callManager.currentConnectionState, callManager.callId)) {
|
if (isConsistentState(
|
||||||
Log.e(TAG,e)
|
expectedState,
|
||||||
|
expectedCallId,
|
||||||
|
callManager.currentConnectionState,
|
||||||
|
callManager.callId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Log.e(TAG, e)
|
||||||
callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE)
|
callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE)
|
||||||
callManager.postConnectionError()
|
callManager.postConnectionError()
|
||||||
terminate()
|
terminate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG,e)
|
Log.e(TAG, e)
|
||||||
callManager.postConnectionError()
|
callManager.postConnectionError()
|
||||||
terminate()
|
terminate()
|
||||||
}
|
}
|
||||||
@ -475,7 +521,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
callManager.silenceIncomingRinger()
|
callManager.silenceIncomingRinger()
|
||||||
callManager.postViewModelState(CallViewModel.State.CALL_INCOMING)
|
callManager.postViewModelState(CallViewModel.State.CALL_INCOMING)
|
||||||
|
|
||||||
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
scheduledTimeout = timeoutExecutor.schedule(
|
||||||
|
TimeoutRunnable(callId, this),
|
||||||
|
TIMEOUT_SECONDS,
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
)
|
||||||
|
|
||||||
callManager.initializeAudioForCall()
|
callManager.initializeAudioForCall()
|
||||||
callManager.initializeVideo(this)
|
callManager.initializeVideo(this)
|
||||||
@ -486,7 +536,13 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
try {
|
try {
|
||||||
val answerFuture = callManager.onIncomingCall(this)
|
val answerFuture = callManager.onIncomingCall(this)
|
||||||
answerFuture.fail { e ->
|
answerFuture.fail { e ->
|
||||||
if (isConsistentState(expectedState,expectedCallId, callManager.currentConnectionState, callManager.callId)) {
|
if (isConsistentState(
|
||||||
|
expectedState,
|
||||||
|
expectedCallId,
|
||||||
|
callManager.currentConnectionState,
|
||||||
|
callManager.callId
|
||||||
|
)
|
||||||
|
) {
|
||||||
Log.e(TAG, e)
|
Log.e(TAG, e)
|
||||||
insertMissedCall(recipient, true)
|
insertMissedCall(recipient, true)
|
||||||
callManager.postConnectionError()
|
callManager.postConnectionError()
|
||||||
@ -496,7 +552,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING)
|
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING)
|
||||||
callManager.setAudioEnabled(true)
|
callManager.setAudioEnabled(true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG,e)
|
Log.e(TAG, e)
|
||||||
callManager.postConnectionError()
|
callManager.postConnectionError()
|
||||||
terminate()
|
terminate()
|
||||||
}
|
}
|
||||||
@ -517,6 +573,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
private fun handleRemoteHangup(intent: Intent) {
|
private fun handleRemoteHangup(intent: Intent) {
|
||||||
if (callManager.callId != getCallId(intent)) {
|
if (callManager.callId != getCallId(intent)) {
|
||||||
Log.e(TAG, "Hangup for non-active call...")
|
Log.e(TAG, "Hangup for non-active call...")
|
||||||
|
stopForeground(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,7 +611,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
val callId = getCallId(intent)
|
val callId = getCallId(intent)
|
||||||
val description = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION)
|
val description = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION)
|
||||||
callManager.handleResponseMessage(recipient, callId, SessionDescription(SessionDescription.Type.ANSWER, description))
|
callManager.handleResponseMessage(
|
||||||
|
recipient,
|
||||||
|
callId,
|
||||||
|
SessionDescription(SessionDescription.Type.ANSWER, description)
|
||||||
|
)
|
||||||
} catch (e: PeerConnectionException) {
|
} catch (e: PeerConnectionException) {
|
||||||
terminate()
|
terminate()
|
||||||
}
|
}
|
||||||
@ -566,7 +627,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
val sdpLineIndexes = intent.getIntArrayExtra(EXTRA_ICE_SDP_LINE_INDEX) ?: return
|
val sdpLineIndexes = intent.getIntArrayExtra(EXTRA_ICE_SDP_LINE_INDEX) ?: return
|
||||||
val sdps = intent.getStringArrayExtra(EXTRA_ICE_SDP) ?: return
|
val sdps = intent.getStringArrayExtra(EXTRA_ICE_SDP) ?: return
|
||||||
if (sdpMids.size != sdpLineIndexes.size || sdpLineIndexes.size != sdps.size) {
|
if (sdpMids.size != sdpLineIndexes.size || sdpLineIndexes.size != sdps.size) {
|
||||||
Log.w(TAG,"sdp info not of equal length")
|
Log.w(TAG, "sdp info not of equal length")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val iceCandidates = sdpMids.indices.map { index ->
|
val iceCandidates = sdpMids.indices.map { index ->
|
||||||
@ -596,7 +657,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
private fun handleIsInCallQuery(intent: Intent) {
|
private fun handleIsInCallQuery(intent: Intent) {
|
||||||
val listener = intent.getParcelableExtra<ResultReceiver>(EXTRA_RESULT_RECEIVER) ?: return
|
val listener = intent.getParcelableExtra<ResultReceiver>(EXTRA_RESULT_RECEIVER) ?: return
|
||||||
val currentState = callManager.currentConnectionState
|
val currentState = callManager.currentConnectionState
|
||||||
val isInCall = if (currentState in arrayOf(*CallState.PENDING_CONNECTION_STATES, CallState.Connected)) 1 else 0
|
val isInCall = if (currentState in arrayOf(
|
||||||
|
*CallState.PENDING_CONNECTION_STATES,
|
||||||
|
CallState.Connected
|
||||||
|
)
|
||||||
|
) 1 else 0
|
||||||
listener.send(isInCall, bundleOf())
|
listener.send(isInCall, bundleOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -615,10 +680,21 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
if (callId == getCallId(intent) && isNetworkAvailable && numTimeouts <= MAX_RECONNECTS) {
|
if (callId == getCallId(intent) && isNetworkAvailable && numTimeouts <= MAX_RECONNECTS) {
|
||||||
Log.i("Loki", "Trying to re-connect")
|
Log.i("Loki", "Trying to re-connect")
|
||||||
callManager.networkReestablished()
|
callManager.networkReestablished()
|
||||||
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
scheduledTimeout = timeoutExecutor.schedule(
|
||||||
|
TimeoutRunnable(callId, this),
|
||||||
|
TIMEOUT_SECONDS,
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
)
|
||||||
} else if (numTimeouts < MAX_RECONNECTS) {
|
} else if (numTimeouts < MAX_RECONNECTS) {
|
||||||
Log.i("Loki", "Network isn't available, timeouts == $numTimeouts out of $MAX_RECONNECTS")
|
Log.i(
|
||||||
scheduledReconnect = timeoutExecutor.schedule(CheckReconnectedRunnable(callId, this), RECONNECT_SECONDS, TimeUnit.SECONDS)
|
"Loki",
|
||||||
|
"Network isn't available, timeouts == $numTimeouts out of $MAX_RECONNECTS"
|
||||||
|
)
|
||||||
|
scheduledReconnect = timeoutExecutor.schedule(
|
||||||
|
CheckReconnectedRunnable(callId, this),
|
||||||
|
RECONNECT_SECONDS,
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Log.i("Loki", "Network isn't available, timing out")
|
Log.i("Loki", "Network isn't available, timing out")
|
||||||
handleLocalHangup(intent)
|
handleLocalHangup(intent)
|
||||||
@ -626,12 +702,15 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun handleCheckTimeout(intent: Intent) {
|
private fun handleCheckTimeout(intent: Intent) {
|
||||||
val callId = callManager.callId ?: return
|
val callId = callManager.callId ?: return
|
||||||
val callState = callManager.currentConnectionState
|
val callState = callManager.currentConnectionState
|
||||||
|
|
||||||
if (callId == getCallId(intent) && (callState !in arrayOf(CallState.Connected, CallState.Connecting))) {
|
if (callId == getCallId(intent) && (callState !in arrayOf(
|
||||||
|
CallState.Connected,
|
||||||
|
CallState.Connecting
|
||||||
|
))
|
||||||
|
) {
|
||||||
Log.w(TAG, "Timing out call: $callId")
|
Log.w(TAG, "Timing out call: $callId")
|
||||||
handleLocalHangup(intent)
|
handleLocalHangup(intent)
|
||||||
}
|
}
|
||||||
@ -665,7 +744,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
return Recipient.from(this, remoteAddress, true)
|
return Recipient.from(this, remoteAddress, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCallId(intent: Intent) : UUID {
|
private fun getCallId(intent: Intent): UUID {
|
||||||
return intent.getSerializableExtra(EXTRA_CALL_ID) as? UUID
|
return intent.getSerializableExtra(EXTRA_CALL_ID) as? UUID
|
||||||
?: throw AssertionError("No callId in intent!")
|
?: throw AssertionError("No callId in intent!")
|
||||||
}
|
}
|
||||||
@ -679,10 +758,13 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isIncomingMessageExpired(intent: Intent) =
|
private fun isIncomingMessageExpired(intent: Intent) =
|
||||||
System.currentTimeMillis() - intent.getLongExtra(EXTRA_TIMESTAMP, -1) > TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)
|
System.currentTimeMillis() - intent.getLongExtra(
|
||||||
|
EXTRA_TIMESTAMP,
|
||||||
|
-1
|
||||||
|
) > TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Log.d(TAG,"onDestroy()")
|
Log.d(TAG, "onDestroy()")
|
||||||
callManager.unregisterListener(this)
|
callManager.unregisterListener(this)
|
||||||
callReceiver?.let { receiver ->
|
callReceiver?.let { receiver ->
|
||||||
unregisterReceiver(receiver)
|
unregisterReceiver(receiver)
|
||||||
@ -697,6 +779,16 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
wantsToAnswer = false
|
wantsToAnswer = false
|
||||||
currentTimeouts = 0
|
currentTimeouts = 0
|
||||||
isNetworkAvailable = false
|
isNetworkAvailable = false
|
||||||
|
if (checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
val telephonyManager = getSystemService(TelephonyManager::class.java)
|
||||||
|
with(telephonyManager) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
|
this.listen(hangupOnCallAnswered, LISTEN_NONE)
|
||||||
|
} else {
|
||||||
|
this.unregisterTelephonyCallback(hangupTelephonyCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -708,7 +800,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CheckReconnectedRunnable(private val callId: UUID, private val context: Context): Runnable {
|
private class CheckReconnectedRunnable(private val callId: UUID, private val context: Context) :
|
||||||
|
Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
val intent = Intent(context, WebRtcCallService::class.java)
|
val intent = Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_CHECK_RECONNECT)
|
.setAction(ACTION_CHECK_RECONNECT)
|
||||||
@ -717,7 +810,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ReconnectTimeoutRunnable(private val callId: UUID, private val context: Context): Runnable {
|
private class ReconnectTimeoutRunnable(private val callId: UUID, private val context: Context) :
|
||||||
|
Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
val intent = Intent(context, WebRtcCallService::class.java)
|
val intent = Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_CHECK_RECONNECT_TIMEOUT)
|
.setAction(ACTION_CHECK_RECONNECT_TIMEOUT)
|
||||||
@ -726,7 +820,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TimeoutRunnable(private val callId: UUID, private val context: Context): Runnable {
|
private class TimeoutRunnable(private val callId: UUID, private val context: Context) :
|
||||||
|
Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
val intent = Intent(context, WebRtcCallService::class.java)
|
val intent = Intent(context, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_CHECK_TIMEOUT)
|
.setAction(ACTION_CHECK_TIMEOUT)
|
||||||
@ -738,14 +833,16 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
private abstract class FailureListener<V>(
|
private abstract class FailureListener<V>(
|
||||||
expectedState: CallState,
|
expectedState: CallState,
|
||||||
expectedCallId: UUID?,
|
expectedCallId: UUID?,
|
||||||
getState: () -> Pair<CallState, UUID?>): StateAwareListener<V>(expectedState, expectedCallId, getState) {
|
getState: () -> Pair<CallState, UUID?>
|
||||||
|
) : StateAwareListener<V>(expectedState, expectedCallId, getState) {
|
||||||
override fun onSuccessContinue(result: V) {}
|
override fun onSuccessContinue(result: V) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private abstract class SuccessOnlyListener<V>(
|
private abstract class SuccessOnlyListener<V>(
|
||||||
expectedState: CallState,
|
expectedState: CallState,
|
||||||
expectedCallId: UUID?,
|
expectedCallId: UUID?,
|
||||||
getState: () -> Pair<CallState, UUID>): StateAwareListener<V>(expectedState, expectedCallId, getState) {
|
getState: () -> Pair<CallState, UUID>
|
||||||
|
) : StateAwareListener<V>(expectedState, expectedCallId, getState) {
|
||||||
override fun onFailureContinue(throwable: Throwable?) {
|
override fun onFailureContinue(throwable: Throwable?) {
|
||||||
Log.e(TAG, throwable)
|
Log.e(TAG, throwable)
|
||||||
throw AssertionError(throwable)
|
throw AssertionError(throwable)
|
||||||
@ -755,7 +852,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
private abstract class StateAwareListener<V>(
|
private abstract class StateAwareListener<V>(
|
||||||
private val expectedState: CallState,
|
private val expectedState: CallState,
|
||||||
private val expectedCallId: UUID?,
|
private val expectedCallId: UUID?,
|
||||||
private val getState: ()->Pair<CallState, UUID?>): FutureTaskListener<V> {
|
private val getState: () -> Pair<CallState, UUID?>
|
||||||
|
) : FutureTaskListener<V> {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Log.tag(StateAwareListener::class.java)
|
private val TAG = Log.tag(StateAwareListener::class.java)
|
||||||
@ -763,7 +861,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
|
|
||||||
override fun onSuccess(result: V) {
|
override fun onSuccess(result: V) {
|
||||||
if (!isConsistentState()) {
|
if (!isConsistentState()) {
|
||||||
Log.w(TAG,"State has changed since request, aborting success callback...")
|
Log.w(TAG, "State has changed since request, aborting success callback...")
|
||||||
} else {
|
} else {
|
||||||
onSuccessContinue(result)
|
onSuccessContinue(result)
|
||||||
}
|
}
|
||||||
@ -772,7 +870,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
override fun onFailure(exception: ExecutionException?) {
|
override fun onFailure(exception: ExecutionException?) {
|
||||||
if (!isConsistentState()) {
|
if (!isConsistentState()) {
|
||||||
Log.w(TAG, exception)
|
Log.w(TAG, exception)
|
||||||
Log.w(TAG,"State has changed since request, aborting failure callback...")
|
Log.w(TAG, "State has changed since request, aborting failure callback...")
|
||||||
} else {
|
} else {
|
||||||
exception?.let {
|
exception?.let {
|
||||||
onFailureContinue(it.cause)
|
onFailureContinue(it.cause)
|
||||||
@ -816,17 +914,29 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
val intent = Intent(this, WebRtcCallService::class.java)
|
val intent = Intent(this, WebRtcCallService::class.java)
|
||||||
.setAction(ACTION_ICE_CONNECTED)
|
.setAction(ACTION_ICE_CONNECTED)
|
||||||
startService(intent)
|
startService(intent)
|
||||||
} else if (newState in arrayOf(FAILED, DISCONNECTED) && (scheduledReconnect == null && scheduledTimeout == null)) {
|
} else if (newState in arrayOf(
|
||||||
|
FAILED,
|
||||||
|
DISCONNECTED
|
||||||
|
) && (scheduledReconnect == null && scheduledTimeout == null)
|
||||||
|
) {
|
||||||
callManager.callId?.let { callId ->
|
callManager.callId?.let { callId ->
|
||||||
callManager.postConnectionEvent(Event.IceDisconnect) {
|
callManager.postConnectionEvent(Event.IceDisconnect) {
|
||||||
callManager.postViewModelState(CallViewModel.State.CALL_RECONNECTING)
|
callManager.postViewModelState(CallViewModel.State.CALL_RECONNECTING)
|
||||||
if (callManager.isInitiator()) {
|
if (callManager.isInitiator()) {
|
||||||
Log.i("Loki", "Starting reconnect timer")
|
Log.i("Loki", "Starting reconnect timer")
|
||||||
scheduledReconnect = timeoutExecutor.schedule(CheckReconnectedRunnable(callId, this), RECONNECT_SECONDS, TimeUnit.SECONDS)
|
scheduledReconnect = timeoutExecutor.schedule(
|
||||||
|
CheckReconnectedRunnable(callId, this),
|
||||||
|
RECONNECT_SECONDS,
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Log.i("Loki", "Starting timeout, awaiting new reconnect")
|
Log.i("Loki", "Starting timeout, awaiting new reconnect")
|
||||||
callManager.postConnectionEvent(Event.PrepareForNewOffer) {
|
callManager.postConnectionEvent(Event.PrepareForNewOffer) {
|
||||||
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
scheduledTimeout = timeoutExecutor.schedule(
|
||||||
|
TimeoutRunnable(callId, this),
|
||||||
|
TIMEOUT_SECONDS,
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -854,7 +964,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||||||
override fun onDataChannel(p0: DataChannel?) {}
|
override fun onDataChannel(p0: DataChannel?) {}
|
||||||
|
|
||||||
override fun onRenegotiationNeeded() {
|
override fun onRenegotiationNeeded() {
|
||||||
Log.w(TAG,"onRenegotiationNeeded was called!")
|
Log.w(TAG, "onRenegotiationNeeded was called!")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {}
|
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user