mirror of
https://github.com/oxen-io/session-android.git
synced 2025-04-03 22:15:38 +00:00
commit
fd0f9ea602
@ -1,38 +1,20 @@
|
|||||||
|
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath "com.android.tools.build:gradle:$gradlePluginVersion"
|
|
||||||
classpath files('libs/gradle-witness.jar')
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
|
|
||||||
classpath "com.google.gms:google-services:$googleServicesVersion"
|
|
||||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'kotlin-kapt'
|
id 'com.google.devtools.ksp'
|
||||||
id 'com.google.dagger.hilt.android'
|
id 'com.google.dagger.hilt.android'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'witness'
|
apply plugin: 'witness'
|
||||||
apply plugin: 'kotlin-kapt'
|
|
||||||
apply plugin: 'kotlin-parcelize'
|
apply plugin: 'kotlin-parcelize'
|
||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
apply plugin: 'dagger.hilt.android.plugin'
|
|
||||||
|
|
||||||
configurations.all {
|
configurations.forEach {
|
||||||
exclude module: "commons-logging"
|
it.exclude module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 379
|
def canonicalVersionCode = 380
|
||||||
def canonicalVersionName = "1.19.1"
|
def canonicalVersionName = "1.19.2"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
@ -67,13 +49,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
exclude 'LICENSE.txt'
|
resources {
|
||||||
exclude 'LICENSE'
|
excludes += ['LICENSE.txt', 'LICENSE', 'NOTICE', 'asm-license.txt', 'META-INF/LICENSE', 'META-INF/NOTICE', 'META-INF/proguard/androidx-annotations.pro']
|
||||||
exclude 'NOTICE'
|
}
|
||||||
exclude 'asm-license.txt'
|
|
||||||
exclude 'META-INF/LICENSE'
|
|
||||||
exclude 'META-INF/NOTICE'
|
|
||||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
@ -88,8 +66,9 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose true
|
compose true
|
||||||
}
|
}
|
||||||
|
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion '1.4.7'
|
kotlinCompilerExtensionVersion '1.5.14'
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@ -111,8 +90,8 @@ android {
|
|||||||
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
||||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||||
|
resourceConfigurations += []
|
||||||
|
|
||||||
resConfigs autoResConfig()
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
// The following argument makes the Android Test Orchestrator run its
|
// The following argument makes the Android Test Orchestrator run its
|
||||||
// "pm clear" command after each test invocation. This command ensures
|
// "pm clear" command after each test invocation. This command ensures
|
||||||
@ -172,7 +151,7 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.forEach { variant ->
|
||||||
variant.outputs.each { output ->
|
variant.outputs.each { output ->
|
||||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||||
def postFix = abiPostFix.get(abiName, 0)
|
def postFix = abiPostFix.get(abiName, 0)
|
||||||
@ -183,10 +162,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
abortOnError true
|
|
||||||
baseline file("lint-baseline.xml")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests {
|
unitTests {
|
||||||
@ -195,7 +170,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
dataBinding true
|
|
||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,9 +189,11 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task testPlayDebugUnitTestCoverageReport(type: JacocoReport, dependsOn: "testPlayDebugUnitTest") {
|
tasks.register('testPlayDebugUnitTestCoverageReport', JacocoReport) {
|
||||||
|
dependsOn 'testPlayDebugUnitTest'
|
||||||
|
|
||||||
reports {
|
reports {
|
||||||
xml.enabled = true
|
xml.required = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add files that should not be listed in the report (e.g. generated Files from dagger)
|
// Add files that should not be listed in the report (e.g. generated Files from dagger)
|
||||||
@ -235,12 +211,20 @@ android {
|
|||||||
// This is enabled with 'enableUnitTestCoverage' in the 'debug' build type.
|
// This is enabled with 'enableUnitTestCoverage' in the 'debug' build type.
|
||||||
executionData.from = "${project.buildDir}/outputs/unit_test_code_coverage/playDebugUnitTest/testPlayDebugUnitTest.exec"
|
executionData.from = "${project.buildDir}/outputs/unit_test_code_coverage/playDebugUnitTest/testPlayDebugUnitTest.exec"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
testNamespace 'network.loki.messenger.test'
|
||||||
|
lint {
|
||||||
|
abortOnError true
|
||||||
|
baseline file('lint-baseline.xml')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation("com.google.dagger:hilt-android:2.46.1")
|
implementation("com.google.dagger:hilt-android:$daggerHiltVersion")
|
||||||
kapt("com.google.dagger:hilt-android-compiler:2.44")
|
ksp("com.google.dagger:hilt-compiler:$daggerHiltVersion")
|
||||||
|
ksp("androidx.hilt:hilt-compiler:$jetpackHiltVersion")
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
@ -260,6 +244,7 @@ dependencies {
|
|||||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||||
implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
|
implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
|
||||||
implementation 'androidx.activity:activity-ktx:1.5.1'
|
implementation 'androidx.activity:activity-ktx:1.5.1'
|
||||||
|
implementation 'androidx.activity:activity-compose:1.5.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||||
implementation "androidx.core:core-ktx:$coreVersion"
|
implementation "androidx.core:core-ktx:$coreVersion"
|
||||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||||
@ -281,18 +266,14 @@ dependencies {
|
|||||||
implementation 'commons-net:commons-net:3.7.2'
|
implementation 'commons-net:commons-net:3.7.2'
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||||
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
|
implementation "com.github.bumptech.glide:compose:1.0.0-beta01"
|
||||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
ksp "com.github.bumptech.glide:ksp:$glideVersion"
|
||||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||||
implementation 'pl.tajchert:waitingdots:0.1.0'
|
implementation 'pl.tajchert:waitingdots:0.1.0'
|
||||||
implementation 'com.vanniktech:android-image-cropper:4.5.0'
|
implementation 'com.vanniktech:android-image-cropper:4.5.0'
|
||||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
|
||||||
implementation "com.google.dagger:hilt-android:$daggerVersion"
|
|
||||||
kapt "com.google.dagger:hilt-compiler:$daggerVersion"
|
|
||||||
implementation 'com.google.zxing:core:3.2.1'
|
|
||||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||||
exclude group: 'com.android.support', module: 'support-annotations'
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
}
|
}
|
||||||
@ -304,7 +285,6 @@ dependencies {
|
|||||||
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
||||||
}
|
}
|
||||||
implementation 'com.annimon:stream:1.1.8'
|
implementation 'com.annimon:stream:1.1.8'
|
||||||
implementation project(':stickyheader')
|
|
||||||
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 'androidx.sqlite:sqlite-ktx:2.3.1'
|
implementation 'androidx.sqlite:sqlite-ktx:2.3.1'
|
||||||
implementation 'net.zetetic:sqlcipher-android:4.5.4@aar'
|
implementation 'net.zetetic:sqlcipher-android:4.5.4@aar'
|
||||||
@ -370,7 +350,6 @@ dependencies {
|
|||||||
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2' // For Robolectric
|
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2' // For Robolectric
|
||||||
testImplementation 'app.cash.turbine:turbine:1.1.0'
|
testImplementation 'app.cash.turbine:turbine:1.1.0'
|
||||||
|
|
||||||
|
|
||||||
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
|
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
|
||||||
implementation "androidx.compose.ui:ui:$composeVersion"
|
implementation "androidx.compose.ui:ui:$composeVersion"
|
||||||
implementation "androidx.compose.animation:animation:$composeVersion"
|
implementation "androidx.compose.animation:animation:$composeVersion"
|
||||||
@ -389,7 +368,8 @@ dependencies {
|
|||||||
implementation "androidx.camera:camera-lifecycle:1.3.2"
|
implementation "androidx.camera:camera-lifecycle:1.3.2"
|
||||||
implementation "androidx.camera:camera-view:1.3.2"
|
implementation "androidx.camera:camera-view:1.3.2"
|
||||||
|
|
||||||
implementation "com.google.mlkit:barcode-scanning:17.2.0"
|
// Note: ZXing 3.5.3 is the latest stable release as of 2024/08/21
|
||||||
|
implementation "com.google.zxing:core:$zxingVersion"
|
||||||
}
|
}
|
||||||
|
|
||||||
static def getLastCommitTimestamp() {
|
static def getLastCommitTimestamp() {
|
||||||
@ -410,8 +390,3 @@ def autoResConfig() {
|
|||||||
.collect { matcher -> matcher.group(1) }
|
.collect { matcher -> matcher.group(1) }
|
||||||
.sort()
|
.sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow references to generated code
|
|
||||||
kapt {
|
|
||||||
correctErrorTypes = true
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<manifest
|
<manifest
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="network.loki.messenger.test">
|
|
||||||
<application>
|
<application>
|
||||||
<uses-library android:name="android.test.runner"
|
<uses-library android:name="android.test.runner"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
@ -40,7 +40,11 @@ import org.session.libsignal.utilities.guava.Optional
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
|
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity
|
import org.thoughtcrime.securesms.home.HomeActivity
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import com.bumptech.glide.Glide
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently not used as part of our CI/Deployment processes !!!!
|
||||||
|
*/
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@ -71,7 +75,7 @@ class HomeActivityTests {
|
|||||||
onView(allOf(isDescendantOfA(withId(R.id.inputBar)),withId(R.id.inputBarEditText))).perform(ViewActions.replaceText(messageToSend))
|
onView(allOf(isDescendantOfA(withId(R.id.inputBar)),withId(R.id.inputBarEditText))).perform(ViewActions.replaceText(messageToSend))
|
||||||
if (linkPreview != null) {
|
if (linkPreview != null) {
|
||||||
val activity = activityMonitor.waitForActivity() as ConversationActivityV2
|
val activity = activityMonitor.waitForActivity() as ConversationActivityV2
|
||||||
val glide = GlideApp.with(activity)
|
val glide = Glide.with(activity)
|
||||||
activity.findViewById<InputBar>(R.id.inputBar).updateLinkPreviewDraft(glide, linkPreview)
|
activity.findViewById<InputBar>(R.id.inputBar).updateLinkPreviewDraft(glide, linkPreview)
|
||||||
}
|
}
|
||||||
onView(allOf(isDescendantOfA(withId(R.id.inputBar)),inputButtonWithDrawable(R.drawable.ic_arrow_up))).perform(ViewActions.click())
|
onView(allOf(isDescendantOfA(withId(R.id.inputBar)),inputButtonWithDrawable(R.drawable.ic_arrow_up))).perform(ViewActions.click())
|
||||||
@ -107,7 +111,7 @@ class HomeActivityTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun goToMyChat() {
|
/* private fun goToMyChat() {
|
||||||
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
||||||
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
||||||
// new chat
|
// new chat
|
||||||
@ -122,7 +126,7 @@ class HomeActivityTests {
|
|||||||
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.typeText(copied))
|
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.typeText(copied))
|
||||||
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.closeSoftKeyboard())
|
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.closeSoftKeyboard())
|
||||||
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
||||||
}
|
}*/
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLaunches_dismiss_seedView() {
|
fun testLaunches_dismiss_seedView() {
|
||||||
@ -145,7 +149,7 @@ class HomeActivityTests {
|
|||||||
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
|
onView(withId(R.id.seedReminderView)).check(matches(not(isDisplayed())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
/* @Test
|
||||||
fun testChat_withSelf() {
|
fun testChat_withSelf() {
|
||||||
setupLoggedInState()
|
setupLoggedInState()
|
||||||
goToMyChat()
|
goToMyChat()
|
||||||
@ -176,7 +180,7 @@ class HomeActivityTests {
|
|||||||
|
|
||||||
onView(isRoot()).perform(waitFor(1000)) // no other way for this to work apparently
|
onView(isRoot()).perform(waitFor(1000)) // no other way for this to work apparently
|
||||||
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
|
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
|
||||||
}
|
}*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform action of waiting for a specific time.
|
* Perform action of waiting for a specific time.
|
||||||
|
@ -291,10 +291,6 @@
|
|||||||
android:name="org.thoughtcrime.securesms.scribbles.StickerSelectActivity"
|
android:name="org.thoughtcrime.securesms.scribbles.StickerSelectActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:theme="@style/Theme.Session.ForceDark" />
|
android:theme="@style/Theme.Session.ForceDark" />
|
||||||
<activity
|
|
||||||
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.AppCompat" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.thoughtcrime.securesms.ShortcutLauncherActivity"
|
android:name="org.thoughtcrime.securesms.ShortcutLauncherActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
@ -310,6 +306,8 @@
|
|||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name="org.thoughtcrime.securesms.media.MediaOverviewActivity" />
|
||||||
|
|
||||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
|
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
|
||||||
android:foregroundServiceType="microphone"
|
android:foregroundServiceType="microphone"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -328,7 +326,6 @@
|
|||||||
<action android:name="android.service.chooser.ChooserTargetService" />
|
<action android:name="android.service.chooser.ChooserTargetService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service android:name="org.thoughtcrime.securesms.service.GenericForegroundService" />
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="org.thoughtcrime.securesms.notifications.MarkReadReceiver"
|
android:name="org.thoughtcrime.securesms.notifications.MarkReadReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
@ -440,9 +437,7 @@
|
|||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<service
|
|
||||||
android:name="org.thoughtcrime.securesms.jobmanager.KeepAliveService"
|
|
||||||
android:enabled="@bool/enable_alarm_manager" />
|
|
||||||
<uses-library
|
<uses-library
|
||||||
android:name="com.sec.android.app.multiwindow"
|
android:name="com.sec.android.app.multiwindow"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
@ -4,12 +4,8 @@ import android.app.ActivityManager;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Build.VERSION;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import android.view.KeyEvent;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;
|
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||||
|
@ -1,132 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.MediaDocumentsAdapter.HeaderViewHolder;
|
|
||||||
import org.thoughtcrime.securesms.MediaDocumentsAdapter.ViewHolder;
|
|
||||||
import org.thoughtcrime.securesms.components.DocumentView;
|
|
||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
import static com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager.TAG;
|
|
||||||
|
|
||||||
public class MediaDocumentsAdapter extends CursorRecyclerViewAdapter<ViewHolder> implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder> {
|
|
||||||
|
|
||||||
private final Calendar calendar;
|
|
||||||
private final Locale locale;
|
|
||||||
|
|
||||||
MediaDocumentsAdapter(Context context, Cursor cursor, Locale locale) {
|
|
||||||
super(context, cursor);
|
|
||||||
|
|
||||||
this.calendar = Calendar.getInstance();
|
|
||||||
this.locale = locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
return new ViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.media_overview_document_item, parent, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
|
||||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor);
|
|
||||||
Slide slide = MediaUtil.getSlideForAttachment(getContext(), mediaRecord.getAttachment());
|
|
||||||
|
|
||||||
if (slide != null && slide.hasDocument()) {
|
|
||||||
viewHolder.documentView.setDocument((DocumentSlide)slide, false);
|
|
||||||
viewHolder.date.setText(DateUtils.getRelativeDate(getContext(), locale, mediaRecord.getDate()));
|
|
||||||
viewHolder.documentView.setVisibility(View.VISIBLE);
|
|
||||||
viewHolder.date.setVisibility(View.VISIBLE);
|
|
||||||
viewHolder.documentView.setOnClickListener(view -> {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType());
|
|
||||||
try {
|
|
||||||
getContext().startActivity(intent);
|
|
||||||
} catch (ActivityNotFoundException anfe) {
|
|
||||||
Log.w(TAG, "No activity existed to view the media.");
|
|
||||||
Toast.makeText(getContext(), R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
viewHolder.documentView.setVisibility(View.GONE);
|
|
||||||
viewHolder.date.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getHeaderId(int position) {
|
|
||||||
if (!isActiveCursor()) return -1;
|
|
||||||
if (isHeaderPosition(position)) return -1;
|
|
||||||
if (isFooterPosition(position)) return -1;
|
|
||||||
if (position >= getItemCount()) return -1;
|
|
||||||
if (position < 0) return -1;
|
|
||||||
|
|
||||||
Cursor cursor = getCursorAtPositionOrThrow(position);
|
|
||||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor);
|
|
||||||
|
|
||||||
calendar.setTime(new Date(mediaRecord.getDate()));
|
|
||||||
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
|
|
||||||
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.media_overview_document_item_header, parent, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
|
|
||||||
Cursor cursor = getCursorAtPositionOrThrow(position);
|
|
||||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor);
|
|
||||||
viewHolder.textView.setText(DateUtils.getRelativeDate(getContext(), locale, mediaRecord.getDate()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
private final DocumentView documentView;
|
|
||||||
private final TextView date;
|
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
this.documentView = itemView.findViewById(R.id.document_view);
|
|
||||||
this.date = itemView.findViewById(R.id.date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
|
|
||||||
private final TextView textView;
|
|
||||||
|
|
||||||
HeaderViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
this.textView = itemView.findViewById(R.id.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,173 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Open Whisper Systems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
|
|
||||||
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
|
||||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
class MediaGalleryAdapter extends StickyHeaderGridAdapter {
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static final String TAG = MediaGalleryAdapter.class.getSimpleName();
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final GlideRequests glideRequests;
|
|
||||||
private final Locale locale;
|
|
||||||
private final ItemClickListener itemClickListener;
|
|
||||||
private final Set<MediaRecord> selected;
|
|
||||||
|
|
||||||
private BucketedThreadMedia media;
|
|
||||||
|
|
||||||
private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder {
|
|
||||||
ThumbnailView imageView;
|
|
||||||
View selectedIndicator;
|
|
||||||
|
|
||||||
ViewHolder(View v) {
|
|
||||||
super(v);
|
|
||||||
imageView = v.findViewById(R.id.image);
|
|
||||||
selectedIndicator = v.findViewById(R.id.selected_indicator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class HeaderHolder extends StickyHeaderGridAdapter.HeaderViewHolder {
|
|
||||||
TextView textView;
|
|
||||||
|
|
||||||
HeaderHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
textView = itemView.findViewById(R.id.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaGalleryAdapter(@NonNull Context context,
|
|
||||||
@NonNull GlideRequests glideRequests,
|
|
||||||
BucketedThreadMedia media,
|
|
||||||
Locale locale,
|
|
||||||
ItemClickListener clickListener)
|
|
||||||
{
|
|
||||||
this.context = context;
|
|
||||||
this.glideRequests = glideRequests;
|
|
||||||
this.locale = locale;
|
|
||||||
this.media = media;
|
|
||||||
this.itemClickListener = clickListener;
|
|
||||||
this.selected = new HashSet<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMedia(BucketedThreadMedia media) {
|
|
||||||
this.media = media;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
|
|
||||||
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_gallery_item_header, parent, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType) {
|
|
||||||
return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_gallery_item, parent, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindHeaderViewHolder(StickyHeaderGridAdapter.HeaderViewHolder viewHolder, int section) {
|
|
||||||
((HeaderHolder)viewHolder).textView.setText(media.getName(section, locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset) {
|
|
||||||
MediaRecord mediaRecord = media.get(section, offset);
|
|
||||||
ThumbnailView thumbnailView = ((ViewHolder)viewHolder).imageView;
|
|
||||||
View selectedIndicator = ((ViewHolder)viewHolder).selectedIndicator;
|
|
||||||
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
|
|
||||||
|
|
||||||
if (slide != null) {
|
|
||||||
thumbnailView.setImageResource(glideRequests, slide, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
|
||||||
thumbnailView.setOnLongClickListener(view -> {
|
|
||||||
itemClickListener.onMediaLongClicked(mediaRecord);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedIndicator.setVisibility(selected.contains(mediaRecord) ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSectionCount() {
|
|
||||||
return media.getSectionCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSectionItemCount(int section) {
|
|
||||||
return media.getSectionItemCount(section);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void toggleSelection(@NonNull MediaRecord mediaRecord) {
|
|
||||||
if (!selected.remove(mediaRecord)) {
|
|
||||||
selected.add(mediaRecord);
|
|
||||||
}
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSelectedMediaCount() {
|
|
||||||
return selected.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public Collection<MediaRecord> getSelectedMedia() {
|
|
||||||
return new HashSet<>(selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearSelection() {
|
|
||||||
selected.clear();
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectAllMedia() {
|
|
||||||
for (int section = 0; section < media.getSectionCount(); section++) {
|
|
||||||
for (int item = 0; item < media.getSectionItemCount(section); item++) {
|
|
||||||
selected.add(media.get(section, item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ItemClickListener {
|
|
||||||
void onMediaClicked(@NonNull MediaRecord mediaRecord);
|
|
||||||
void onMediaLongClicked(MediaRecord mediaRecord);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,504 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Open Whisper Systems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.view.ActionMode;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
|
|
||||||
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
|
||||||
import org.session.libsession.snode.SnodeAPI;
|
|
||||||
import org.session.libsession.utilities.Address;
|
|
||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
|
|
||||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
|
|
||||||
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
|
||||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.session.libsession.utilities.task.ProgressDialogAsyncTask;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import kotlin.Unit;
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity for displaying media attachments in-app
|
|
||||||
*/
|
|
||||||
public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private final static String TAG = MediaOverviewActivity.class.getSimpleName();
|
|
||||||
|
|
||||||
public static final String ADDRESS_EXTRA = "address";
|
|
||||||
|
|
||||||
private Toolbar toolbar;
|
|
||||||
private TabLayout tabLayout;
|
|
||||||
private ViewPager viewPager;
|
|
||||||
private Recipient recipient;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle bundle, boolean ready) {
|
|
||||||
setContentView(R.layout.media_overview_activity);
|
|
||||||
|
|
||||||
initializeResources();
|
|
||||||
initializeToolbar();
|
|
||||||
|
|
||||||
this.tabLayout.setupWithViewPager(viewPager);
|
|
||||||
this.viewPager.setAdapter(new MediaOverviewPagerAdapter(getSupportFragmentManager()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
super.onOptionsItemSelected(item);
|
|
||||||
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case android.R.id.home: finish(); return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeResources() {
|
|
||||||
Address address = getIntent().getParcelableExtra(ADDRESS_EXTRA);
|
|
||||||
|
|
||||||
this.viewPager = ViewUtil.findById(this, R.id.pager);
|
|
||||||
this.toolbar = ViewUtil.findById(this, R.id.toolbar);
|
|
||||||
this.tabLayout = ViewUtil.findById(this, R.id.tab_layout);
|
|
||||||
this.recipient = Recipient.from(this, address, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeToolbar() {
|
|
||||||
setSupportActionBar(this.toolbar);
|
|
||||||
ActionBar actionBar = getSupportActionBar();
|
|
||||||
actionBar.setTitle(recipient.toShortString());
|
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
|
||||||
actionBar.setHomeButtonEnabled(true);
|
|
||||||
this.recipient.addListener(recipient -> {
|
|
||||||
Util.runOnMain(() -> actionBar.setTitle(recipient.toShortString()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onEnterMultiSelect() {
|
|
||||||
tabLayout.setEnabled(false);
|
|
||||||
viewPager.setEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onExitMultiSelect() {
|
|
||||||
tabLayout.setEnabled(true);
|
|
||||||
viewPager.setEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MediaOverviewPagerAdapter extends FragmentStatePagerAdapter {
|
|
||||||
|
|
||||||
MediaOverviewPagerAdapter(FragmentManager fragmentManager) {
|
|
||||||
super(fragmentManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Fragment getItem(int position) {
|
|
||||||
Fragment fragment;
|
|
||||||
|
|
||||||
if (position == 0) fragment = new MediaOverviewGalleryFragment();
|
|
||||||
else if (position == 1) fragment = new MediaOverviewDocumentsFragment();
|
|
||||||
else throw new AssertionError();
|
|
||||||
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putString(MediaOverviewGalleryFragment.ADDRESS_EXTRA, recipient.getAddress().serialize());
|
|
||||||
args.putSerializable(MediaOverviewGalleryFragment.LOCALE_EXTRA, Locale.getDefault());
|
|
||||||
|
|
||||||
fragment.setArguments(args);
|
|
||||||
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CharSequence getPageTitle(int position) {
|
|
||||||
if (position == 0) return getString(R.string.MediaOverviewActivity_Media);
|
|
||||||
else if (position == 1) return getString(R.string.MediaOverviewActivity_Documents);
|
|
||||||
else throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static abstract class MediaOverviewFragment<T> extends Fragment implements LoaderManager.LoaderCallbacks<T> {
|
|
||||||
|
|
||||||
public static final String ADDRESS_EXTRA = "address";
|
|
||||||
public static final String LOCALE_EXTRA = "locale_extra";
|
|
||||||
|
|
||||||
protected TextView noMedia;
|
|
||||||
protected Recipient recipient;
|
|
||||||
protected RecyclerView recyclerView;
|
|
||||||
protected Locale locale;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle bundle) {
|
|
||||||
super.onCreate(bundle);
|
|
||||||
|
|
||||||
String address = getArguments().getString(ADDRESS_EXTRA);
|
|
||||||
Locale locale = (Locale)getArguments().getSerializable(LOCALE_EXTRA);
|
|
||||||
|
|
||||||
if (address == null) throw new AssertionError();
|
|
||||||
if (locale == null) throw new AssertionError();
|
|
||||||
|
|
||||||
this.recipient = Recipient.from(getContext(), Address.fromSerialized(address), true);
|
|
||||||
this.locale = locale;
|
|
||||||
|
|
||||||
getLoaderManager().initLoader(0, null, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class MediaOverviewGalleryFragment
|
|
||||||
extends MediaOverviewFragment<BucketedThreadMedia>
|
|
||||||
implements MediaGalleryAdapter.ItemClickListener
|
|
||||||
{
|
|
||||||
|
|
||||||
private StickyHeaderGridLayoutManager gridManager;
|
|
||||||
private ActionMode actionMode;
|
|
||||||
private ActionModeCallback actionModeCallback = new ActionModeCallback();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.media_overview_gallery_fragment, container, false);
|
|
||||||
|
|
||||||
this.recyclerView = ViewUtil.findById(view, R.id.media_grid);
|
|
||||||
this.noMedia = ViewUtil.findById(view, R.id.no_images);
|
|
||||||
this.gridManager = new StickyHeaderGridLayoutManager(getResources().getInteger(R.integer.media_overview_cols));
|
|
||||||
|
|
||||||
this.recyclerView.setAdapter(new MediaGalleryAdapter(getContext(),
|
|
||||||
GlideApp.with(this),
|
|
||||||
new BucketedThreadMedia(getContext()),
|
|
||||||
locale,
|
|
||||||
this));
|
|
||||||
this.recyclerView.setLayoutManager(gridManager);
|
|
||||||
this.recyclerView.setHasFixedSize(true);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
|
||||||
super.onConfigurationChanged(newConfig);
|
|
||||||
if (gridManager != null) {
|
|
||||||
this.gridManager = new StickyHeaderGridLayoutManager(getResources().getInteger(R.integer.media_overview_cols));
|
|
||||||
this.recyclerView.setLayoutManager(gridManager);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull Loader<BucketedThreadMedia> onCreateLoader(int i, Bundle bundle) {
|
|
||||||
return new BucketedThreadMediaLoader(getContext(), recipient.getAddress());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(@NonNull Loader<BucketedThreadMedia> loader, BucketedThreadMedia bucketedThreadMedia) {
|
|
||||||
((MediaGalleryAdapter) recyclerView.getAdapter()).setMedia(bucketedThreadMedia);
|
|
||||||
((MediaGalleryAdapter) recyclerView.getAdapter()).notifyAllSectionsDataSetChanged();
|
|
||||||
|
|
||||||
noMedia.setVisibility(recyclerView.getAdapter().getItemCount() > 0 ? View.GONE : View.VISIBLE);
|
|
||||||
getActivity().invalidateOptionsMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(@NonNull Loader<BucketedThreadMedia> cursorLoader) {
|
|
||||||
((MediaGalleryAdapter) recyclerView.getAdapter()).setMedia(new BucketedThreadMedia(getContext()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMediaClicked(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
|
||||||
if (actionMode != null) {
|
|
||||||
handleMediaMultiSelectClick(mediaRecord);
|
|
||||||
} else {
|
|
||||||
handleMediaPreviewClick(mediaRecord);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleMediaMultiSelectClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
|
||||||
MediaGalleryAdapter adapter = getListAdapter();
|
|
||||||
|
|
||||||
adapter.toggleSelection(mediaRecord);
|
|
||||||
if (adapter.getSelectedMediaCount() == 0) {
|
|
||||||
actionMode.finish();
|
|
||||||
} else {
|
|
||||||
actionMode.setTitle(String.valueOf(adapter.getSelectedMediaCount()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleMediaPreviewClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
|
||||||
if (mediaRecord.getAttachment().getDataUri() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Context context = getContext();
|
|
||||||
if (context == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
|
||||||
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
|
|
||||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
|
|
||||||
intent.putExtra(MediaPreviewActivity.ADDRESS_EXTRA, recipient.getAddress());
|
|
||||||
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, mediaRecord.isOutgoing());
|
|
||||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, true);
|
|
||||||
|
|
||||||
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
|
|
||||||
context.startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMediaLongClicked(MediaDatabase.MediaRecord mediaRecord) {
|
|
||||||
if (actionMode == null) {
|
|
||||||
((MediaGalleryAdapter) recyclerView.getAdapter()).toggleSelection(mediaRecord);
|
|
||||||
recyclerView.getAdapter().notifyDataSetChanged();
|
|
||||||
|
|
||||||
enterMultiSelect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("CodeBlock2Expr")
|
|
||||||
@SuppressLint({"InlinedApi", "StaticFieldLeak"})
|
|
||||||
private void handleSaveMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
|
|
||||||
final Context context = requireContext();
|
|
||||||
|
|
||||||
SaveAttachmentTask.showWarningDialog(context, mediaRecords.size(), () -> {
|
|
||||||
Permissions.with(this)
|
|
||||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
||||||
.maxSdkVersion(Build.VERSION_CODES.P)
|
|
||||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
|
||||||
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
|
||||||
.onAllGranted(() -> {
|
|
||||||
new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>(
|
|
||||||
context,
|
|
||||||
R.string.MediaOverviewActivity_collecting_attachments,
|
|
||||||
R.string.please_wait) {
|
|
||||||
@Override
|
|
||||||
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
|
|
||||||
List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>();
|
|
||||||
|
|
||||||
for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) {
|
|
||||||
if (mediaRecord.getAttachment().getDataUri() != null) {
|
|
||||||
attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getDataUri(),
|
|
||||||
mediaRecord.getContentType(),
|
|
||||||
mediaRecord.getDate(),
|
|
||||||
mediaRecord.getAttachment().getFileName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return attachments;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) {
|
|
||||||
super.onPostExecute(attachments);
|
|
||||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(context, attachments.size());
|
|
||||||
saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR,
|
|
||||||
attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()]));
|
|
||||||
actionMode.finish();
|
|
||||||
boolean containsIncoming = mediaRecords.parallelStream().anyMatch(m -> !m.isOutgoing());
|
|
||||||
if (containsIncoming) {
|
|
||||||
sendMediaSavedNotificationIfNeeded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.execute();
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendMediaSavedNotificationIfNeeded() {
|
|
||||||
if (recipient.isGroupRecipient()) return;
|
|
||||||
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(SnodeAPI.getNowWithOffset()));
|
|
||||||
MessageSender.send(message, recipient.getAddress());
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
private void handleDeleteMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
|
|
||||||
int recordCount = mediaRecords.size();
|
|
||||||
|
|
||||||
DeleteMediaDialog.show(
|
|
||||||
requireContext(),
|
|
||||||
recordCount,
|
|
||||||
() -> new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>(
|
|
||||||
requireContext(),
|
|
||||||
R.string.MediaOverviewActivity_Media_delete_progress_title,
|
|
||||||
R.string.MediaOverviewActivity_Media_delete_progress_message) {
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(MediaDatabase.MediaRecord... records) {
|
|
||||||
if (records == null || records.length == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (MediaDatabase.MediaRecord record : records) {
|
|
||||||
AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()])));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleSelectAllMedia() {
|
|
||||||
getListAdapter().selectAllMedia();
|
|
||||||
actionMode.setTitle(String.valueOf(getListAdapter().getSelectedMediaCount()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private MediaGalleryAdapter getListAdapter() {
|
|
||||||
return (MediaGalleryAdapter) recyclerView.getAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void enterMultiSelect() {
|
|
||||||
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(actionModeCallback);
|
|
||||||
((MediaOverviewActivity) getActivity()).onEnterMultiSelect();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ActionModeCallback implements ActionMode.Callback {
|
|
||||||
|
|
||||||
private int originalStatusBarColor;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
|
||||||
mode.getMenuInflater().inflate(R.menu.media_overview_context, menu);
|
|
||||||
mode.setTitle("1");
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
Window window = getActivity().getWindow();
|
|
||||||
originalStatusBarColor = window.getStatusBarColor();
|
|
||||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
|
|
||||||
switch (menuItem.getItemId()) {
|
|
||||||
case R.id.save:
|
|
||||||
handleSaveMedia(getListAdapter().getSelectedMedia());
|
|
||||||
return true;
|
|
||||||
case R.id.delete:
|
|
||||||
handleDeleteMedia(getListAdapter().getSelectedMedia());
|
|
||||||
actionMode.finish();
|
|
||||||
return true;
|
|
||||||
case R.id.select_all:
|
|
||||||
handleSelectAllMedia();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyActionMode(ActionMode mode) {
|
|
||||||
actionMode = null;
|
|
||||||
getListAdapter().clearSelection();
|
|
||||||
((MediaOverviewActivity) getActivity()).onExitMultiSelect();
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
getActivity().getWindow().setStatusBarColor(originalStatusBarColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class MediaOverviewDocumentsFragment extends MediaOverviewFragment<Cursor> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.media_overview_documents_fragment, container, false);
|
|
||||||
MediaDocumentsAdapter adapter = new MediaDocumentsAdapter(getContext(), null, locale);
|
|
||||||
|
|
||||||
this.recyclerView = ViewUtil.findById(view, R.id.recycler_view);
|
|
||||||
this.noMedia = ViewUtil.findById(view, R.id.no_documents);
|
|
||||||
|
|
||||||
this.recyclerView.setAdapter(adapter);
|
|
||||||
this.recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
|
|
||||||
this.recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, false, true));
|
|
||||||
this.recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
|
||||||
return new ThreadMediaLoader(getContext(), recipient.getAddress(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
|
||||||
((CursorRecyclerViewAdapter)this.recyclerView.getAdapter()).changeCursor(data);
|
|
||||||
getActivity().invalidateOptionsMenu();
|
|
||||||
|
|
||||||
this.noMedia.setVisibility(data.getCount() > 0 ? View.GONE : View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
|
||||||
((CursorRecyclerViewAdapter)this.recyclerView.getAdapter()).changeCursor(null);
|
|
||||||
getActivity().invalidateOptionsMenu();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,7 +17,6 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
@ -26,7 +25,6 @@ import android.net.Uri;
|
|||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Build.VERSION;
|
import android.os.Build.VERSION;
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
@ -57,6 +55,9 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.RequestManager;
|
||||||
|
|
||||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||||
@ -70,10 +71,9 @@ import org.thoughtcrime.securesms.components.MediaView;
|
|||||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||||
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
|
import org.thoughtcrime.securesms.media.MediaOverviewActivity;
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||||
@ -281,7 +281,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
mediaPager.setOffscreenPageLimit(1);
|
mediaPager.setOffscreenPageLimit(1);
|
||||||
|
|
||||||
albumRail = findViewById(R.id.media_preview_album_rail);
|
albumRail = findViewById(R.id.media_preview_album_rail);
|
||||||
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
|
albumRailAdapter = new MediaRailAdapter(Glide.with(this), this, false);
|
||||||
|
|
||||||
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
||||||
albumRail.setAdapter(albumRailAdapter);
|
albumRail.setAdapter(albumRailAdapter);
|
||||||
@ -370,7 +370,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
if (conversationRecipient != null) {
|
if (conversationRecipient != null) {
|
||||||
getSupportLoaderManager().restartLoader(0, null, this);
|
getSupportLoaderManager().restartLoader(0, null, this);
|
||||||
} else {
|
} else {
|
||||||
adapter = new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize);
|
adapter = new SingleItemPagerAdapter(this, Glide.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize);
|
||||||
mediaPager.setAdapter(adapter);
|
mediaPager.setAdapter(adapter);
|
||||||
|
|
||||||
if (initialCaption != null) {
|
if (initialCaption != null) {
|
||||||
@ -391,9 +391,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showOverview() {
|
private void showOverview() {
|
||||||
Intent intent = new Intent(this, MediaOverviewActivity.class);
|
startActivity(MediaOverviewActivity.createIntent(this, conversationRecipient.getAddress()));
|
||||||
intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, conversationRecipient.getAddress());
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void forward() {
|
private void forward() {
|
||||||
@ -518,7 +516,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
|
|
||||||
mediaPager.removeOnPageChangeListener(viewPagerListener);
|
mediaPager.removeOnPageChangeListener(viewPagerListener);
|
||||||
|
|
||||||
adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
|
adapter = new CursorPagerAdapter(this, Glide.with(this), getWindow(), data.first, data.second, leftIsRecent);
|
||||||
mediaPager.setAdapter(adapter);
|
mediaPager.setAdapter(adapter);
|
||||||
|
|
||||||
viewModel.setCursor(this, data.first, leftIsRecent);
|
viewModel.setCursor(this, data.first, leftIsRecent);
|
||||||
@ -588,7 +586,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
|
|
||||||
private static class SingleItemPagerAdapter extends MediaItemAdapter {
|
private static class SingleItemPagerAdapter extends MediaItemAdapter {
|
||||||
|
|
||||||
private final GlideRequests glideRequests;
|
private final RequestManager glideRequests;
|
||||||
private final Window window;
|
private final Window window;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
private final String mediaType;
|
private final String mediaType;
|
||||||
@ -596,7 +594,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
|
|
||||||
private final LayoutInflater inflater;
|
private final LayoutInflater inflater;
|
||||||
|
|
||||||
SingleItemPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
|
SingleItemPagerAdapter(@NonNull Context context, @NonNull RequestManager glideRequests,
|
||||||
@NonNull Window window, @NonNull Uri uri, @NonNull String mediaType,
|
@NonNull Window window, @NonNull Uri uri, @NonNull String mediaType,
|
||||||
long size)
|
long size)
|
||||||
{
|
{
|
||||||
@ -663,14 +661,14 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>();
|
private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>();
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final GlideRequests glideRequests;
|
private final RequestManager glideRequests;
|
||||||
private final Window window;
|
private final Window window;
|
||||||
private final Cursor cursor;
|
private final Cursor cursor;
|
||||||
private final boolean leftIsRecent;
|
private final boolean leftIsRecent;
|
||||||
|
|
||||||
private int autoPlayPosition;
|
private int autoPlayPosition;
|
||||||
|
|
||||||
CursorPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
|
CursorPagerAdapter(@NonNull Context context, @NonNull RequestManager glideRequests,
|
||||||
@NonNull Window window, @NonNull Cursor cursor, int autoPlayPosition,
|
@NonNull Window window, @NonNull Cursor cursor, int autoPlayPosition,
|
||||||
boolean leftIsRecent)
|
boolean leftIsRecent)
|
||||||
{
|
{
|
||||||
|
@ -34,7 +34,7 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||||||
private BroadcastReceiver clearKeyReceiver;
|
private BroadcastReceiver clearKeyReceiver;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected final void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
Log.i(TAG, "onCreate(" + savedInstanceState + ")");
|
Log.i(TAG, "onCreate(" + savedInstanceState + ")");
|
||||||
onPreCreate();
|
onPreCreate();
|
||||||
|
|
||||||
|
@ -1,22 +1,19 @@
|
|||||||
package org.thoughtcrime.securesms.audio;
|
package org.thoughtcrime.securesms.audio;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioRecord;
|
import android.media.AudioRecord;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.media.MediaRecorder;
|
import android.media.MediaRecorder;
|
||||||
import android.os.Build;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
|
||||||
public class AudioCodec {
|
public class AudioCodec {
|
||||||
|
|
||||||
private static final String TAG = AudioCodec.class.getSimpleName();
|
private static final String TAG = AudioCodec.class.getSimpleName();
|
||||||
|
@ -46,7 +46,7 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
private final @NonNull Handler progressEventHandler;
|
private final @NonNull Handler progressEventHandler;
|
||||||
private final @NonNull AudioManager audioManager;
|
private final @NonNull AudioManager audioManager;
|
||||||
private final @NonNull SensorManager sensorManager;
|
private final @NonNull SensorManager sensorManager;
|
||||||
private final @NonNull Sensor proximitySensor;
|
private final Sensor proximitySensor;
|
||||||
private final @Nullable WakeLock wakeLock;
|
private final @Nullable WakeLock wakeLock;
|
||||||
|
|
||||||
private @NonNull WeakReference<Listener> listener;
|
private @NonNull WeakReference<Listener> listener;
|
||||||
@ -132,7 +132,9 @@ public class AudioSlidePlayer implements SensorEventListener {
|
|||||||
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
|
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
|
||||||
}
|
}
|
||||||
|
|
||||||
sensorManager.registerListener(AudioSlidePlayer.this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
|
if(proximitySensor != null) {
|
||||||
|
sensorManager.registerListener(AudioSlidePlayer.this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
setPlaying(AudioSlidePlayer.this);
|
setPlaying(AudioSlidePlayer.this);
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir
|
|||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.NoExternalStorageException
|
import org.session.libsignal.utilities.NoExternalStorageException
|
||||||
import org.thoughtcrime.securesms.util.FileProviderUtil
|
import org.thoughtcrime.securesms.util.FileProviderUtil
|
||||||
import org.thoughtcrime.securesms.util.IntentUtils
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
@ -104,13 +103,8 @@ class AvatarSelection(
|
|||||||
includeClear: Boolean
|
includeClear: Boolean
|
||||||
): Intent {
|
): Intent {
|
||||||
val extraIntents: MutableList<Intent> = LinkedList()
|
val extraIntents: MutableList<Intent> = LinkedList()
|
||||||
var galleryIntent = Intent(Intent.ACTION_PICK)
|
val galleryIntent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
galleryIntent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*")
|
galleryIntent.setType("image/*")
|
||||||
|
|
||||||
if (!IntentUtils.isResolvable(context, galleryIntent)) {
|
|
||||||
galleryIntent = Intent(Intent.ACTION_GET_CONTENT)
|
|
||||||
galleryIntent.setType("image/*")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tempCaptureFile != null) {
|
if (tempCaptureFile != null) {
|
||||||
val uri = FileProviderUtil.getUriFor(context, tempCaptureFile)
|
val uri = FileProviderUtil.getUriFor(context, tempCaptureFile)
|
||||||
|
@ -2,15 +2,10 @@ package org.thoughtcrime.securesms.components;
|
|||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.animation.Animator;
|
import android.animation.Animator;
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@ -26,8 +21,12 @@ import android.widget.ImageView;
|
|||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.PopupWindow;
|
import android.widget.PopupWindow;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.loader.app.LoaderManager;
|
||||||
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
import org.session.libsession.utilities.ViewUtil;
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
@ -126,25 +125,19 @@ public class AttachmentTypeSelector extends PopupWindow {
|
|||||||
public void onGlobalLayout() {
|
public void onGlobalLayout() {
|
||||||
getContentView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
getContentView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
animateWindowInCircular(anchor, getContentView());
|
||||||
animateWindowInCircular(anchor, getContentView());
|
|
||||||
} else {
|
|
||||||
animateWindowInTranslate(getContentView());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
animateButtonIn(imageButton, ANIMATION_DURATION / 2);
|
||||||
animateButtonIn(imageButton, ANIMATION_DURATION / 2);
|
animateButtonIn(cameraButton, ANIMATION_DURATION / 2);
|
||||||
animateButtonIn(cameraButton, ANIMATION_DURATION / 2);
|
|
||||||
|
|
||||||
animateButtonIn(audioButton, ANIMATION_DURATION / 3);
|
animateButtonIn(audioButton, ANIMATION_DURATION / 3);
|
||||||
animateButtonIn(locationButton, ANIMATION_DURATION / 3);
|
animateButtonIn(locationButton, ANIMATION_DURATION / 3);
|
||||||
animateButtonIn(documentButton, ANIMATION_DURATION / 4);
|
animateButtonIn(documentButton, ANIMATION_DURATION / 4);
|
||||||
animateButtonIn(gifButton, ANIMATION_DURATION / 4);
|
animateButtonIn(gifButton, ANIMATION_DURATION / 4);
|
||||||
animateButtonIn(contactButton, 0);
|
animateButtonIn(contactButton, 0);
|
||||||
animateButtonIn(closeButton, 0);
|
animateButtonIn(closeButton, 0);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateHeight() {
|
private void updateHeight() {
|
||||||
@ -159,11 +152,7 @@ public class AttachmentTypeSelector extends PopupWindow {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dismiss() {
|
public void dismiss() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
animateWindowOutCircular(currentAnchor, getContentView());
|
||||||
animateWindowOutCircular(currentAnchor, getContentView());
|
|
||||||
} else {
|
|
||||||
animateWindowOutTranslate(getContentView());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setListener(@Nullable AttachmentClickedListener listener) {
|
public void setListener(@Nullable AttachmentClickedListener listener) {
|
||||||
@ -182,7 +171,6 @@ public class AttachmentTypeSelector extends PopupWindow {
|
|||||||
button.startAnimation(animation);
|
button.startAnimation(animation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
private void animateWindowInCircular(@Nullable View anchor, @NonNull View contentView) {
|
private void animateWindowInCircular(@Nullable View anchor, @NonNull View contentView) {
|
||||||
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
|
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
|
||||||
Animator animator = ViewAnimationUtils.createCircularReveal(contentView,
|
Animator animator = ViewAnimationUtils.createCircularReveal(contentView,
|
||||||
@ -201,7 +189,6 @@ public class AttachmentTypeSelector extends PopupWindow {
|
|||||||
getContentView().startAnimation(animation);
|
getContentView().startAnimation(animation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
private void animateWindowOutCircular(@Nullable View anchor, @NonNull View contentView) {
|
private void animateWindowOutCircular(@Nullable View anchor, @NonNull View contentView) {
|
||||||
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
|
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
|
||||||
Animator animator = ViewAnimationUtils.createCircularReveal(getContentView(),
|
Animator animator = ViewAnimationUtils.createCircularReveal(getContentView(),
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
@ -16,15 +15,14 @@ import android.view.inputmethod.InputConnection;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import androidx.core.os.BuildCompat;
|
import androidx.core.os.BuildCompat;
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat;
|
import androidx.core.view.inputmethod.EditorInfoCompat;
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||||
|
|
||||||
public class ComposeText extends EmojiEditText {
|
public class ComposeText extends EmojiEditText {
|
||||||
|
|
||||||
@ -136,7 +134,6 @@ public class ComposeText extends EmojiEditText {
|
|||||||
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < 21) return inputConnection;
|
|
||||||
if (mediaListener == null) return inputConnection;
|
if (mediaListener == null) return inputConnection;
|
||||||
if (inputConnection == null) return null;
|
if (inputConnection == null) return null;
|
||||||
|
|
||||||
@ -154,7 +151,6 @@ public class ComposeText extends EmojiEditText {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR2)
|
|
||||||
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
|
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
|
||||||
|
|
||||||
private static final String TAG = CommitContentListener.class.getSimpleName();
|
private static final String TAG = CommitContentListener.class.getSimpleName();
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
@ -27,7 +25,6 @@ public class ConversationItemAlertView extends LinearLayout {
|
|||||||
initialize(attrs);
|
initialize(attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(VERSION_CODES.HONEYCOMB)
|
|
||||||
public ConversationItemAlertView(final Context context, AttributeSet attrs, int defStyle) {
|
public ConversationItemAlertView(final Context context, AttributeSet attrs, int defStyle) {
|
||||||
super(context, attrs, defStyle);
|
super(context, attrs, defStyle);
|
||||||
initialize(attrs);
|
initialize(attrs);
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.animation.AlphaAnimation;
|
import android.view.animation.AlphaAnimation;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
@ -11,6 +8,8 @@ import android.view.animation.AnimationSet;
|
|||||||
import android.view.animation.ScaleAnimation;
|
import android.view.animation.ScaleAnimation;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||||
|
|
||||||
public class HidingLinearLayout extends LinearLayout {
|
public class HidingLinearLayout extends LinearLayout {
|
||||||
|
|
||||||
public HidingLinearLayout(Context context) {
|
public HidingLinearLayout(Context context) {
|
||||||
@ -21,7 +20,6 @@ public class HidingLinearLayout extends LinearLayout {
|
|||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
|
||||||
public HidingLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
public HidingLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
public class InputPanel extends LinearLayout {
|
public class InputPanel extends LinearLayout {
|
||||||
|
|
||||||
public InputPanel(Context context) {
|
public InputPanel(Context context) {
|
||||||
@ -18,7 +17,6 @@ public class InputPanel extends LinearLayout {
|
|||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
|
||||||
public InputPanel(Context context, AttributeSet attrs, int defStyleAttr) {
|
public InputPanel(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
@ -16,26 +16,25 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import androidx.appcompat.widget.LinearLayoutCompat;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import androidx.appcompat.widget.LinearLayoutCompat;
|
||||||
|
|
||||||
import org.session.libsession.utilities.ServiceUtil;
|
import org.session.libsession.utilities.ServiceUtil;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LinearLayout that, when a view container, will report back when it thinks a soft keyboard
|
* LinearLayout that, when a view container, will report back when it thinks a soft keyboard
|
||||||
* has been opened and what its height would be.
|
* has been opened and what its height would be.
|
||||||
@ -95,7 +94,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateKeyboardState() {
|
private void updateKeyboardState() {
|
||||||
if (viewInset == 0 && Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) viewInset = getViewInset();
|
if (viewInset == 0) viewInset = getViewInset();
|
||||||
|
|
||||||
getWindowVisibleDisplayFrame(rect);
|
getWindowVisibleDisplayFrame(rect);
|
||||||
|
|
||||||
@ -118,7 +117,6 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
|
||||||
private int getViewInset() {
|
private int getViewInset() {
|
||||||
try {
|
try {
|
||||||
Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
|
Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
|
||||||
|
@ -3,24 +3,24 @@ package org.thoughtcrime.securesms.components;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import androidx.annotation.NonNull;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.bumptech.glide.RequestManager;
|
||||||
|
|
||||||
|
import org.session.libsession.utilities.Stub;
|
||||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||||
import org.thoughtcrime.securesms.video.VideoPlayer;
|
import org.thoughtcrime.securesms.video.VideoPlayer;
|
||||||
|
|
||||||
import org.session.libsession.utilities.Stub;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class MediaView extends FrameLayout {
|
public class MediaView extends FrameLayout {
|
||||||
|
|
||||||
private ZoomingImageView imageView;
|
private ZoomingImageView imageView;
|
||||||
@ -41,12 +41,6 @@ public class MediaView extends FrameLayout {
|
|||||||
initialize();
|
initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public MediaView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
private void initialize() {
|
||||||
inflate(getContext(), R.layout.media_view, this);
|
inflate(getContext(), R.layout.media_view, this);
|
||||||
|
|
||||||
@ -54,7 +48,7 @@ public class MediaView extends FrameLayout {
|
|||||||
this.videoView = new Stub<>(findViewById(R.id.video_player_stub));
|
this.videoView = new Stub<>(findViewById(R.id.video_player_stub));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void set(@NonNull GlideRequests glideRequests,
|
public void set(@NonNull RequestManager glideRequests,
|
||||||
@NonNull Window window,
|
@NonNull Window window,
|
||||||
@NonNull Uri source,
|
@NonNull Uri source,
|
||||||
@NonNull String mediaType,
|
@NonNull String mediaType,
|
||||||
|
@ -20,8 +20,8 @@ import org.session.libsession.utilities.GroupUtil
|
|||||||
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
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import com.bumptech.glide.Glide
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
|
|
||||||
class ProfilePictureView @JvmOverloads constructor(
|
class ProfilePictureView @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null
|
context: Context, attrs: AttributeSet? = null
|
||||||
@ -29,7 +29,7 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
private val TAG = "ProfilePictureView"
|
private val TAG = "ProfilePictureView"
|
||||||
|
|
||||||
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
|
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
|
||||||
private val glide: GlideRequests = GlideApp.with(this)
|
private val glide: RequestManager = Glide.with(this)
|
||||||
private val prefs = AppTextSecurePreferences(context)
|
private val prefs = AppTextSecurePreferences(context)
|
||||||
private val userPublicKey = prefs.getLocalNumber()
|
private val userPublicKey = prefs.getLocalNumber()
|
||||||
var publicKey: String? = null
|
var publicKey: String? = null
|
||||||
|
@ -1,19 +1,11 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -21,16 +13,24 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.loader.app.LoaderManager;
|
||||||
|
import androidx.loader.content.Loader;
|
||||||
|
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.load.Key;
|
import com.bumptech.glide.load.Key;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
import com.bumptech.glide.signature.MediaStoreSignature;
|
import com.bumptech.glide.signature.MediaStoreSignature;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import org.session.libsession.utilities.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||||
import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader;
|
import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.LoaderCallbacks<Cursor> {
|
public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
|||||||
|
|
||||||
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);
|
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);
|
||||||
|
|
||||||
GlideApp.with(getContext().getApplicationContext())
|
Glide.with(getContext().getApplicationContext())
|
||||||
.load(uri)
|
.load(uri)
|
||||||
.signature(signature)
|
.signature(signature)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
@ -130,14 +130,12 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(16)
|
|
||||||
@SuppressWarnings("SuspiciousNameCombination")
|
@SuppressWarnings("SuspiciousNameCombination")
|
||||||
private String getWidthColumn(int orientation) {
|
private String getWidthColumn(int orientation) {
|
||||||
if (orientation == 0 || orientation == 180) return MediaStore.Images.ImageColumns.WIDTH;
|
if (orientation == 0 || orientation == 180) return MediaStore.Images.ImageColumns.WIDTH;
|
||||||
else return MediaStore.Images.ImageColumns.HEIGHT;
|
else return MediaStore.Images.ImageColumns.HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(16)
|
|
||||||
@SuppressWarnings("SuspiciousNameCombination")
|
@SuppressWarnings("SuspiciousNameCombination")
|
||||||
private String getHeightColumn(int orientation) {
|
private String getHeightColumn(int orientation) {
|
||||||
if (orientation == 0 || orientation == 180) return MediaStore.Images.ImageColumns.HEIGHT;
|
if (orientation == 0 || orientation == 180) return MediaStore.Images.ImageColumns.HEIGHT;
|
||||||
|
@ -94,15 +94,11 @@ public class SearchToolbar extends LinearLayout {
|
|||||||
|
|
||||||
searchItem.expandActionView();
|
searchItem.expandActionView();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 21) {
|
Animator animator = ViewAnimationUtils.createCircularReveal(this, (int)x, (int)y, 0, getWidth());
|
||||||
Animator animator = ViewAnimationUtils.createCircularReveal(this, (int)x, (int)y, 0, getWidth());
|
animator.setDuration(400);
|
||||||
animator.setDuration(400);
|
|
||||||
|
|
||||||
setVisibility(View.VISIBLE);
|
setVisibility(View.VISIBLE);
|
||||||
animator.start();
|
animator.start();
|
||||||
} else {
|
|
||||||
setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,19 +112,15 @@ public class SearchToolbar extends LinearLayout {
|
|||||||
|
|
||||||
if (listener != null) listener.onSearchClosed();
|
if (listener != null) listener.onSearchClosed();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 21) {
|
Animator animator = ViewAnimationUtils.createCircularReveal(this, (int)x, (int)y, getWidth(), 0);
|
||||||
Animator animator = ViewAnimationUtils.createCircularReveal(this, (int)x, (int)y, getWidth(), 0);
|
animator.setDuration(400);
|
||||||
animator.setDuration(400);
|
animator.addListener(new AnimationCompleteListener() {
|
||||||
animator.addListener(new AnimationCompleteListener() {
|
@Override
|
||||||
@Override
|
public void onAnimationEnd(Animator animation) {
|
||||||
public void onAnimationEnd(Animator animation) {
|
setVisibility(View.INVISIBLE);
|
||||||
setVisibility(View.INVISIBLE);
|
}
|
||||||
}
|
});
|
||||||
});
|
animator.start();
|
||||||
animator.start();
|
|
||||||
} else {
|
|
||||||
setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.os.Build.VERSION_CODES;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
@ -24,7 +21,7 @@ public class SquareFrameLayout extends FrameLayout {
|
|||||||
this(context, attrs, 0);
|
this(context, attrs, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(VERSION_CODES.HONEYCOMB) @SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public SquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
public SquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import android.view.View;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import com.bumptech.glide.RequestManager;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
|
@ -25,7 +25,7 @@ import network.loki.messenger.R;
|
|||||||
import org.thoughtcrime.securesms.components.subsampling.AttachmentBitmapDecoder;
|
import org.thoughtcrime.securesms.components.subsampling.AttachmentBitmapDecoder;
|
||||||
import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder;
|
import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder;
|
||||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import com.bumptech.glide.RequestManager;
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||||
@ -62,7 +62,7 @@ public class ZoomingImageView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
public void setImageUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri, @NonNull String contentType)
|
public void setImageUri(@NonNull RequestManager glideRequests, @NonNull Uri uri, @NonNull String contentType)
|
||||||
{
|
{
|
||||||
final Context context = getContext();
|
final Context context = getContext();
|
||||||
final int maxTextureSize = BitmapUtil.getMaxTextureSize();
|
final int maxTextureSize = BitmapUtil.getMaxTextureSize();
|
||||||
@ -97,7 +97,7 @@ public class ZoomingImageView extends FrameLayout {
|
|||||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setImageViewUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
|
private void setImageViewUri(@NonNull RequestManager glideRequests, @NonNull Uri uri) {
|
||||||
photoView.setVisibility(View.VISIBLE);
|
photoView.setVisibility(View.VISIBLE);
|
||||||
subsamplingImageView.setVisibility(View.GONE);
|
subsamplingImageView.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.components.camera;
|
package org.thoughtcrime.securesms.components.camera;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
@ -28,26 +27,26 @@ import android.hardware.Camera.Parameters;
|
|||||||
import android.hardware.Camera.Size;
|
import android.hardware.Camera.Size;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Build.VERSION;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import android.view.OrientationEventListener;
|
import android.view.OrientationEventListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import androidx.annotation.NonNull;
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
import androidx.annotation.Nullable;
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsignal.utilities.Log;
|
||||||
|
import org.session.libsignal.utilities.guava.Optional;
|
||||||
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public class CameraView extends ViewGroup {
|
public class CameraView extends ViewGroup {
|
||||||
private static final String TAG = CameraView.class.getSimpleName();
|
private static final String TAG = CameraView.class.getSimpleName();
|
||||||
@ -91,7 +90,6 @@ public class CameraView extends ViewGroup {
|
|||||||
addView(surface);
|
addView(surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
if (state != State.PAUSED) return;
|
if (state != State.PAUSED) return;
|
||||||
state = State.RESUMED;
|
state = State.RESUMED;
|
||||||
@ -255,33 +253,15 @@ public class CameraView extends ViewGroup {
|
|||||||
return Camera.getNumberOfCameras() > 1;
|
return Camera.getNumberOfCameras() > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRearCamera() {
|
|
||||||
return cameraId == CameraInfo.CAMERA_FACING_BACK;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void flipCamera() {
|
|
||||||
if (Camera.getNumberOfCameras() > 1) {
|
|
||||||
cameraId = cameraId == CameraInfo.CAMERA_FACING_BACK
|
|
||||||
? CameraInfo.CAMERA_FACING_FRONT
|
|
||||||
: CameraInfo.CAMERA_FACING_BACK;
|
|
||||||
onPause();
|
|
||||||
onResume();
|
|
||||||
TextSecurePreferences.setDirectCaptureCameraId(getContext(), cameraId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(14)
|
|
||||||
private void onCameraReady(final @NonNull Camera camera) {
|
private void onCameraReady(final @NonNull Camera camera) {
|
||||||
final Parameters parameters = camera.getParameters();
|
final Parameters parameters = camera.getParameters();
|
||||||
|
|
||||||
if (VERSION.SDK_INT >= 14) {
|
parameters.setRecordingHint(true);
|
||||||
parameters.setRecordingHint(true);
|
final List<String> focusModes = parameters.getSupportedFocusModes();
|
||||||
final List<String> focusModes = parameters.getSupportedFocusModes();
|
if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
|
||||||
if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
|
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
|
||||||
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
|
} else if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
|
||||||
} else if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
|
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
|
||||||
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
displayOrientation = CameraUtils.getCameraDisplayOrientation(getActivity(), getCameraInfo());
|
displayOrientation = CameraUtils.getCameraDisplayOrientation(getActivity(), getCameraInfo());
|
||||||
@ -465,7 +445,7 @@ public class CameraView extends ViewGroup {
|
|||||||
}
|
}
|
||||||
final float newWidth = visibleRect.width() * scale;
|
final float newWidth = visibleRect.width() * scale;
|
||||||
final float newHeight = visibleRect.height() * scale;
|
final float newHeight = visibleRect.height() * scale;
|
||||||
final float centerX = (VERSION.SDK_INT < 14 || isTroublemaker()) ? previewWidth - newWidth / 2 : previewWidth / 2;
|
final float centerX = (isTroublemaker()) ? previewWidth - newWidth / 2 : previewWidth / 2;
|
||||||
final float centerY = previewHeight / 2;
|
final float centerY = previewHeight / 2;
|
||||||
|
|
||||||
visibleRect.set((int) (centerX - newWidth / 2),
|
visibleRect.set((int) (centerX - newWidth / 2),
|
||||||
|
@ -12,7 +12,7 @@ import android.widget.ImageView;
|
|||||||
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import com.bumptech.glide.RequestManager;
|
||||||
import org.thoughtcrime.securesms.util.ResUtil;
|
import org.thoughtcrime.securesms.util.ResUtil;
|
||||||
|
|
||||||
import org.session.libsession.utilities.ThemeUtil;
|
import org.session.libsession.utilities.ThemeUtil;
|
||||||
@ -87,7 +87,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index) {
|
public void loadCategoryTabIcon(@NonNull RequestManager glideRequests, @NonNull ImageView imageView, int index) {
|
||||||
Drawable drawable = ResUtil.getDrawable(context, models.get(index).getIconAttr());
|
Drawable drawable = ResUtil.getDrawable(context, models.get(index).getIconAttr());
|
||||||
imageView.setImageDrawable(drawable);
|
imageView.setImageDrawable(drawable);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.components.emoji;
|
package org.thoughtcrime.securesms.components.emoji;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
@ -8,6 +9,8 @@ import android.widget.PopupWindow;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.session.libsession.utilities.ThemeUtil;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
@ -26,7 +29,9 @@ public class EmojiVariationSelectorPopup extends PopupWindow {
|
|||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.list = (ViewGroup) getContentView();
|
this.list = (ViewGroup) getContentView();
|
||||||
|
|
||||||
setBackgroundDrawable(null);
|
setBackgroundDrawable(
|
||||||
|
new ColorDrawable(ThemeUtil.getThemedColor(context, R.attr.colorPrimary))
|
||||||
|
);
|
||||||
setOutsideTouchable(true);
|
setOutsideTouchable(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ import android.widget.FrameLayout;
|
|||||||
import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
|
import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
|
||||||
import org.thoughtcrime.securesms.components.RepeatableImageKey;
|
import org.thoughtcrime.securesms.components.RepeatableImageKey;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import com.bumptech.glide.Glide;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ public class MediaKeyboard extends FrameLayout implements InputView,
|
|||||||
this.searchButton = view.findViewById(R.id.media_keyboard_search);
|
this.searchButton = view.findViewById(R.id.media_keyboard_search);
|
||||||
this.addButton = view.findViewById(R.id.media_keyboard_add);
|
this.addButton = view.findViewById(R.id.media_keyboard_add);
|
||||||
|
|
||||||
this.categoryTabAdapter = new MediaKeyboardBottomTabAdapter(GlideApp.with(this), this);
|
this.categoryTabAdapter = new MediaKeyboardBottomTabAdapter(Glide.with(this), this);
|
||||||
|
|
||||||
categoryTabs.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
|
categoryTabs.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||||
categoryTabs.setAdapter(categoryTabAdapter);
|
categoryTabs.setAdapter(categoryTabAdapter);
|
||||||
|
@ -9,20 +9,20 @@ import android.widget.ImageView;
|
|||||||
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider.TabIconProvider;
|
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider.TabIconProvider;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import com.bumptech.glide.RequestManager;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKeyboardBottomTabAdapter.MediaKeyboardBottomTabViewHolder> {
|
public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKeyboardBottomTabAdapter.MediaKeyboardBottomTabViewHolder> {
|
||||||
|
|
||||||
private final GlideRequests glideRequests;
|
private final RequestManager glideRequests;
|
||||||
private final EventListener eventListener;
|
private final EventListener eventListener;
|
||||||
|
|
||||||
private TabIconProvider tabIconProvider;
|
private TabIconProvider tabIconProvider;
|
||||||
private int activePosition;
|
private int activePosition;
|
||||||
private int count;
|
private int count;
|
||||||
|
|
||||||
public MediaKeyboardBottomTabAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
public MediaKeyboardBottomTabAdapter(@NonNull RequestManager glideRequests, @NonNull EventListener eventListener) {
|
||||||
this.glideRequests = glideRequests;
|
this.glideRequests = glideRequests;
|
||||||
this.eventListener = eventListener;
|
this.eventListener = eventListener;
|
||||||
}
|
}
|
||||||
@ -71,7 +71,7 @@ public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKey
|
|||||||
this.indicator = itemView.findViewById(R.id.media_keyboard_bottom_tab_indicator);
|
this.indicator = itemView.findViewById(R.id.media_keyboard_bottom_tab_indicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
void bind(@NonNull GlideRequests glideRequests,
|
void bind(@NonNull RequestManager glideRequests,
|
||||||
@NonNull EventListener eventListener,
|
@NonNull EventListener eventListener,
|
||||||
@NonNull TabIconProvider tabIconProvider,
|
@NonNull TabIconProvider tabIconProvider,
|
||||||
int index,
|
int index,
|
||||||
|
@ -8,7 +8,7 @@ import android.widget.ImageView;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import com.bumptech.glide.RequestManager;
|
||||||
|
|
||||||
public interface MediaKeyboardProvider {
|
public interface MediaKeyboardProvider {
|
||||||
@LayoutRes int getProviderIconView(boolean selected);
|
@LayoutRes int getProviderIconView(boolean selected);
|
||||||
@ -48,6 +48,6 @@ public interface MediaKeyboardProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TabIconProvider {
|
interface TabIconProvider {
|
||||||
void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index);
|
void loadCategoryTabIcon(@NonNull RequestManager glideRequests, @NonNull ImageView imageView, int index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,10 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import network.loki.messenger.databinding.ContactSelectionListDividerBinding
|
import network.loki.messenger.databinding.ContactSelectionListDividerBinding
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
|
|
||||||
class ContactSelectionListAdapter(private val context: Context, private val multiSelect: Boolean) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
class ContactSelectionListAdapter(private val context: Context, private val multiSelect: Boolean) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
lateinit var glide: GlideRequests
|
lateinit var glide: RequestManager
|
||||||
val selectedContacts = mutableSetOf<Recipient>()
|
val selectedContacts = mutableSetOf<Recipient>()
|
||||||
var items = listOf<ContactSelectionListItem>()
|
var items = listOf<ContactSelectionListItem>()
|
||||||
set(value) { field = value; notifyDataSetChanged() }
|
set(value) { field = value; notifyDataSetChanged() }
|
||||||
|
@ -11,7 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
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
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import com.bumptech.glide.Glide
|
||||||
|
|
||||||
class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener {
|
class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener {
|
||||||
private lateinit var binding: ContactSelectionListFragmentBinding
|
private lateinit var binding: ContactSelectionListFragmentBinding
|
||||||
@ -27,7 +27,7 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
|
|||||||
|
|
||||||
private val listAdapter by lazy {
|
private val listAdapter by lazy {
|
||||||
val result = ContactSelectionListAdapter(requireActivity(), multiSelect)
|
val result = ContactSelectionListAdapter(requireActivity(), multiSelect)
|
||||||
result.glide = GlideApp.with(this)
|
result.glide = Glide.with(this)
|
||||||
result.contactClickListener = this
|
result.contactClickListener = this
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ 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
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import com.bumptech.glide.Glide
|
||||||
|
|
||||||
class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||||
private lateinit var binding: ActivitySelectContactsBinding
|
private lateinit var binding: ActivitySelectContactsBinding
|
||||||
@ -21,7 +21,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
|
|||||||
private lateinit var usersToExclude: Set<String>
|
private lateinit var usersToExclude: Set<String>
|
||||||
|
|
||||||
private val selectContactsAdapter by lazy {
|
private val selectContactsAdapter by lazy {
|
||||||
SelectContactsAdapter(this, GlideApp.with(this))
|
SelectContactsAdapter(this, Glide.with(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -4,10 +4,10 @@ import android.content.Context
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
|
||||||
class SelectContactsAdapter(private val context: Context, private val glide: GlideRequests) : RecyclerView.Adapter<SelectContactsAdapter.ViewHolder>() {
|
class SelectContactsAdapter(private val context: Context, private val glide: RequestManager) : RecyclerView.Adapter<SelectContactsAdapter.ViewHolder>() {
|
||||||
val selectedMembers = mutableSetOf<String>()
|
val selectedMembers = mutableSetOf<String>()
|
||||||
var members = listOf<String>()
|
var members = listOf<String>()
|
||||||
set(value) { field = value; notifyDataSetChanged() }
|
set(value) { field = value; notifyDataSetChanged() }
|
||||||
|
@ -10,7 +10,7 @@ import network.loki.messenger.databinding.ViewUserBinding
|
|||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
|
|
||||||
class UserView : LinearLayout {
|
class UserView : LinearLayout {
|
||||||
private lateinit var binding: ViewUserBinding
|
private lateinit var binding: ViewUserBinding
|
||||||
@ -45,7 +45,7 @@ class UserView : LinearLayout {
|
|||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun bind(user: Recipient, glide: GlideRequests, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
|
fun bind(user: Recipient, glide: RequestManager, actionIndicator: ActionIndicator, isSelected: Boolean = false) {
|
||||||
val isLocalUser = user.isLocalNumber
|
val isLocalUser = user.isLocalNumber
|
||||||
fun getUserDisplayName(publicKey: String): String {
|
fun getUserDisplayName(publicKey: String): String {
|
||||||
if (isLocalUser) return context.getString(R.string.MessageRecord_you)
|
if (isLocalUser) return context.getString(R.string.MessageRecord_you)
|
||||||
|
@ -7,11 +7,13 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@ -22,16 +24,18 @@ import org.thoughtcrime.securesms.conversation.start.NullStartConversationDelega
|
|||||||
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
||||||
import org.thoughtcrime.securesms.ui.Divider
|
import org.thoughtcrime.securesms.ui.Divider
|
||||||
import org.thoughtcrime.securesms.ui.ItemButton
|
import org.thoughtcrime.securesms.ui.ItemButton
|
||||||
|
import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon
|
||||||
|
import org.thoughtcrime.securesms.ui.components.BasicAppBar
|
||||||
|
import org.thoughtcrime.securesms.ui.components.QrImage
|
||||||
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||||
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
|
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
|
||||||
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
|
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
|
||||||
import org.thoughtcrime.securesms.ui.theme.ThemeColors
|
import org.thoughtcrime.securesms.ui.theme.ThemeColors
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
|
||||||
import org.thoughtcrime.securesms.ui.components.AppBar
|
|
||||||
import org.thoughtcrime.securesms.ui.components.QrImage
|
|
||||||
import org.thoughtcrime.securesms.ui.contentDescription
|
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalType
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun StartConversationScreen(
|
internal fun StartConversationScreen(
|
||||||
accountId: String,
|
accountId: String,
|
||||||
@ -41,7 +45,11 @@ internal fun StartConversationScreen(
|
|||||||
LocalColors.current.backgroundSecondary,
|
LocalColors.current.backgroundSecondary,
|
||||||
shape = MaterialTheme.shapes.small
|
shape = MaterialTheme.shapes.small
|
||||||
)) {
|
)) {
|
||||||
AppBar(stringResource(R.string.dialog_start_conversation_title), onClose = delegate::onDialogClosePressed)
|
BasicAppBar(
|
||||||
|
title = stringResource(R.string.dialog_start_conversation_title),
|
||||||
|
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
|
||||||
|
actions = { AppBarCloseIcon(onClose = delegate::onDialogClosePressed) }
|
||||||
|
)
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()),
|
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()),
|
||||||
color = LocalColors.current.backgroundSecondary
|
color = LocalColors.current.backgroundSecondary
|
||||||
|
@ -8,24 +8,28 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon
|
||||||
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
|
import org.thoughtcrime.securesms.ui.components.BackAppBar
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
|
||||||
import org.thoughtcrime.securesms.ui.components.AppBar
|
|
||||||
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
|
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.components.SlimOutlineCopyButton
|
import org.thoughtcrime.securesms.ui.components.SlimOutlineCopyButton
|
||||||
import org.thoughtcrime.securesms.ui.components.border
|
import org.thoughtcrime.securesms.ui.components.border
|
||||||
import org.thoughtcrime.securesms.ui.contentDescription
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalType
|
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun InviteFriend(
|
internal fun InviteFriend(
|
||||||
accountId: String,
|
accountId: String,
|
||||||
@ -38,7 +42,12 @@ internal fun InviteFriend(
|
|||||||
LocalColors.current.backgroundSecondary,
|
LocalColors.current.backgroundSecondary,
|
||||||
shape = MaterialTheme.shapes.small
|
shape = MaterialTheme.shapes.small
|
||||||
)) {
|
)) {
|
||||||
AppBar(stringResource(R.string.invite_a_friend), onBack = onBack, onClose = onClose)
|
BackAppBar(
|
||||||
|
title = stringResource(R.string.invite_a_friend),
|
||||||
|
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
|
||||||
|
onBack = onBack,
|
||||||
|
actions = { AppBarCloseIcon(onClose = onClose) }
|
||||||
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)
|
modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)
|
||||||
.padding(top = LocalDimensions.current.spacing),
|
.padding(top = LocalDimensions.current.spacing),
|
||||||
|
@ -17,6 +17,7 @@ import androidx.compose.foundation.pager.HorizontalPager
|
|||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@ -29,6 +30,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
@ -43,9 +45,10 @@ import kotlinx.coroutines.flow.emptyFlow
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.conversation.start.StartConversationFragment.Companion.PEEK_RATIO
|
import org.thoughtcrime.securesms.conversation.start.StartConversationFragment.Companion.PEEK_RATIO
|
||||||
import org.thoughtcrime.securesms.ui.LoadingArcOr
|
import org.thoughtcrime.securesms.ui.LoadingArcOr
|
||||||
import org.thoughtcrime.securesms.ui.components.AppBar
|
import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon
|
||||||
|
import org.thoughtcrime.securesms.ui.components.BackAppBar
|
||||||
import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon
|
import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon
|
||||||
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
|
import org.thoughtcrime.securesms.ui.components.QRScannerScreen
|
||||||
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
|
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
|
||||||
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
||||||
@ -60,7 +63,7 @@ import kotlin.math.max
|
|||||||
|
|
||||||
private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
|
private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun NewMessage(
|
internal fun NewMessage(
|
||||||
state: State,
|
state: State,
|
||||||
@ -76,12 +79,17 @@ internal fun NewMessage(
|
|||||||
LocalColors.current.backgroundSecondary,
|
LocalColors.current.backgroundSecondary,
|
||||||
shape = MaterialTheme.shapes.small
|
shape = MaterialTheme.shapes.small
|
||||||
)) {
|
)) {
|
||||||
AppBar(stringResource(R.string.messageNew), onClose = onClose, onBack = onBack)
|
BackAppBar(
|
||||||
|
title = stringResource(R.string.messageNew),
|
||||||
|
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
|
||||||
|
onBack = onBack,
|
||||||
|
actions = { AppBarCloseIcon(onClose = onClose) }
|
||||||
|
)
|
||||||
SessionTabRow(pagerState, TITLES)
|
SessionTabRow(pagerState, TITLES)
|
||||||
HorizontalPager(pagerState) {
|
HorizontalPager(pagerState) {
|
||||||
when (TITLES[it]) {
|
when (TITLES[it]) {
|
||||||
R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp)
|
R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp)
|
||||||
R.string.qrScan -> MaybeScanQrCode(qrErrors, onScan = callbacks::onScanQrCode)
|
R.string.qrScan -> QRScannerScreen(qrErrors, onScan = callbacks::onScanQrCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,7 @@ 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.AudioSlide
|
||||||
import org.thoughtcrime.securesms.mms.GifSlide
|
import org.thoughtcrime.securesms.mms.GifSlide
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import com.bumptech.glide.Glide
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||||
import org.thoughtcrime.securesms.mms.Slide
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
@ -347,7 +347,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
adapter
|
adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val glide by lazy { GlideApp.with(this) }
|
private val glide by lazy { Glide.with(this) }
|
||||||
private val lockViewHitMargin by lazy { toPx(40, resources) }
|
private val lockViewHitMargin by lazy { toPx(40, resources) }
|
||||||
private val gifButton by lazy { InputBarButton(this, R.drawable.ic_gif_white_24dp, hasOpaqueBackground = true, isGIFButton = true) }
|
private val gifButton by lazy { InputBarButton(this, R.drawable.ic_gif_white_24dp, hasOpaqueBackground = true, isGIFButton = true) }
|
||||||
private val documentButton by lazy { InputBarButton(this, R.drawable.ic_document_small_dark, hasOpaqueBackground = true) }
|
private val documentButton by lazy { InputBarButton(this, R.drawable.ic_document_small_dark, hasOpaqueBackground = true) }
|
||||||
|
@ -26,7 +26,7 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDel
|
|||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
||||||
import org.thoughtcrime.securesms.showSessionDialog
|
import org.thoughtcrime.securesms.showSessionDialog
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
@ -42,7 +42,7 @@ class ConversationAdapter(
|
|||||||
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: (DatabaseAttachment) -> Unit,
|
private val onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit,
|
||||||
private val glide: GlideRequests,
|
private val glide: RequestManager,
|
||||||
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() }
|
||||||
|
@ -261,8 +261,14 @@ class ConversationViewModel(
|
|||||||
_recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId))
|
_recipient.updateTo(repository.maybeGetRecipientForThreadId(threadId))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hidesInputBar(): Boolean = openGroup?.canWrite != true &&
|
/**
|
||||||
blindedRecipient?.blocksCommunityMessageRequests == true
|
* The input should be hidden when:
|
||||||
|
* - We are in a community without write access
|
||||||
|
* - We are dealing with a contact from a community (blinded recipient) that does not allow
|
||||||
|
* requests form community members
|
||||||
|
*/
|
||||||
|
fun hidesInputBar(): Boolean = openGroup?.canWrite == false ||
|
||||||
|
blindedRecipient?.blocksCommunityMessageRequests == true
|
||||||
|
|
||||||
fun legacyBannerRecipient(context: Context): Recipient? = recipient?.run {
|
fun legacyBannerRecipient(context: Context): Recipient? = recipient?.run {
|
||||||
storage.getLastLegacyRecipient(address.serialize())?.let { Recipient.from(context, Address.fromSerialized(it), false) }
|
storage.getLastLegacyRecipient(address.serialize())?.let { Recipient.from(context, Address.fromSerialized(it), false) }
|
||||||
|
@ -16,15 +16,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.conversation.v2
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build.VERSION
|
|
||||||
import android.os.Build.VERSION_CODES
|
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
@ -33,24 +27,16 @@ import android.view.View
|
|||||||
import com.annimon.stream.Stream
|
import com.annimon.stream.Stream
|
||||||
import com.google.android.mms.pdu_alt.CharacterSets
|
import com.google.android.mms.pdu_alt.CharacterSets
|
||||||
import com.google.android.mms.pdu_alt.EncodedStringValue
|
import com.google.android.mms.pdu_alt.EncodedStringValue
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.components.ComposeText
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.util.Arrays
|
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
import network.loki.messenger.R
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.thoughtcrime.securesms.components.ComposeText
|
|
||||||
|
|
||||||
object Util {
|
object Util {
|
||||||
private val TAG: String = Log.tag(Util::class.java)
|
private val TAG: String = Log.tag(Util::class.java)
|
||||||
|
|
||||||
private val BUILD_LIFESPAN = TimeUnit.DAYS.toMillis(90)
|
|
||||||
|
|
||||||
fun <T> asList(vararg elements: T): List<T> {
|
fun <T> asList(vararg elements: T): List<T> {
|
||||||
val result = mutableListOf<T>() // LinkedList()
|
val result = mutableListOf<T>() // LinkedList()
|
||||||
Collections.addAll(result, *elements)
|
Collections.addAll(result, *elements)
|
||||||
@ -106,19 +92,6 @@ object Util {
|
|||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rightPad(value: String, length: Int): String {
|
|
||||||
if (value.length >= length) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
val out = StringBuilder(value)
|
|
||||||
while (out.length < length) {
|
|
||||||
out.append(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
return out.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEmpty(value: Array<EncodedStringValue?>?): Boolean {
|
fun isEmpty(value: Array<EncodedStringValue?>?): Boolean {
|
||||||
return value == null || value.size == 0
|
return value == null || value.size == 0
|
||||||
}
|
}
|
||||||
@ -135,64 +108,6 @@ object Util {
|
|||||||
return charSequence == null || charSequence.length == 0
|
return charSequence == null || charSequence.length == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasItems(collection: Collection<*>?): Boolean {
|
|
||||||
return collection != null && !collection.isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <K, V> getOrDefault(map: Map<K, V>, key: K, defaultValue: V): V? {
|
|
||||||
return if (map.containsKey(key)) map[key] else defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFirstNonEmpty(vararg values: String?): String {
|
|
||||||
for (value in values) {
|
|
||||||
if (!value.isNullOrEmpty()) { return value }
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun emptyIfNull(value: String?): String {
|
|
||||||
return value ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun emptyIfNull(value: CharSequence?): CharSequence {
|
|
||||||
return value ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBoldedString(value: String?): CharSequence {
|
|
||||||
val spanned = SpannableString(value)
|
|
||||||
spanned.setSpan(
|
|
||||||
StyleSpan(Typeface.BOLD), 0,
|
|
||||||
spanned.length,
|
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
||||||
)
|
|
||||||
|
|
||||||
return spanned
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toIsoString(bytes: ByteArray?): String {
|
|
||||||
try {
|
|
||||||
return String(bytes!!, charset(CharacterSets.MIMENAME_ISO_8859_1))
|
|
||||||
} catch (e: UnsupportedEncodingException) {
|
|
||||||
throw AssertionError("ISO_8859_1 must be supported!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toIsoBytes(isoString: String): ByteArray {
|
|
||||||
try {
|
|
||||||
return isoString.toByteArray(charset(CharacterSets.MIMENAME_ISO_8859_1))
|
|
||||||
} catch (e: UnsupportedEncodingException) {
|
|
||||||
throw AssertionError("ISO_8859_1 must be supported!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUtf8Bytes(utf8String: String): ByteArray {
|
|
||||||
try {
|
|
||||||
return utf8String.toByteArray(charset(CharacterSets.MIMENAME_UTF_8))
|
|
||||||
} catch (e: UnsupportedEncodingException) {
|
|
||||||
throw AssertionError("UTF_8 must be supported!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun wait(lock: Any, timeout: Long) {
|
fun wait(lock: Any, timeout: Long) {
|
||||||
try {
|
try {
|
||||||
(lock as Object).wait(timeout)
|
(lock as Object).wait(timeout)
|
||||||
@ -227,20 +142,6 @@ object Util {
|
|||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
|
|
||||||
fun combine(vararg elements: ByteArray?): ByteArray {
|
|
||||||
try {
|
|
||||||
val baos = ByteArrayOutputStream()
|
|
||||||
|
|
||||||
for (element in elements) {
|
|
||||||
baos.write(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
return baos.toByteArray()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw AssertionError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun trim(input: ByteArray?, length: Int): ByteArray {
|
fun trim(input: ByteArray?, length: Int): ByteArray {
|
||||||
val result = ByteArray(length)
|
val result = ByteArray(length)
|
||||||
System.arraycopy(input, 0, result, 0, result.size)
|
System.arraycopy(input, 0, result, 0, result.size)
|
||||||
@ -248,57 +149,11 @@ object Util {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSecretBytes(size: Int): ByteArray {
|
|
||||||
return getSecretBytes(SecureRandom(), size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSecretBytes(secureRandom: SecureRandom, size: Int): ByteArray {
|
|
||||||
val secret = ByteArray(size)
|
|
||||||
secureRandom.nextBytes(secret)
|
|
||||||
return secret
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> getRandomElement(elements: Array<T>): T {
|
|
||||||
return elements[SecureRandom().nextInt(elements.size)]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> getRandomElement(elements: List<T>): T {
|
|
||||||
return elements[SecureRandom().nextInt(elements.size)]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun equals(a: Any?, b: Any?): Boolean {
|
|
||||||
return a === b || (a != null && a == b)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hashCode(vararg objects: Any?): Int {
|
|
||||||
return objects.contentHashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uri(uri: String?): Uri? {
|
fun uri(uri: String?): Uri? {
|
||||||
return if (uri == null) null
|
return if (uri == null) null
|
||||||
else Uri.parse(uri)
|
else Uri.parse(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(VERSION_CODES.KITKAT)
|
|
||||||
fun isLowMemory(context: Context): Boolean {
|
|
||||||
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
||||||
|
|
||||||
return (VERSION.SDK_INT >= VERSION_CODES.KITKAT && activityManager.isLowRamDevice) ||
|
|
||||||
activityManager.largeMemoryClass <= 64
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clamp(value: Int, min: Int, max: Int): Int {
|
|
||||||
return min(max(value.toDouble(), min.toDouble()), max.toDouble()).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clamp(value: Long, min: Long, max: Long): Long {
|
|
||||||
return min(max(value.toDouble(), min.toDouble()), max.toDouble()).toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clamp(value: Float, min: Float, max: Float): Float {
|
|
||||||
return min(max(value.toDouble(), min.toDouble()), max.toDouble()).toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns half of the difference between the given length, and the length when scaled by the
|
* Returns half of the difference between the given length, and the length when scaled by the
|
||||||
* given scale.
|
* given scale.
|
||||||
@ -308,74 +163,6 @@ object Util {
|
|||||||
return (length - scaledLength) / 2
|
return (length - scaledLength) / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readTextFromClipboard(context: Context): String? {
|
|
||||||
run {
|
|
||||||
val clipboardManager =
|
|
||||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
return if (clipboardManager.hasPrimaryClip() && clipboardManager.primaryClip!!.itemCount > 0) {
|
|
||||||
clipboardManager.primaryClip!!.getItemAt(0).text.toString()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun writeTextToClipboard(context: Context, text: String) {
|
|
||||||
writeTextToClipboard(context, context.getString(R.string.app_name), text)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun writeTextToClipboard(context: Context, label: String, text: String) {
|
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
val clip = ClipData.newPlainText(label, text)
|
|
||||||
clipboard.setPrimaryClip(clip)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toIntExact(value: Long): Int {
|
|
||||||
if (value.toInt().toLong() != value) {
|
|
||||||
throw ArithmeticException("integer overflow")
|
|
||||||
}
|
|
||||||
return value.toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isEquals(first: Long?, second: Long): Boolean {
|
|
||||||
return first != null && first == second
|
|
||||||
}
|
|
||||||
|
|
||||||
@SafeVarargs
|
|
||||||
fun <T> concatenatedList(vararg items: Collection<T>): List<T> {
|
|
||||||
val concat: MutableList<T> = ArrayList(
|
|
||||||
Stream.of(*items).reduce(0) { sum: Int, list: Collection<T> -> sum + list.size })
|
|
||||||
|
|
||||||
for (list in items) {
|
|
||||||
concat.addAll(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
return concat
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isLong(value: String): Boolean {
|
|
||||||
try {
|
|
||||||
value.toLong()
|
|
||||||
return true
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseInt(integer: String, defaultValue: Int): Int {
|
|
||||||
return try {
|
|
||||||
integer.toInt()
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method to determine if we're currently in a left-to-right or right-to-left language like Arabic
|
|
||||||
fun usingRightToLeftLanguage(context: Context): Boolean {
|
|
||||||
val config = context.resources.configuration
|
|
||||||
return config.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method to determine if we're currently in a left-to-right or right-to-left language like Arabic
|
// Method to determine if we're currently in a left-to-right or right-to-left language like Arabic
|
||||||
fun usingLeftToRightLanguage(context: Context): Boolean {
|
fun usingLeftToRightLanguage(context: Context): Boolean {
|
||||||
val config = context.resources.configuration
|
val config = context.resources.configuration
|
||||||
|
@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.MediaPreviewActivity
|
|||||||
import org.thoughtcrime.securesms.components.CornerMask
|
import org.thoughtcrime.securesms.components.CornerMask
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
|
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 com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.mms.Slide
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
import org.thoughtcrime.securesms.util.ActivityDispatcher
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ class AlbumThumbnailView : RelativeLayout {
|
|||||||
slideSize = -1
|
slideSize = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(glideRequests: GlideRequests, message: MmsMessageRecord,
|
fun bind(glideRequests: RequestManager, message: MmsMessageRecord,
|
||||||
isStart: Boolean, isEnd: Boolean) {
|
isStart: Boolean, isEnd: Boolean) {
|
||||||
slides = message.slideDeck.thumbnailSlides
|
slides = message.slideDeck.thumbnailSlides
|
||||||
if (slides.isEmpty()) {
|
if (slides.isEmpty()) {
|
||||||
|
@ -7,7 +7,7 @@ import android.widget.LinearLayout
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import network.loki.messenger.databinding.ViewLinkPreviewDraftBinding
|
import network.loki.messenger.databinding.ViewLinkPreviewDraftBinding
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ class LinkPreviewDraftView : LinearLayout {
|
|||||||
binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(glide: GlideRequests, linkPreview: LinkPreview) {
|
fun update(glide: RequestManager, linkPreview: LinkPreview) {
|
||||||
// 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
|
||||||
|
@ -9,13 +9,13 @@ import android.widget.BaseAdapter
|
|||||||
import android.widget.ListView
|
import android.widget.ListView
|
||||||
import org.session.libsession.messaging.mentions.Mention
|
import org.session.libsession.messaging.mentions.Mention
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
|
|
||||||
class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) {
|
class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) {
|
||||||
private var mentionCandidates = listOf<Mention>()
|
private var mentionCandidates = listOf<Mention>()
|
||||||
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue }
|
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue }
|
||||||
var glide: GlideRequests? = null
|
var glide: RequestManager? = null
|
||||||
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.glide = newValue }
|
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.glide = newValue }
|
||||||
var openGroupServer: String? = null
|
var openGroupServer: String? = null
|
||||||
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.openGroupServer = openGroupServer }
|
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.openGroupServer = openGroupServer }
|
||||||
@ -28,7 +28,7 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
|||||||
private class Adapter(private val context: Context) : BaseAdapter() {
|
private class Adapter(private val context: Context) : BaseAdapter() {
|
||||||
var mentionCandidates = listOf<Mention>()
|
var mentionCandidates = listOf<Mention>()
|
||||||
set(newValue) { field = newValue; notifyDataSetChanged() }
|
set(newValue) { field = newValue; notifyDataSetChanged() }
|
||||||
var glide: GlideRequests? = null
|
var glide: RequestManager? = null
|
||||||
var openGroupServer: String? = null
|
var openGroupServer: String? = null
|
||||||
var openGroupRoom: String? = null
|
var openGroupRoom: String? = null
|
||||||
|
|
||||||
|
@ -8,13 +8,13 @@ import android.widget.LinearLayout
|
|||||||
import network.loki.messenger.databinding.ViewMentionCandidateBinding
|
import network.loki.messenger.databinding.ViewMentionCandidateBinding
|
||||||
import org.session.libsession.messaging.mentions.Mention
|
import org.session.libsession.messaging.mentions.Mention
|
||||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
|
|
||||||
class MentionCandidateView : LinearLayout {
|
class MentionCandidateView : LinearLayout {
|
||||||
private lateinit var binding: ViewMentionCandidateBinding
|
private lateinit var binding: ViewMentionCandidateBinding
|
||||||
var mentionCandidate = Mention("", "")
|
var mentionCandidate = Mention("", "")
|
||||||
set(newValue) { field = newValue; update() }
|
set(newValue) { field = newValue; update() }
|
||||||
var glide: GlideRequests? = null
|
var glide: RequestManager? = null
|
||||||
var openGroupServer: String? = null
|
var openGroupServer: String? = null
|
||||||
var openGroupRoom: String? = null
|
var openGroupRoom: String? = null
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.conversation.v2.messages.QuoteView
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate
|
import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate
|
||||||
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
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.util.addTextChangedListener
|
import org.thoughtcrime.securesms.util.addTextChangedListener
|
||||||
import org.thoughtcrime.securesms.util.contains
|
import org.thoughtcrime.securesms.util.contains
|
||||||
|
|
||||||
@ -188,7 +188,7 @@ class InputBar @JvmOverloads constructor(
|
|||||||
|
|
||||||
private fun startRecordingVoiceMessage() { delegate?.startRecordingVoiceMessage() }
|
private fun startRecordingVoiceMessage() { delegate?.startRecordingVoiceMessage() }
|
||||||
|
|
||||||
fun draftQuote(thread: Recipient, message: MessageRecord, glide: GlideRequests) {
|
fun draftQuote(thread: Recipient, message: MessageRecord, glide: RequestManager) {
|
||||||
quoteView?.let(binding.inputBarAdditionalContentContainer::removeView)
|
quoteView?.let(binding.inputBarAdditionalContentContainer::removeView)
|
||||||
|
|
||||||
quote = message
|
quote = message
|
||||||
@ -238,7 +238,7 @@ class InputBar @JvmOverloads constructor(
|
|||||||
requestLayout()
|
requestLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLinkPreviewDraft(glide: GlideRequests, updatedLinkPreview: LinkPreview) {
|
fun updateLinkPreviewDraft(glide: RequestManager, updatedLinkPreview: LinkPreview) {
|
||||||
// Update our `linkPreview` property with the new (provided as an argument to this function)
|
// Update our `linkPreview` property with the new (provided as an argument to this function)
|
||||||
// then update the View from that.
|
// then update the View from that.
|
||||||
linkPreview = updatedLinkPreview.also { linkPreviewDraftView?.update(glide, it) }
|
linkPreview = updatedLinkPreview.also { linkPreviewDraftView?.update(glide, it) }
|
||||||
|
@ -148,11 +148,8 @@ class InputBarButton : RelativeLayout {
|
|||||||
|
|
||||||
private fun onDown(event: MotionEvent) {
|
private fun onDown(event: MotionEvent) {
|
||||||
expand()
|
expand()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
||||||
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
|
||||||
} else {
|
|
||||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
||||||
}
|
|
||||||
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
longPressCallback?.let { gestureHandler.removeCallbacks(it) }
|
||||||
val newLongPressCallback = Runnable { onLongPress?.invoke() }
|
val newLongPressCallback = Runnable { onLongPress?.invoke() }
|
||||||
this.longPressCallback = newLongPressCallback
|
this.longPressCallback = newLongPressCallback
|
||||||
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputConnection
|
import android.view.inputmethod.InputConnection
|
||||||
@ -57,7 +56,7 @@ class InputBarEditText : AppCompatEditText {
|
|||||||
InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, opts ->
|
InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, opts ->
|
||||||
val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
||||||
// read and display inputContentInfo asynchronously
|
// read and display inputContentInfo asynchronously
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && lacksPermission) {
|
if (lacksPermission) {
|
||||||
try {
|
try {
|
||||||
inputContentInfo.requestPermission()
|
inputContentInfo.requestPermission()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -24,7 +24,7 @@ import org.session.libsession.utilities.TextSecurePreferences
|
|||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
import org.thoughtcrime.securesms.MediaOverviewActivity
|
import org.thoughtcrime.securesms.media.MediaOverviewActivity
|
||||||
import org.thoughtcrime.securesms.ShortcutLauncherActivity
|
import org.thoughtcrime.securesms.ShortcutLauncherActivity
|
||||||
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
|
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
|
||||||
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
||||||
@ -149,10 +149,8 @@ object ConversationMenuHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showAllMedia(context: Context, thread: Recipient) {
|
private fun showAllMedia(context: Context, thread: Recipient) {
|
||||||
val intent = Intent(context, MediaOverviewActivity::class.java)
|
|
||||||
intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, thread.address)
|
|
||||||
val activity = context as AppCompatActivity
|
val activity = context as AppCompatActivity
|
||||||
activity.startActivity(intent)
|
activity.startActivity(MediaOverviewActivity.createIntent(context, thread.address))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun search(context: Context) {
|
private fun search(context: Context) {
|
||||||
|
@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.components.CornerMask
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
|
import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||||
|
|
||||||
class LinkPreviewView : LinearLayout {
|
class LinkPreviewView : LinearLayout {
|
||||||
@ -32,7 +32,7 @@ class LinkPreviewView : LinearLayout {
|
|||||||
// region Updating
|
// region Updating
|
||||||
fun bind(
|
fun bind(
|
||||||
message: MmsMessageRecord,
|
message: MmsMessageRecord,
|
||||||
glide: GlideRequests,
|
glide: RequestManager,
|
||||||
isStartOfMessageCluster: Boolean,
|
isStartOfMessageCluster: Boolean,
|
||||||
isEndOfMessageCluster: Boolean
|
isEndOfMessageCluster: Boolean
|
||||||
) {
|
) {
|
||||||
|
@ -18,7 +18,7 @@ import org.session.libsession.utilities.getColorFromAttr
|
|||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
|
||||||
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import org.thoughtcrime.securesms.util.getAccentColor
|
import org.thoughtcrime.securesms.util.getAccentColor
|
||||||
@ -68,7 +68,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
|||||||
// region Updating
|
// region Updating
|
||||||
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
|
fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient,
|
||||||
isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long,
|
isOutgoingMessage: Boolean, isOpenGroupInvitation: Boolean, threadID: Long,
|
||||||
isOriginalMissing: Boolean, glide: GlideRequests) {
|
isOriginalMissing: Boolean, glide: RequestManager) {
|
||||||
// Author
|
// Author
|
||||||
val author = contactDb.getContactWithAccountID(authorPublicKey)
|
val author = contactDb.getContactWithAccountID(authorPublicKey)
|
||||||
val localNumber = TextSecurePreferences.getLocalNumber(context)
|
val localNumber = TextSecurePreferences.getLocalNumber(context)
|
||||||
|
@ -38,8 +38,8 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getInt
|
|||||||
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
|
||||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import com.bumptech.glide.Glide
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil
|
import org.thoughtcrime.securesms.util.SearchUtil
|
||||||
import org.thoughtcrime.securesms.util.getAccentColor
|
import org.thoughtcrime.securesms.util.getAccentColor
|
||||||
@ -63,7 +63,7 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||||||
message: MessageRecord,
|
message: MessageRecord,
|
||||||
isStartOfMessageCluster: Boolean = true,
|
isStartOfMessageCluster: Boolean = true,
|
||||||
isEndOfMessageCluster: Boolean = true,
|
isEndOfMessageCluster: Boolean = true,
|
||||||
glide: GlideRequests = GlideApp.with(this),
|
glide: RequestManager = Glide.with(this),
|
||||||
thread: Recipient,
|
thread: Recipient,
|
||||||
searchQuery: String? = null,
|
searchQuery: String? = null,
|
||||||
contactIsTrusted: Boolean = true,
|
contactIsTrusted: Boolean = true,
|
||||||
|
@ -52,8 +52,8 @@ import org.thoughtcrime.securesms.database.ThreadDatabase
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||||
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import com.bumptech.glide.Glide
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
import org.thoughtcrime.securesms.util.toDp
|
import org.thoughtcrime.securesms.util.toDp
|
||||||
@ -141,7 +141,7 @@ class VisibleMessageView : FrameLayout {
|
|||||||
message: MessageRecord,
|
message: MessageRecord,
|
||||||
previous: MessageRecord? = null,
|
previous: MessageRecord? = null,
|
||||||
next: MessageRecord? = null,
|
next: MessageRecord? = null,
|
||||||
glide: GlideRequests = GlideApp.with(this),
|
glide: RequestManager = Glide.with(this),
|
||||||
searchQuery: String? = null,
|
searchQuery: String? = null,
|
||||||
contact: Contact? = null,
|
contact: Contact? = null,
|
||||||
senderAccountID: String,
|
senderAccountID: String,
|
||||||
|
@ -44,7 +44,7 @@ import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
|||||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import com.bumptech.glide.RequestManager;
|
||||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
@ -126,7 +126,7 @@ public class AttachmentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
public ListenableFuture<Boolean> setMedia(@NonNull final GlideRequests glideRequests,
|
public ListenableFuture<Boolean> setMedia(@NonNull final RequestManager glideRequests,
|
||||||
@NonNull final Uri uri,
|
@NonNull final Uri uri,
|
||||||
@NonNull final MediaType mediaType,
|
@NonNull final MediaType mediaType,
|
||||||
@NonNull final MediaConstraints constraints,
|
@NonNull final MediaConstraints constraints,
|
||||||
|
@ -25,8 +25,8 @@ import org.session.libsignal.utilities.SettableFuture
|
|||||||
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
|
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
|
||||||
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
|
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
|
||||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequest
|
import com.bumptech.glide.RequestBuilder
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.mms.Slide
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
|
|
||||||
open class ThumbnailView @JvmOverloads constructor(
|
open class ThumbnailView @JvmOverloads constructor(
|
||||||
@ -104,13 +104,13 @@ open class ThumbnailView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setImageResource(
|
fun setImageResource(
|
||||||
glide: GlideRequests,
|
glide: RequestManager,
|
||||||
slide: Slide,
|
slide: Slide,
|
||||||
isPreview: Boolean
|
isPreview: Boolean
|
||||||
): ListenableFuture<Boolean> = setImageResource(glide, slide, isPreview, 0, 0)
|
): ListenableFuture<Boolean> = setImageResource(glide, slide, isPreview, 0, 0)
|
||||||
|
|
||||||
fun setImageResource(
|
fun setImageResource(
|
||||||
glide: GlideRequests, slide: Slide,
|
glide: RequestManager, slide: Slide,
|
||||||
isPreview: Boolean, naturalWidth: Int,
|
isPreview: Boolean, naturalWidth: Int,
|
||||||
naturalHeight: Int
|
naturalHeight: Int
|
||||||
): ListenableFuture<Boolean> {
|
): ListenableFuture<Boolean> {
|
||||||
@ -152,9 +152,9 @@ open class ThumbnailView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildThumbnailGlideRequest(
|
private fun buildThumbnailGlideRequest(
|
||||||
glide: GlideRequests,
|
glide: RequestManager,
|
||||||
slide: Slide
|
slide: Slide
|
||||||
): GlideRequest<Drawable> = glide.load(DecryptableUri(slide.thumbnailUri!!))
|
): RequestBuilder<Drawable> = glide.load(DecryptableUri(slide.thumbnailUri!!))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.overrideDimensions()
|
.overrideDimensions()
|
||||||
.transition(DrawableTransitionOptions.withCrossFade())
|
.transition(DrawableTransitionOptions.withCrossFade())
|
||||||
@ -162,21 +162,21 @@ open class ThumbnailView @JvmOverloads constructor(
|
|||||||
.missingThumbnailPicture(slide.isInProgress)
|
.missingThumbnailPicture(slide.isInProgress)
|
||||||
|
|
||||||
private fun buildPlaceholderGlideRequest(
|
private fun buildPlaceholderGlideRequest(
|
||||||
glide: GlideRequests,
|
glide: RequestManager,
|
||||||
slide: Slide
|
slide: Slide
|
||||||
): GlideRequest<Bitmap> = glide.asBitmap()
|
): RequestBuilder<Bitmap> = glide.asBitmap()
|
||||||
.load(slide.getPlaceholderRes(context.theme))
|
.load(slide.getPlaceholderRes(context.theme))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.overrideDimensions()
|
.overrideDimensions()
|
||||||
.fitCenter()
|
.fitCenter()
|
||||||
|
|
||||||
open fun clear(glideRequests: GlideRequests) {
|
open fun clear(glideRequests: RequestManager) {
|
||||||
glideRequests.clear(binding.thumbnailImage)
|
glideRequests.clear(binding.thumbnailImage)
|
||||||
slide = null
|
slide = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setImageResource(
|
fun setImageResource(
|
||||||
glideRequests: GlideRequests,
|
glideRequests: RequestManager,
|
||||||
uri: Uri
|
uri: Uri
|
||||||
): ListenableFuture<Boolean> = glideRequests.load(DecryptableUri(uri))
|
): ListenableFuture<Boolean> = glideRequests.load(DecryptableUri(uri))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
@ -184,19 +184,19 @@ open class ThumbnailView @JvmOverloads constructor(
|
|||||||
.transform(CenterCrop())
|
.transform(CenterCrop())
|
||||||
.intoDrawableTargetAsFuture()
|
.intoDrawableTargetAsFuture()
|
||||||
|
|
||||||
private fun GlideRequest<Drawable>.intoDrawableTargetAsFuture() =
|
private fun RequestBuilder<Drawable>.intoDrawableTargetAsFuture() =
|
||||||
SettableFuture<Boolean>().also {
|
SettableFuture<Boolean>().also {
|
||||||
binding.run {
|
binding.run {
|
||||||
GlideDrawableListeningTarget(thumbnailImage, thumbnailLoadIndicator, it)
|
GlideDrawableListeningTarget(thumbnailImage, thumbnailLoadIndicator, it)
|
||||||
}.let { into(it) }
|
}.let { into(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> GlideRequest<T>.overrideDimensions() =
|
private fun <T> RequestBuilder<T>.overrideDimensions() =
|
||||||
dimensDelegate.resourceSize().takeIf { 0 !in it }
|
dimensDelegate.resourceSize().takeIf { 0 !in it }
|
||||||
?.let { override(it[WIDTH], it[HEIGHT]) }
|
?.let { override(it[WIDTH], it[HEIGHT]) }
|
||||||
?: override(getDefaultWidth(), getDefaultHeight())
|
?: override(getDefaultWidth(), getDefaultHeight())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> GlideRequest<T>.missingThumbnailPicture(
|
private fun <T> RequestBuilder<T>.missingThumbnailPicture(
|
||||||
inProgress: Boolean
|
inProgress: Boolean
|
||||||
) = takeIf { inProgress } ?: apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture))
|
) = takeIf { inProgress } ?: apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture))
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package org.thoughtcrime.securesms.crypto;
|
package org.thoughtcrime.securesms.crypto;
|
||||||
|
|
||||||
|
|
||||||
|
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A provider that is responsible for creating or retrieving the AttachmentSecret model.
|
* A provider that is responsible for creating or retrieving the AttachmentSecret model.
|
||||||
*
|
*
|
||||||
@ -59,31 +59,22 @@ public class AttachmentSecretProvider {
|
|||||||
{
|
{
|
||||||
AttachmentSecret attachmentSecret = AttachmentSecret.fromString(unencryptedSecret);
|
AttachmentSecret attachmentSecret = AttachmentSecret.fromString(unencryptedSecret);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes());
|
||||||
return attachmentSecret;
|
|
||||||
} else {
|
|
||||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes());
|
|
||||||
|
|
||||||
TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
|
TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
|
||||||
TextSecurePreferences.setAttachmentUnencryptedSecret(context, null);
|
TextSecurePreferences.setAttachmentUnencryptedSecret(context, null);
|
||||||
|
|
||||||
return attachmentSecret;
|
return attachmentSecret;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AttachmentSecret getEncryptedAttachmentSecret(@NonNull String serializedEncryptedSecret) {
|
private AttachmentSecret getEncryptedAttachmentSecret(@NonNull String serializedEncryptedSecret) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret);
|
||||||
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
|
return AttachmentSecret.fromString(new String(KeyStoreHelper.unseal(encryptedSecret)));
|
||||||
} else {
|
|
||||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret);
|
|
||||||
return AttachmentSecret.fromString(new String(KeyStoreHelper.unseal(encryptedSecret)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AttachmentSecret createAndStoreAttachmentSecret(@NonNull Context context) {
|
private AttachmentSecret createAndStoreAttachmentSecret(@NonNull Context context) {
|
||||||
SecureRandom random = new SecureRandom();
|
|
||||||
byte[] secret = new byte[32];
|
byte[] secret = new byte[32];
|
||||||
random.nextBytes(secret);
|
SECURE_RANDOM.nextBytes(secret);
|
||||||
|
|
||||||
AttachmentSecret attachmentSecret = new AttachmentSecret(null, null, secret);
|
AttachmentSecret attachmentSecret = new AttachmentSecret(null, null, secret);
|
||||||
storeAttachmentSecret(context, attachmentSecret);
|
storeAttachmentSecret(context, attachmentSecret);
|
||||||
@ -92,12 +83,8 @@ public class AttachmentSecretProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void storeAttachmentSecret(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret) {
|
private void storeAttachmentSecret(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes());
|
||||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes());
|
TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
|
||||||
TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
|
|
||||||
} else {
|
|
||||||
TextSecurePreferences.setAttachmentUnencryptedSecret(context, attachmentSecret.serialize());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -42,9 +42,8 @@ class BiometricSecretProvider {
|
|||||||
builder.setUnlockedDeviceRequired(true)
|
builder.setUnlockedDeviceRequired(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
builder.setInvalidatedByBiometricEnrollment(true)
|
||||||
builder.setInvalidatedByBiometricEnrollment(true)
|
|
||||||
}
|
|
||||||
keyGenerator.initialize(builder.build())
|
keyGenerator.initialize(builder.build())
|
||||||
keyGenerator.generateKeyPair()
|
keyGenerator.generateKeyPair()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package org.thoughtcrime.securesms.crypto;
|
package org.thoughtcrime.securesms.crypto;
|
||||||
|
|
||||||
|
|
||||||
|
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@ -8,7 +10,6 @@ import androidx.annotation.NonNull;
|
|||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
public class DatabaseSecretProvider {
|
public class DatabaseSecretProvider {
|
||||||
|
|
||||||
@ -35,43 +36,30 @@ public class DatabaseSecretProvider {
|
|||||||
try {
|
try {
|
||||||
DatabaseSecret databaseSecret = new DatabaseSecret(unencryptedSecret);
|
DatabaseSecret databaseSecret = new DatabaseSecret(unencryptedSecret);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
|
||||||
return databaseSecret;
|
|
||||||
} else {
|
|
||||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
|
|
||||||
|
|
||||||
TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
|
TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
|
||||||
TextSecurePreferences.setDatabaseUnencryptedSecret(context, null);
|
TextSecurePreferences.setDatabaseUnencryptedSecret(context, null);
|
||||||
|
|
||||||
return databaseSecret;
|
return databaseSecret;
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DatabaseSecret getEncryptedDatabaseSecret(@NonNull String serializedEncryptedSecret) {
|
private DatabaseSecret getEncryptedDatabaseSecret(@NonNull String serializedEncryptedSecret) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret);
|
||||||
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
|
return new DatabaseSecret(KeyStoreHelper.unseal(encryptedSecret));
|
||||||
} else {
|
|
||||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret);
|
|
||||||
return new DatabaseSecret(KeyStoreHelper.unseal(encryptedSecret));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) {
|
private DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) {
|
||||||
SecureRandom random = new SecureRandom();
|
|
||||||
byte[] secret = new byte[32];
|
byte[] secret = new byte[32];
|
||||||
random.nextBytes(secret);
|
SECURE_RANDOM.nextBytes(secret);
|
||||||
|
|
||||||
DatabaseSecret databaseSecret = new DatabaseSecret(secret);
|
DatabaseSecret databaseSecret = new DatabaseSecret(secret);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
|
||||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
|
TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
|
||||||
TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
|
|
||||||
} else {
|
|
||||||
TextSecurePreferences.setDatabaseUnencryptedSecret(context, databaseSecret.asString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return databaseSecret;
|
return databaseSecret;
|
||||||
}
|
}
|
||||||
|
@ -129,27 +129,19 @@ public class IdentityKeyUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String getUnencryptedSecret(String key, String unencryptedSecret, Context context) {
|
private static String getUnencryptedSecret(String key, String unencryptedSecret, Context context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(unencryptedSecret.getBytes());
|
||||||
return unencryptedSecret;
|
|
||||||
} else {
|
|
||||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(unencryptedSecret.getBytes());
|
|
||||||
|
|
||||||
// save the encrypted suffix secret "key_encrypted"
|
// save the encrypted suffix secret "key_encrypted"
|
||||||
save(context,key+ENCRYPTED_SUFFIX,encryptedSecret.serialize());
|
save(context,key+ENCRYPTED_SUFFIX,encryptedSecret.serialize());
|
||||||
// delete the regular secret "key"
|
// delete the regular secret "key"
|
||||||
delete(context,key);
|
delete(context,key);
|
||||||
|
|
||||||
return unencryptedSecret;
|
return unencryptedSecret;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getEncryptedSecret(String encryptedSecret) {
|
private static String getEncryptedSecret(String encryptedSecret) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
KeyStoreHelper.SealedData sealedData = KeyStoreHelper.SealedData.fromString(encryptedSecret);
|
||||||
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
|
return new String(KeyStoreHelper.unseal(sealedData));
|
||||||
} else {
|
|
||||||
KeyStoreHelper.SealedData sealedData = KeyStoreHelper.SealedData.fromString(encryptedSecret);
|
|
||||||
return new String(KeyStoreHelper.unseal(sealedData));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -157,17 +149,14 @@ public class IdentityKeyUtil {
|
|||||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||||
Editor preferencesEditor = preferences.edit();
|
Editor preferencesEditor = preferences.edit();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
boolean isEncryptedSuffix = key.endsWith(ENCRYPTED_SUFFIX);
|
||||||
boolean isEncryptedSuffix = key.endsWith(ENCRYPTED_SUFFIX);
|
if (isEncryptedSuffix) {
|
||||||
if (isEncryptedSuffix) {
|
|
||||||
preferencesEditor.putString(key, value);
|
|
||||||
} else {
|
|
||||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(value.getBytes());
|
|
||||||
preferencesEditor.putString(key+ENCRYPTED_SUFFIX, encryptedSecret.serialize());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
preferencesEditor.putString(key, value);
|
preferencesEditor.putString(key, value);
|
||||||
|
} else {
|
||||||
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(value.getBytes());
|
||||||
|
preferencesEditor.putString(key+ENCRYPTED_SUFFIX, encryptedSecret.serialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
|
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package org.thoughtcrime.securesms.crypto;
|
package org.thoughtcrime.securesms.crypto;
|
||||||
|
|
||||||
|
|
||||||
|
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
@ -11,7 +13,6 @@ import java.io.OutputStream;
|
|||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.CipherOutputStream;
|
import javax.crypto.CipherOutputStream;
|
||||||
@ -31,7 +32,7 @@ public class ModernEncryptingPartOutputStream {
|
|||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
byte[] random = new byte[32];
|
byte[] random = new byte[32];
|
||||||
new SecureRandom().nextBytes(random);
|
SECURE_RANDOM.nextBytes(random);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Mac mac = Mac.getInstance("HmacSHA256");
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
@ -966,11 +966,6 @@ public class AttachmentDatabase extends Database {
|
|||||||
|
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
private ThumbnailData generateVideoThumbnail(AttachmentId attachmentId) {
|
private ThumbnailData generateVideoThumbnail(AttachmentId attachmentId) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
||||||
Log.w(TAG, "Video thumbnails not supported...");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, DATA);
|
DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, DATA);
|
||||||
|
|
||||||
if (dataInfo == null) {
|
if (dataInfo == null) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.database;
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
|
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -26,7 +28,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
|||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -303,7 +304,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
|||||||
public void updateProfilePicture(String groupID, byte[] newValue) {
|
public void updateProfilePicture(String groupID, byte[] newValue) {
|
||||||
long avatarId;
|
long avatarId;
|
||||||
|
|
||||||
if (newValue != null) avatarId = Math.abs(new SecureRandom().nextLong());
|
if (newValue != null) avatarId = Math.abs(SECURE_RANDOM.nextLong());
|
||||||
else avatarId = 0;
|
else avatarId = 0;
|
||||||
|
|
||||||
|
|
||||||
@ -458,12 +459,6 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
|||||||
database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId});
|
database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId});
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] allocateGroupId() {
|
|
||||||
byte[] groupId = new byte[16];
|
|
||||||
new SecureRandom().nextBytes(groupId);
|
|
||||||
return groupId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasGroup(@NonNull String groupId) {
|
public boolean hasGroup(@NonNull String groupId) {
|
||||||
try (Cursor cursor = databaseHelper.getReadableDatabase().rawQuery(
|
try (Cursor cursor = databaseHelper.getReadableDatabase().rawQuery(
|
||||||
"SELECT 1 FROM " + TABLE_NAME + " WHERE " + GROUP_ID + " = ? LIMIT 1",
|
"SELECT 1 FROM " + TABLE_NAME + " WHERE " + GROUP_ID + " = ? LIMIT 1",
|
||||||
|
@ -166,8 +166,6 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
|
|
||||||
const val RESET_SEQ_NO = "UPDATE $lastMessageServerIDTable SET $lastMessageServerID = 0;"
|
const val RESET_SEQ_NO = "UPDATE $lastMessageServerIDTable SET $lastMessageServerID = 0;"
|
||||||
|
|
||||||
const val EMPTY_VERSION = "0.0.0"
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,15 +173,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(snodePoolTable, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor ->
|
return database.get(snodePoolTable, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor ->
|
||||||
val snodePoolAsString = cursor.getString(cursor.getColumnIndexOrThrow(snodePool))
|
val snodePoolAsString = cursor.getString(cursor.getColumnIndexOrThrow(snodePool))
|
||||||
snodePoolAsString.split(", ").mapNotNull { snodeAsString ->
|
snodePoolAsString.split(", ").mapNotNull(::Snode)
|
||||||
val components = snodeAsString.split("-")
|
|
||||||
val address = components[0]
|
|
||||||
val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null
|
|
||||||
val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null
|
|
||||||
val x25519Key = components.getOrNull(3) ?: return@mapNotNull null
|
|
||||||
val version = components.getOrNull(4) ?: EMPTY_VERSION
|
|
||||||
Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version)
|
|
||||||
}
|
|
||||||
}?.toSet() ?: setOf()
|
}?.toSet() ?: setOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,18 +221,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
fun get(indexPath: String): Snode? {
|
fun get(indexPath: String): Snode? {
|
||||||
return database.get(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor ->
|
return database.get(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor ->
|
||||||
val snodeAsString = cursor.getString(cursor.getColumnIndexOrThrow(snode))
|
Snode(cursor.getString(cursor.getColumnIndexOrThrow(snode)))
|
||||||
val components = snodeAsString.split("-")
|
|
||||||
val address = components[0]
|
|
||||||
val port = components.getOrNull(1)?.toIntOrNull()
|
|
||||||
val ed25519Key = components.getOrNull(2)
|
|
||||||
val x25519Key = components.getOrNull(3)
|
|
||||||
val version = components.getOrNull(4) ?: EMPTY_VERSION
|
|
||||||
if (port != null && ed25519Key != null && x25519Key != null) {
|
|
||||||
Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val result = mutableListOf<List<Snode>>()
|
val result = mutableListOf<List<Snode>>()
|
||||||
@ -276,15 +255,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(swarmTable, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor ->
|
return database.get(swarmTable, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor ->
|
||||||
val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm))
|
val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm))
|
||||||
swarmAsString.split(", ").mapNotNull { targetAsString ->
|
swarmAsString.split(", ").mapNotNull(::Snode)
|
||||||
val components = targetAsString.split("-")
|
|
||||||
val address = components[0]
|
|
||||||
val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null
|
|
||||||
val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null
|
|
||||||
val x25519Key = components.getOrNull(3) ?: return@mapNotNull null
|
|
||||||
val version = components.getOrNull(4) ?: EMPTY_VERSION
|
|
||||||
Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version)
|
|
||||||
}
|
|
||||||
}?.toSet()
|
}?.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,11 +46,11 @@ import org.session.libsession.utilities.IdentityKeyMismatchList
|
|||||||
import org.session.libsession.utilities.NetworkFailure
|
import org.session.libsession.utilities.NetworkFailure
|
||||||
import org.session.libsession.utilities.NetworkFailureList
|
import org.session.libsession.utilities.NetworkFailureList
|
||||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled
|
import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled
|
||||||
import org.session.libsession.utilities.Util.toIsoBytes
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
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.queue
|
import org.session.libsignal.utilities.ThreadUtils.queue
|
||||||
|
import org.session.libsignal.utilities.Util.SECURE_RANDOM
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment
|
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase.InsertListener
|
import org.thoughtcrime.securesms.database.SmsDatabase.InsertListener
|
||||||
@ -66,7 +66,6 @@ import org.thoughtcrime.securesms.mms.SlideDeck
|
|||||||
import org.thoughtcrime.securesms.util.asSequence
|
import org.thoughtcrime.securesms.util.asSequence
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
|
|
||||||
class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : MessagingDatabase(context, databaseHelper) {
|
class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : MessagingDatabase(context, databaseHelper) {
|
||||||
@ -1200,7 +1199,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
|||||||
|
|
||||||
inner class OutgoingMessageReader(private val message: OutgoingMediaMessage?,
|
inner class OutgoingMessageReader(private val message: OutgoingMediaMessage?,
|
||||||
private val threadId: Long) {
|
private val threadId: Long) {
|
||||||
private val id = SecureRandom().nextLong()
|
private val id = SECURE_RANDOM.nextLong()
|
||||||
val current: MessageRecord
|
val current: MessageRecord
|
||||||
get() {
|
get() {
|
||||||
val slideDeck = SlideDeck(context, message!!.attachments)
|
val slideDeck = SlideDeck(context, message!!.attachments)
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.database;
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
|
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
@ -49,7 +51,6 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
|||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@ -784,7 +785,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
public OutgoingMessageReader(OutgoingTextMessage message, long threadId) {
|
public OutgoingMessageReader(OutgoingTextMessage message, long threadId) {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.threadId = threadId;
|
this.threadId = threadId;
|
||||||
this.id = new SecureRandom().nextLong();
|
this.id = SECURE_RANDOM.nextLong();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageRecord getCurrent() {
|
public MessageRecord getCurrent() {
|
||||||
|
@ -252,11 +252,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||||
String channelId = context.getString(R.string.NotificationChannel_failures);
|
String channelId = context.getString(R.string.NotificationChannel_failures);
|
||||||
|
|
||||||
if (NotificationChannels.supported()) {
|
NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH);
|
||||||
NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH);
|
channel.enableVibration(true);
|
||||||
channel.enableVibration(true);
|
notificationManager.createNotificationChannel(channel);
|
||||||
notificationManager.createNotificationChannel(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
@ -266,10 +264,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
.setContentText(context.getString(R.string.ErrorNotifier_migration_downgrade))
|
.setContentText(context.getString(R.string.ErrorNotifier_migration_downgrade))
|
||||||
.setAutoCancel(true);
|
.setAutoCancel(true);
|
||||||
|
|
||||||
if (!NotificationChannels.supported()) {
|
|
||||||
builder.setPriority(NotificationCompat.PRIORITY_HIGH);
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager.notify(5874, builder.build());
|
notificationManager.notify(5874, builder.build());
|
||||||
|
|
||||||
// Throw the error (app will crash but there is nothing else we can do unfortunately)
|
// Throw the error (app will crash but there is nothing else we can do unfortunately)
|
||||||
|
@ -29,8 +29,8 @@ import org.session.libsession.utilities.ViewUtil;
|
|||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||||
import org.thoughtcrime.securesms.giph.model.GiphyImage;
|
import org.thoughtcrime.securesms.giph.model.GiphyImage;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import com.bumptech.glide.Glide;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import com.bumptech.glide.RequestManager;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
@ -43,7 +43,7 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
|||||||
private static final String TAG = GiphyAdapter.class.getSimpleName();
|
private static final String TAG = GiphyAdapter.class.getSimpleName();
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final GlideRequests glideRequests;
|
private final RequestManager glideRequests;
|
||||||
|
|
||||||
private List<GiphyImage> images;
|
private List<GiphyImage> images;
|
||||||
private OnItemClickListener listener;
|
private OnItemClickListener listener;
|
||||||
@ -117,7 +117,7 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GiphyAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, @NonNull List<GiphyImage> images) {
|
GiphyAdapter(@NonNull Context context, @NonNull RequestManager glideRequests, @NonNull List<GiphyImage> images) {
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
this.glideRequests = glideRequests;
|
this.glideRequests = glideRequests;
|
||||||
this.images = images;
|
this.images = images;
|
||||||
@ -150,7 +150,7 @@ class GiphyAdapter extends RecyclerView.Adapter<GiphyAdapter.GiphyViewHolder> {
|
|||||||
holder.thumbnail.setAspectRatio(image.getGifAspectRatio());
|
holder.thumbnail.setAspectRatio(image.getGifAspectRatio());
|
||||||
holder.gifProgress.setVisibility(View.GONE);
|
holder.gifProgress.setVisibility(View.GONE);
|
||||||
|
|
||||||
RequestBuilder<Drawable> thumbnailRequest = GlideApp.with(context)
|
RequestBuilder<Drawable> thumbnailRequest = Glide.with(context)
|
||||||
.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
|
.load(new ChunkedImageUrl(image.getStillUrl(), image.getStillSize()))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE);
|
.diskCacheStrategy(DiskCacheStrategy.NONE);
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import android.widget.TextView;
|
|||||||
import org.thoughtcrime.securesms.giph.model.GiphyImage;
|
import org.thoughtcrime.securesms.giph.model.GiphyImage;
|
||||||
import org.thoughtcrime.securesms.giph.net.GiphyLoader;
|
import org.thoughtcrime.securesms.giph.net.GiphyLoader;
|
||||||
import org.thoughtcrime.securesms.giph.util.InfiniteScrollListener;
|
import org.thoughtcrime.securesms.giph.util.InfiniteScrollListener;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import com.bumptech.glide.Glide;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
import org.session.libsession.utilities.ViewUtil;
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ public abstract class GiphyFragment extends Fragment implements LoaderManager.Lo
|
|||||||
public void onActivityCreated(Bundle bundle) {
|
public void onActivityCreated(Bundle bundle) {
|
||||||
super.onActivityCreated(bundle);
|
super.onActivityCreated(bundle);
|
||||||
|
|
||||||
this.giphyAdapter = new GiphyAdapter(getActivity(), GlideApp.with(this), new LinkedList<>());
|
this.giphyAdapter = new GiphyAdapter(getActivity(), Glide.with(this), new LinkedList<>());
|
||||||
this.giphyAdapter.setListener(this);
|
this.giphyAdapter.setListener(this);
|
||||||
|
|
||||||
setLayoutManager(TextSecurePreferences.isGifSearchInGridLayout(getContext()));
|
setLayoutManager(TextSecurePreferences.isGifSearchInGridLayout(getContext()));
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
package org.thoughtcrime.securesms.glide;
|
package org.thoughtcrime.securesms.glide;
|
||||||
|
|
||||||
|
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
import okhttp3.Headers;
|
import okhttp3.Headers;
|
||||||
import okhttp3.Interceptor;
|
import okhttp3.Interceptor;
|
||||||
@ -30,15 +31,15 @@ public class PaddedHeadersInterceptor implements Interceptor {
|
|||||||
|
|
||||||
private @NonNull Headers getPaddedHeaders(@NonNull Headers headers) {
|
private @NonNull Headers getPaddedHeaders(@NonNull Headers headers) {
|
||||||
return headers.newBuilder()
|
return headers.newBuilder()
|
||||||
.add(PADDING_HEADER, getRandomString(new SecureRandom(), MIN_RANDOM_BYTES, MAX_RANDOM_BYTES))
|
.add(PADDING_HEADER, getRandomString(MIN_RANDOM_BYTES, MAX_RANDOM_BYTES))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull String getRandomString(@NonNull SecureRandom secureRandom, int minLength, int maxLength) {
|
private static @NonNull String getRandomString(int minLength, int maxLength) {
|
||||||
char[] buffer = new char[secureRandom.nextInt(maxLength - minLength) + minLength];
|
char[] buffer = new char[SECURE_RANDOM.nextInt(maxLength - minLength) + minLength];
|
||||||
|
|
||||||
for (int i = 0 ; i < buffer.length; i++) {
|
for (int i = 0 ; i < buffer.length; i++) {
|
||||||
buffer[i] = (char) (secureRandom.nextInt(74) + 48); // Random char from 0-Z
|
buffer[i] = (char) (SECURE_RANDOM.nextInt(74) + 48); // Random char from 0-Z
|
||||||
}
|
}
|
||||||
|
|
||||||
return new String(buffer);
|
return new String(buffer);
|
||||||
|
@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import com.bumptech.glide.Glide
|
||||||
import org.thoughtcrime.securesms.util.fadeIn
|
import org.thoughtcrime.securesms.util.fadeIn
|
||||||
import org.thoughtcrime.securesms.util.fadeOut
|
import org.thoughtcrime.securesms.util.fadeOut
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -55,7 +55,7 @@ class CreateGroupFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
val adapter = SelectContactsAdapter(requireContext(), GlideApp.with(requireContext()))
|
val adapter = SelectContactsAdapter(requireContext(), Glide.with(requireContext()))
|
||||||
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
|
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
|
||||||
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
||||||
binding.contactSearch.callbacks = object : KeyboardPageSearchView.Callbacks {
|
binding.contactSearch.callbacks = object : KeyboardPageSearchView.Callbacks {
|
||||||
|
@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.database.Storage
|
|||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup
|
import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import com.bumptech.glide.Glide
|
||||||
import org.thoughtcrime.securesms.util.fadeIn
|
import org.thoughtcrime.securesms.util.fadeIn
|
||||||
import org.thoughtcrime.securesms.util.fadeOut
|
import org.thoughtcrime.securesms.util.fadeOut
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -76,9 +76,9 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
|
|
||||||
private val memberListAdapter by lazy {
|
private val memberListAdapter by lazy {
|
||||||
if (isSelfAdmin)
|
if (isSelfAdmin)
|
||||||
EditClosedGroupMembersAdapter(this, GlideApp.with(this), isSelfAdmin, this::onMemberClick)
|
EditClosedGroupMembersAdapter(this, Glide.with(this), isSelfAdmin, this::onMemberClick)
|
||||||
else
|
else
|
||||||
EditClosedGroupMembersAdapter(this, GlideApp.with(this), isSelfAdmin)
|
EditClosedGroupMembersAdapter(this, Glide.with(this), isSelfAdmin)
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var mainContentContainer: LinearLayout
|
private lateinit var mainContentContainer: LinearLayout
|
||||||
|
@ -5,13 +5,13 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.thoughtcrime.securesms.contacts.UserView
|
import org.thoughtcrime.securesms.contacts.UserView
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
|
||||||
class EditClosedGroupMembersAdapter(
|
class EditClosedGroupMembersAdapter(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val glide: GlideRequests,
|
private val glide: RequestManager,
|
||||||
private val admin: Boolean,
|
private val admin: Boolean,
|
||||||
private val memberClickListener: ((String) -> Unit)? = null
|
private val memberClickListener: ((String) -> Unit)? = null
|
||||||
) : RecyclerView.Adapter<EditClosedGroupMembersAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<EditClosedGroupMembersAdapter.ViewHolder>() {
|
||||||
|
@ -62,8 +62,8 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout
|
|||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchResult
|
import org.thoughtcrime.securesms.home.search.GlobalSearchResult
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel
|
import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel
|
||||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import com.bumptech.glide.Glide
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import com.bumptech.glide.RequestManager
|
||||||
import org.thoughtcrime.securesms.notifications.PushRegistry
|
import org.thoughtcrime.securesms.notifications.PushRegistry
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.preferences.SettingsActivity
|
import org.thoughtcrime.securesms.preferences.SettingsActivity
|
||||||
@ -89,7 +89,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
||||||
|
|
||||||
private lateinit var binding: ActivityHomeBinding
|
private lateinit var binding: ActivityHomeBinding
|
||||||
private lateinit var glide: GlideRequests
|
private lateinit var glide: RequestManager
|
||||||
|
|
||||||
@Inject lateinit var threadDb: ThreadDatabase
|
@Inject lateinit var threadDb: ThreadDatabase
|
||||||
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
||||||
@ -148,7 +148,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// Set custom toolbar
|
// Set custom toolbar
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
// Set up Glide
|
// Set up Glide
|
||||||
glide = GlideApp.with(this)
|
glide = Glide.with(this)
|
||||||
// Set up toolbar buttons
|
// Set up toolbar buttons
|
||||||
binding.profileButton.setOnClickListener { openSettings() }
|
binding.profileButton.setOnClickListener { openSettings() }
|
||||||
binding.searchViewContainer.setOnClickListener {
|
binding.searchViewContainer.setOnClickListener {
|
||||||
|
@ -7,10 +7,10 @@ import androidx.recyclerview.widget.DiffUtil
|
|||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||||
|
import com.bumptech.glide.RequestManager
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
|
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
|
||||||
|
|
||||||
class HomeAdapter(
|
class HomeAdapter(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -74,7 +74,7 @@ class HomeAdapter(
|
|||||||
return data.threads[offsetPosition].threadId
|
return data.threads[offsetPosition].threadId
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var glide: GlideRequests
|
lateinit var glide: RequestManager
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
||||||
when (viewType) {
|
when (viewType) {
|
||||||
@ -104,7 +104,6 @@ class HomeAdapter(
|
|||||||
holder.binding.run {
|
holder.binding.run {
|
||||||
messageRequests?.let {
|
messageRequests?.let {
|
||||||
unreadCountTextView.text = it.count
|
unreadCountTextView.text = it.count
|
||||||
timestampTextView.text = it.timestamp
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import android.widget.RelativeLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@ -38,7 +39,6 @@ import org.thoughtcrime.securesms.util.disableClipping
|
|||||||
import org.thoughtcrime.securesms.util.fadeIn
|
import org.thoughtcrime.securesms.util.fadeIn
|
||||||
import org.thoughtcrime.securesms.util.fadeOut
|
import org.thoughtcrime.securesms.util.fadeOut
|
||||||
import org.thoughtcrime.securesms.util.getAccentColor
|
import org.thoughtcrime.securesms.util.getAccentColor
|
||||||
import org.thoughtcrime.securesms.util.getColorWithID
|
|
||||||
|
|
||||||
class PathActivity : PassphraseRequiredActionBarActivity() {
|
class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||||
private lateinit var binding: ActivityPathBinding
|
private lateinit var binding: ActivityPathBinding
|
||||||
@ -264,8 +264,8 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
job?.cancel()
|
job?.cancel()
|
||||||
job = GlobalScope.launch {
|
job = GlobalScope.launch {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
delay(dotAnimationStartDelay)
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
delay(dotAnimationStartDelay)
|
|
||||||
expand()
|
expand()
|
||||||
delay(EXPAND_ANIM_DELAY_MILLS)
|
delay(EXPAND_ANIM_DELAY_MILLS)
|
||||||
collapse()
|
collapse()
|
||||||
@ -283,7 +283,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
private fun expand() {
|
private fun expand() {
|
||||||
dotView.animateSizeChange(R.dimen.path_row_dot_size, R.dimen.path_row_expanded_dot_size)
|
dotView.animateSizeChange(R.dimen.path_row_dot_size, R.dimen.path_row_expanded_dot_size)
|
||||||
@ColorRes val startColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
|
@ColorRes val startColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
|
||||||
val startColor = context.resources.getColorWithID(startColorID, context.theme)
|
val startColor = ContextCompat.getColor(context, startColorID)
|
||||||
val endColor = context.getAccentColor()
|
val endColor = context.getAccentColor()
|
||||||
GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor)
|
GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor)
|
||||||
}
|
}
|
||||||
@ -292,7 +292,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
dotView.animateSizeChange(R.dimen.path_row_expanded_dot_size, R.dimen.path_row_dot_size)
|
dotView.animateSizeChange(R.dimen.path_row_expanded_dot_size, R.dimen.path_row_dot_size)
|
||||||
@ColorRes val endColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
|
@ColorRes val endColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
|
||||||
val startColor = context.getAccentColor()
|
val startColor = context.getAccentColor()
|
||||||
val endColor = context.resources.getColorWithID(endColorID, context.theme)
|
val endColor = ContextCompat.getColor(context, endColorID)
|
||||||
GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor)
|
GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import android.graphics.Paint
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.coroutineScope
|
import androidx.lifecycle.coroutineScope
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -17,7 +18,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ViewUtil
|
import org.thoughtcrime.securesms.conversation.v2.ViewUtil
|
||||||
import org.thoughtcrime.securesms.util.getColorWithID
|
|
||||||
import org.thoughtcrime.securesms.util.toPx
|
import org.thoughtcrime.securesms.util.toPx
|
||||||
|
|
||||||
class PathStatusView : View {
|
class PathStatusView : View {
|
||||||
@ -104,7 +104,7 @@ class PathStatusView : View {
|
|||||||
sessionShadowColor = hasPathsColor
|
sessionShadowColor = hasPathsColor
|
||||||
} else {
|
} else {
|
||||||
setBackgroundResource(R.drawable.paths_building_dot)
|
setBackgroundResource(R.drawable.paths_building_dot)
|
||||||
val pathsBuildingColor = resources.getColorWithID(R.color.paths_building, context.theme)
|
val pathsBuildingColor = ContextCompat.getColor(context, R.color.paths_building)
|
||||||
mainColor = pathsBuildingColor
|
mainColor = pathsBuildingColor
|
||||||
sessionShadowColor = pathsBuildingColor
|
sessionShadowColor = pathsBuildingColor
|
||||||
}
|
}
|
||||||
|
@ -10,21 +10,19 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.session.libsignal.utilities.SettableFuture
|
|
||||||
import org.thoughtcrime.securesms.search.SearchRepository
|
import org.thoughtcrime.securesms.search.SearchRepository
|
||||||
import org.thoughtcrime.securesms.search.model.SearchResult
|
import org.thoughtcrime.securesms.search.model.SearchResult
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@ -47,11 +45,8 @@ class GlobalSearchViewModel @Inject constructor(
|
|||||||
// User input delay in case we get a new query within a few hundred ms this
|
// User input delay in case we get a new query within a few hundred ms this
|
||||||
// coroutine will be cancelled and the expensive query will not be run.
|
// coroutine will be cancelled and the expensive query will not be run.
|
||||||
delay(300)
|
delay(300)
|
||||||
val settableFuture = SettableFuture<SearchResult>()
|
|
||||||
searchRepository.query(query.toString(), settableFuture::set)
|
|
||||||
try {
|
try {
|
||||||
// search repository doesn't play nicely with suspend functions (yet)
|
searchRepository.suspendQuery(query.toString()).toGlobalSearchResult()
|
||||||
settableFuture.get(10_000, TimeUnit.MILLISECONDS).toGlobalSearchResult()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
GlobalSearchResult(query.toString())
|
GlobalSearchResult(query.toString())
|
||||||
}
|
}
|
||||||
@ -69,6 +64,12 @@ class GlobalSearchViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun SearchRepository.suspendQuery(query: String): SearchResult {
|
||||||
|
return suspendCoroutine { cont ->
|
||||||
|
query(query, cont::resume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-emit whenever refreshes emits.
|
* Re-emit whenever refreshes emits.
|
||||||
* */
|
* */
|
||||||
|
@ -8,11 +8,6 @@ public interface Constraint {
|
|||||||
|
|
||||||
boolean isMet();
|
boolean isMet();
|
||||||
|
|
||||||
@NonNull String getFactoryKey();
|
|
||||||
|
|
||||||
@RequiresApi(26)
|
|
||||||
void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder);
|
|
||||||
|
|
||||||
interface Factory<T extends Constraint> {
|
interface Factory<T extends Constraint> {
|
||||||
T create();
|
T create();
|
||||||
}
|
}
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.jobmanager;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service that keeps the application in memory while the app is closed.
|
|
||||||
*
|
|
||||||
* Important: Should only be used on API < 26.
|
|
||||||
*/
|
|
||||||
public class KeepAliveService extends Service {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable IBinder onBind(Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
return START_STICKY;
|
|
||||||
}
|
|
||||||
}
|
|
@ -28,17 +28,6 @@ public class NetworkConstraint implements Constraint {
|
|||||||
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
|
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull String getFactoryKey() {
|
|
||||||
return KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(26)
|
|
||||||
@Override
|
|
||||||
public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) {
|
|
||||||
jobInfoBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Factory implements Constraint.Factory<NetworkConstraint> {
|
public static final class Factory implements Constraint.Factory<NetworkConstraint> {
|
||||||
|
|
||||||
private final Application application;
|
private final Application application;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.logging;
|
package org.thoughtcrime.securesms.logging;
|
||||||
|
|
||||||
import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK;
|
import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK;
|
||||||
|
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
@ -17,7 +18,6 @@ import java.io.IOException;
|
|||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
@ -64,7 +64,7 @@ class LogFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void writeEntry(@NonNull String entry) throws IOException {
|
void writeEntry(@NonNull String entry) throws IOException {
|
||||||
new SecureRandom().nextBytes(ivBuffer);
|
SECURE_RANDOM.nextBytes(ivBuffer);
|
||||||
|
|
||||||
byte[] plaintext = entry.getBytes();
|
byte[] plaintext = entry.getBytes();
|
||||||
try {
|
try {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.logging;
|
package org.thoughtcrime.securesms.logging;
|
||||||
|
|
||||||
|
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@ -9,7 +11,6 @@ import org.session.libsignal.utilities.Base64;
|
|||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
class LogSecretProvider {
|
class LogSecretProvider {
|
||||||
|
|
||||||
@ -31,25 +32,16 @@ class LogSecretProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] parseEncryptedSecret(String secret) {
|
private static byte[] parseEncryptedSecret(String secret) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(secret);
|
||||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(secret);
|
return KeyStoreHelper.unseal(encryptedSecret);
|
||||||
return KeyStoreHelper.unseal(encryptedSecret);
|
|
||||||
} else {
|
|
||||||
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] createAndStoreSecret(@NonNull Context context) {
|
private static byte[] createAndStoreSecret(@NonNull Context context) {
|
||||||
SecureRandom random = new SecureRandom();
|
|
||||||
byte[] secret = new byte[32];
|
byte[] secret = new byte[32];
|
||||||
random.nextBytes(secret);
|
SECURE_RANDOM.nextBytes(secret);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(secret);
|
||||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(secret);
|
TextSecurePreferences.setLogEncryptedSecret(context, encryptedSecret.serialize());
|
||||||
TextSecurePreferences.setLogEncryptedSecret(context, encryptedSecret.serialize());
|
|
||||||
} else {
|
|
||||||
TextSecurePreferences.setLogUnencryptedSecret(context, Base64.encodeBytes(secret));
|
|
||||||
}
|
|
||||||
|
|
||||||
return secret;
|
return secret;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package org.thoughtcrime.securesms.media
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AttachmentHeader(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
){
|
||||||
|
Text(
|
||||||
|
modifier = modifier
|
||||||
|
.background(LocalColors.current.background)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
horizontal = LocalDimensions.current.smallSpacing,
|
||||||
|
vertical = LocalDimensions.current.xsSpacing
|
||||||
|
),
|
||||||
|
text = text,
|
||||||
|
style = LocalType.current.xl,
|
||||||
|
color = LocalColors.current.text
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
package org.thoughtcrime.securesms.media
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.Util
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun DocumentsPage(
|
||||||
|
nestedScrollConnection: NestedScrollConnection,
|
||||||
|
content: TabContent?,
|
||||||
|
onItemClicked: (MediaOverviewItem) -> Unit,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
content == null -> {
|
||||||
|
// Loading
|
||||||
|
}
|
||||||
|
|
||||||
|
content.isEmpty() -> {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.media_overview_documents_fragment__no_documents_found),
|
||||||
|
style = LocalType.current.base,
|
||||||
|
color = LocalColors.current.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(nestedScrollConnection)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(2.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.xxsSpacing)
|
||||||
|
) {
|
||||||
|
for ((bucketTitle, files) in content) {
|
||||||
|
stickyHeader {
|
||||||
|
AttachmentHeader(text = bucketTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(files) { file ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onItemClicked(file) })
|
||||||
|
.padding(LocalDimensions.current.smallSpacing),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.xxsSpacing),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painterResource(R.drawable.ic_document_large_dark),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.xxxsSpacing)) {
|
||||||
|
Text(
|
||||||
|
text = file.fileName.orEmpty(),
|
||||||
|
style = LocalType.current.large,
|
||||||
|
color = LocalColors.current.text
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = Util.getPrettyFileSize(file.fileSize),
|
||||||
|
style = LocalType.current.small,
|
||||||
|
color = LocalColors.current.textSecondary,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = file.date,
|
||||||
|
style = LocalType.current.small,
|
||||||
|
color = LocalColors.current.textSecondary,
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package org.thoughtcrime.securesms.media
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.temporal.WeekFields
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data structure that describes a series of time points in the past. It's primarily
|
||||||
|
* used to bucket items into categories like "Today", "Yesterday", "This week", "This month", etc.
|
||||||
|
*
|
||||||
|
* Call [getBucketText] to get the appropriate string resource for a given time. If no bucket is
|
||||||
|
* appropriate, it will return null.
|
||||||
|
*/
|
||||||
|
class FixedTimeBuckets(
|
||||||
|
private val startOfToday: ZonedDateTime,
|
||||||
|
private val startOfYesterday: ZonedDateTime,
|
||||||
|
private val startOfThisWeek: ZonedDateTime,
|
||||||
|
private val startOfThisMonth: ZonedDateTime
|
||||||
|
) {
|
||||||
|
constructor(now: ZonedDateTime = ZonedDateTime.now()) : this(
|
||||||
|
startOfToday = now.toLocalDate().atStartOfDay(now.zone),
|
||||||
|
startOfYesterday = now.toLocalDate().minusDays(1).atStartOfDay(now.zone),
|
||||||
|
startOfThisWeek = now.toLocalDate()
|
||||||
|
.with(WeekFields.of(Locale.getDefault()).dayOfWeek(), 1)
|
||||||
|
.atStartOfDay(now.zone),
|
||||||
|
startOfThisMonth = now.toLocalDate().withDayOfMonth(1).atStartOfDay(now.zone)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the given time against the buckets and return the appropriate string resource the time
|
||||||
|
* bucket. If no bucket is appropriate, it will return null.
|
||||||
|
*/
|
||||||
|
@StringRes
|
||||||
|
fun getBucketText(time: ZonedDateTime): Int? {
|
||||||
|
return when {
|
||||||
|
time >= startOfToday -> R.string.BucketedThreadMedia_Today
|
||||||
|
time >= startOfYesterday -> R.string.BucketedThreadMedia_Yesterday
|
||||||
|
time >= startOfThisWeek -> R.string.BucketedThreadMedia_This_week
|
||||||
|
time >= startOfThisMonth -> R.string.BucketedThreadMedia_This_month
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package org.thoughtcrime.securesms.media
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.content.IntentCompat
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
|
import org.thoughtcrime.securesms.ui.setComposeContent
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class MediaOverviewActivity : PassphraseRequiredActionBarActivity() {
|
||||||
|
@Inject
|
||||||
|
lateinit var viewModelFactory: MediaOverviewViewModel.AssistedFactory
|
||||||
|
|
||||||
|
private val viewModel: MediaOverviewViewModel by viewModels {
|
||||||
|
viewModelFactory.create(IntentCompat.getParcelableExtra(intent, EXTRA_ADDRESS, Address::class.java)!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setComposeContent {
|
||||||
|
MediaOverviewScreen(viewModel, onClose = this::finish)
|
||||||
|
}
|
||||||
|
|
||||||
|
supportActionBar?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_ADDRESS = "address"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun createIntent(context: Context, address: Address): Intent {
|
||||||
|
return Intent(context, MediaOverviewActivity::class.java).apply {
|
||||||
|
putExtra(EXTRA_ADDRESS, address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,311 @@
|
|||||||
|
package org.thoughtcrime.securesms.media
|
||||||
|
|
||||||
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.ui.AlertDialog
|
||||||
|
import org.thoughtcrime.securesms.ui.DialogButtonModel
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||||
|
|
||||||
|
@OptIn(
|
||||||
|
ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun MediaOverviewScreen(
|
||||||
|
viewModel: MediaOverviewViewModel,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
) {
|
||||||
|
val selectedItems by viewModel.selectedItemIDs.collectAsState()
|
||||||
|
val selectionMode by viewModel.inSelectionMode.collectAsState()
|
||||||
|
val topAppBarState = rememberTopAppBarState()
|
||||||
|
var showingDeleteConfirmation by remember { mutableStateOf(false) }
|
||||||
|
var showingSaveAttachmentWarning by remember { mutableStateOf(false) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val requestStoragePermission =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
if (granted) {
|
||||||
|
viewModel.onSaveClicked()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In selection mode, the app bar should not be scrollable and should be pinned
|
||||||
|
val appBarScrollBehavior = if (selectionMode) {
|
||||||
|
TopAppBarDefaults.pinnedScrollBehavior(topAppBarState, canScroll = { false })
|
||||||
|
} else {
|
||||||
|
TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the top app bar offset (so that it shows up) when entering selection mode
|
||||||
|
LaunchedEffect(selectionMode) {
|
||||||
|
if (selectionMode) {
|
||||||
|
topAppBarState.heightOffset = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(onBack = viewModel::onBackClicked)
|
||||||
|
|
||||||
|
// Event handling
|
||||||
|
LaunchedEffect(viewModel.events) {
|
||||||
|
viewModel.events.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
MediaOverviewEvent.Close -> onClose()
|
||||||
|
is MediaOverviewEvent.NavigateToActivity -> {
|
||||||
|
try {
|
||||||
|
context.startActivity(event.intent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
R.string.ConversationItem_unable_to_open_media,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MediaOverviewEvent.ShowSaveAttachmentError -> {
|
||||||
|
val message = context.resources.getQuantityText(
|
||||||
|
R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card,
|
||||||
|
event.errorCount
|
||||||
|
)
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
is MediaOverviewEvent.ShowSaveAttachmentSuccess -> {
|
||||||
|
val message = if (event.directory.isNotBlank()) {
|
||||||
|
context.resources.getString(
|
||||||
|
R.string.SaveAttachmentTask_saved_to,
|
||||||
|
event.directory
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
context.resources.getString(R.string.SaveAttachmentTask_saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(appBarScrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
MediaOverviewTopAppBar(
|
||||||
|
selectionMode = selectionMode,
|
||||||
|
title = viewModel.title.collectAsState().value,
|
||||||
|
onBackClicked = viewModel::onBackClicked,
|
||||||
|
onSaveClicked = { showingSaveAttachmentWarning = true },
|
||||||
|
onDeleteClicked = { showingDeleteConfirmation = true },
|
||||||
|
onSelectAllClicked = viewModel::onSelectAllClicked,
|
||||||
|
numSelected = selectedItems.size,
|
||||||
|
appBarScrollBehavior = appBarScrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddings ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddings)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
val pagerState = rememberPagerState(pageCount = { MediaOverviewTab.entries.size })
|
||||||
|
val selectedTab by viewModel.selectedTab.collectAsState()
|
||||||
|
|
||||||
|
// Apply "selectedTab" view model state to pager
|
||||||
|
LaunchedEffect(selectedTab) {
|
||||||
|
pagerState.animateScrollToPage(selectedTab.ordinal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply "selectedTab" pager state to view model
|
||||||
|
LaunchedEffect(pagerState.currentPage) {
|
||||||
|
viewModel.onTabItemClicked(MediaOverviewTab.entries[pagerState.currentPage])
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionTabRow(
|
||||||
|
pagerState = pagerState,
|
||||||
|
titles = MediaOverviewTab.entries.map { it.titleResId }
|
||||||
|
)
|
||||||
|
|
||||||
|
val content = viewModel.mediaListState.collectAsState()
|
||||||
|
val canLongPress = viewModel.canLongPress.collectAsState().value
|
||||||
|
|
||||||
|
HorizontalPager(
|
||||||
|
pagerState,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) { index ->
|
||||||
|
when (MediaOverviewTab.entries[index]) {
|
||||||
|
MediaOverviewTab.Media -> {
|
||||||
|
val haptics = LocalHapticFeedback.current
|
||||||
|
|
||||||
|
MediaPage(
|
||||||
|
content = content.value?.mediaContent,
|
||||||
|
selectedItemIDs = selectedItems,
|
||||||
|
onItemClicked = viewModel::onItemClicked,
|
||||||
|
nestedScrollConnection = appBarScrollBehavior.nestedScrollConnection,
|
||||||
|
onItemLongClicked = if(canLongPress){{
|
||||||
|
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
viewModel.onItemLongClicked(it)
|
||||||
|
}} else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaOverviewTab.Documents -> DocumentsPage(
|
||||||
|
nestedScrollConnection = appBarScrollBehavior.nestedScrollConnection,
|
||||||
|
content = content.value?.documentContent,
|
||||||
|
onItemClicked = viewModel::onItemClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showingDeleteConfirmation) {
|
||||||
|
DeleteConfirmationDialog(
|
||||||
|
onDismissRequest = { showingDeleteConfirmation = false },
|
||||||
|
onAccepted = viewModel::onDeleteClicked,
|
||||||
|
numSelected = selectedItems.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showingSaveAttachmentWarning) {
|
||||||
|
SaveAttachmentWarningDialog(
|
||||||
|
onDismissRequest = { showingSaveAttachmentWarning = false },
|
||||||
|
onAccepted = {
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
|
requestStoragePermission.launch(WRITE_EXTERNAL_STORAGE)
|
||||||
|
} else {
|
||||||
|
viewModel.onSaveClicked()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
numSelected = selectedItems.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val showingActionDialog = viewModel.showingActionProgress.collectAsState().value
|
||||||
|
if (showingActionDialog != null) {
|
||||||
|
ActionProgressDialog(showingActionDialog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SaveAttachmentWarningDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onAccepted: () -> Unit,
|
||||||
|
numSelected: Int,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = context.getString(R.string.ConversationFragment_save_to_sd_card),
|
||||||
|
text = context.resources.getQuantityString(
|
||||||
|
R.plurals.ConversationFragment_saving_n_media_to_storage_warning,
|
||||||
|
numSelected,
|
||||||
|
numSelected
|
||||||
|
),
|
||||||
|
buttons = listOf(
|
||||||
|
DialogButtonModel(GetString(R.string.save), onClick = onAccepted),
|
||||||
|
DialogButtonModel(GetString(android.R.string.cancel), dismissOnClick = true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DeleteConfirmationDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onAccepted: () -> Unit,
|
||||||
|
numSelected: Int,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = context.resources.getQuantityString(
|
||||||
|
R.plurals.ConversationFragment_delete_selected_messages, numSelected
|
||||||
|
),
|
||||||
|
text = context.resources.getQuantityString(
|
||||||
|
R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages,
|
||||||
|
numSelected,
|
||||||
|
numSelected,
|
||||||
|
),
|
||||||
|
buttons = listOf(
|
||||||
|
DialogButtonModel(GetString(R.string.delete), color = LocalColors.current.danger, onClick = onAccepted),
|
||||||
|
DialogButtonModel(GetString(android.R.string.cancel), dismissOnClick = true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
private fun ActionProgressDialog(
|
||||||
|
text: String
|
||||||
|
) {
|
||||||
|
BasicAlertDialog(
|
||||||
|
onDismissRequest = {},
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(LocalColors.current.background, shape = MaterialTheme.shapes.medium)
|
||||||
|
.padding(LocalDimensions.current.mediumSpacing),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(color = LocalColors.current.primary)
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style = LocalType.current.large,
|
||||||
|
color = LocalColors.current.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val MediaOverviewTab.titleResId: Int
|
||||||
|
get() = when (this) {
|
||||||
|
MediaOverviewTab.Media -> R.string.MediaOverviewActivity_Media
|
||||||
|
MediaOverviewTab.Documents -> R.string.MediaOverviewActivity_Documents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
|||||||
|
package org.thoughtcrime.securesms.media
|
||||||
|
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.ui.components.ActionAppBar
|
||||||
|
import org.thoughtcrime.securesms.ui.components.AppBarBackIcon
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun MediaOverviewTopAppBar(
|
||||||
|
selectionMode: Boolean,
|
||||||
|
numSelected: Int,
|
||||||
|
title: String,
|
||||||
|
onBackClicked: () -> Unit,
|
||||||
|
onSaveClicked: () -> Unit,
|
||||||
|
onDeleteClicked: () -> Unit,
|
||||||
|
onSelectAllClicked: () -> Unit,
|
||||||
|
appBarScrollBehavior: TopAppBarScrollBehavior
|
||||||
|
) {
|
||||||
|
ActionAppBar(
|
||||||
|
title = title,
|
||||||
|
actionModeTitle = numSelected.toString(),
|
||||||
|
navigationIcon = { AppBarBackIcon(onBack = onBackClicked) },
|
||||||
|
scrollBehavior = appBarScrollBehavior,
|
||||||
|
actionMode = selectionMode,
|
||||||
|
actionModeActions = {
|
||||||
|
IconButton(onClick = onSaveClicked) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_baseline_save_24),
|
||||||
|
contentDescription = stringResource(R.string.save),
|
||||||
|
tint = LocalColors.current.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = onDeleteClicked) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_baseline_delete_24),
|
||||||
|
contentDescription = stringResource(R.string.delete),
|
||||||
|
tint = LocalColors.current.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = onSelectAllClicked) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_baseline_select_all_24),
|
||||||
|
contentDescription = stringResource(R.string.MediaOverviewActivity_Select_all),
|
||||||
|
tint = LocalColors.current.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,431 @@
|
|||||||
|
package org.thoughtcrime.securesms.media
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.shareIn
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.messaging.messages.control.DataExtractionNotification
|
||||||
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
|
import org.session.libsession.snode.SnodeAPI
|
||||||
|
import org.session.libsession.utilities.Address
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||||
|
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
|
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
|
import org.thoughtcrime.securesms.util.AttachmentUtil
|
||||||
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||||
|
import org.thoughtcrime.securesms.util.asSequence
|
||||||
|
import org.thoughtcrime.securesms.util.observeChanges
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class MediaOverviewViewModel(
|
||||||
|
private val address: Address,
|
||||||
|
private val application: Application,
|
||||||
|
private val threadDatabase: ThreadDatabase,
|
||||||
|
private val mediaDatabase: MediaDatabase
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
|
private val timeBuckets by lazy { FixedTimeBuckets() }
|
||||||
|
private val monthTimeBucketFormatter =
|
||||||
|
DateTimeFormatter.ofPattern("MMMM yyyy", Locale.getDefault())
|
||||||
|
|
||||||
|
private val recipient: SharedFlow<Recipient> = application.contentResolver
|
||||||
|
.observeChanges(DatabaseContentProviders.Attachment.CONTENT_URI)
|
||||||
|
.onStart { emit(DatabaseContentProviders.Attachment.CONTENT_URI) }
|
||||||
|
.map { Recipient.from(application, address, false) }
|
||||||
|
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||||
|
|
||||||
|
val title: StateFlow<String> = recipient
|
||||||
|
.map { it.toShortString() }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, "")
|
||||||
|
|
||||||
|
val mediaListState: StateFlow<MediaOverviewContent?> = recipient
|
||||||
|
.map { recipient ->
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val threadId = threadDatabase.getOrCreateThreadIdFor(recipient)
|
||||||
|
val mediaItems = mediaDatabase.getGalleryMediaForThread(threadId)
|
||||||
|
.use { cursor ->
|
||||||
|
cursor.asSequence()
|
||||||
|
.map { MediaRecord.from(application, it) }
|
||||||
|
.groupRecordsByTimeBuckets()
|
||||||
|
}
|
||||||
|
|
||||||
|
val documentItems = mediaDatabase.getDocumentMediaForThread(threadId)
|
||||||
|
.use { cursor ->
|
||||||
|
cursor.asSequence()
|
||||||
|
.map { MediaRecord.from(application, it) }
|
||||||
|
.groupRecordsByRelativeTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaOverviewContent(
|
||||||
|
mediaContent = mediaItems,
|
||||||
|
documentContent = documentItems,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
|
private val mutableSelectedItemIDs = MutableStateFlow(emptySet<Long>())
|
||||||
|
val selectedItemIDs: StateFlow<Set<Long>> get() = mutableSelectedItemIDs
|
||||||
|
|
||||||
|
val inSelectionMode: StateFlow<Boolean> = selectedItemIDs
|
||||||
|
.map { it.isNotEmpty() }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, mutableSelectedItemIDs.value.isNotEmpty())
|
||||||
|
|
||||||
|
val canLongPress: StateFlow<Boolean> = inSelectionMode
|
||||||
|
.map { !it }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, true)
|
||||||
|
|
||||||
|
private val mutableEvents = MutableSharedFlow<MediaOverviewEvent>()
|
||||||
|
val events get() = mutableEvents
|
||||||
|
|
||||||
|
private val mutableSelectedTab = MutableStateFlow(MediaOverviewTab.Media)
|
||||||
|
val selectedTab: StateFlow<MediaOverviewTab> get() = mutableSelectedTab
|
||||||
|
|
||||||
|
private val mutableShowingActionProgress = MutableStateFlow<String?>(null)
|
||||||
|
val showingActionProgress: StateFlow<String?> get() = mutableShowingActionProgress
|
||||||
|
|
||||||
|
private val selectedMedia: Sequence<MediaOverviewItem>
|
||||||
|
get() {
|
||||||
|
val selected = selectedItemIDs.value
|
||||||
|
return mediaListState.value
|
||||||
|
?.mediaContent
|
||||||
|
?.asSequence()
|
||||||
|
.orEmpty()
|
||||||
|
.flatMap { it.second.asSequence() }
|
||||||
|
.filter { it.id in selected }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Sequence<MediaRecord>.groupRecordsByTimeBuckets(): List<Pair<BucketTitle, List<MediaOverviewItem>>> {
|
||||||
|
return this
|
||||||
|
.groupBy { record ->
|
||||||
|
val time =
|
||||||
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(record.date), ZoneId.of("UTC"))
|
||||||
|
timeBuckets.getBucketText(time)?.let(application::getString)
|
||||||
|
?: time.toLocalDate().withDayOfMonth(1)
|
||||||
|
}
|
||||||
|
.map { (bucket, records) ->
|
||||||
|
val bucketTitle = when (bucket) {
|
||||||
|
is String -> bucket
|
||||||
|
is LocalDate -> bucket.format(monthTimeBucketFormatter)
|
||||||
|
else -> error("Invalid bucket type: $bucket")
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketTitle to records.map { record ->
|
||||||
|
MediaOverviewItem(
|
||||||
|
id = record.attachment.attachmentId.rowId,
|
||||||
|
slide = MediaUtil.getSlideForAttachment(application, record.attachment),
|
||||||
|
mediaRecord = record,
|
||||||
|
date = bucketTitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Sequence<MediaRecord>.groupRecordsByRelativeTime(): List<Pair<BucketTitle, List<MediaOverviewItem>>> {
|
||||||
|
return this
|
||||||
|
.groupBy { record ->
|
||||||
|
DateUtils.getRelativeDate(application, Locale.getDefault(), record.date)
|
||||||
|
}
|
||||||
|
.map { (bucket, records) ->
|
||||||
|
bucket to records.map { record ->
|
||||||
|
MediaOverviewItem(
|
||||||
|
id = record.attachment.attachmentId.rowId,
|
||||||
|
slide = MediaUtil.getSlideForAttachment(application, record.attachment),
|
||||||
|
mediaRecord = record,
|
||||||
|
date = bucket
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun onItemClicked(item: MediaOverviewItem) {
|
||||||
|
if (inSelectionMode.value) {
|
||||||
|
val newSet = mutableSelectedItemIDs.value.toMutableSet()
|
||||||
|
if (item.id in newSet) {
|
||||||
|
newSet.remove(item.id)
|
||||||
|
} else {
|
||||||
|
newSet.add(item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableSelectedItemIDs.value = newSet
|
||||||
|
} else if (!item.slide.hasDocument()) {
|
||||||
|
val mediaRecord = item.mediaRecord
|
||||||
|
|
||||||
|
// The item clicked is a media item, so we should open the media viewer
|
||||||
|
val intent = Intent(application, MediaPreviewActivity::class.java)
|
||||||
|
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.date)
|
||||||
|
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.attachment.size)
|
||||||
|
intent.putExtra(MediaPreviewActivity.ADDRESS_EXTRA, address)
|
||||||
|
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, mediaRecord.isOutgoing)
|
||||||
|
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, true)
|
||||||
|
|
||||||
|
intent.setDataAndType(
|
||||||
|
mediaRecord.attachment.dataUri,
|
||||||
|
mediaRecord.contentType
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
mutableEvents.emit(MediaOverviewEvent.NavigateToActivity(intent))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
intent.setDataAndType(
|
||||||
|
PartAuthority.getAttachmentPublicUri(item.slide.uri),
|
||||||
|
item.slide.contentType
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
mutableEvents.emit(MediaOverviewEvent.NavigateToActivity(intent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTabItemClicked(tab: MediaOverviewTab) {
|
||||||
|
if (inSelectionMode.value) {
|
||||||
|
// Not allowing to switch tabs while in selection mode
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableSelectedTab.value = tab
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onItemLongClicked(id: Long) {
|
||||||
|
mutableSelectedItemIDs.value = setOf(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSaveClicked() {
|
||||||
|
if (!inSelectionMode.value) {
|
||||||
|
// Not in selection mode, so we should not be able to save
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
val selectedMedia = selectedMedia.toList()
|
||||||
|
|
||||||
|
mutableShowingActionProgress.value = application.resources.getQuantityString(
|
||||||
|
R.plurals.ConversationFragment_saving_n_attachments,
|
||||||
|
selectedMedia.size,
|
||||||
|
selectedMedia.size,
|
||||||
|
)
|
||||||
|
|
||||||
|
val attachments = selectedMedia
|
||||||
|
.asSequence()
|
||||||
|
.mapNotNull {
|
||||||
|
val uri = it.mediaRecord.attachment.dataUri ?: return@mapNotNull null
|
||||||
|
SaveAttachmentTask.Attachment(
|
||||||
|
uri = uri,
|
||||||
|
contentType = it.mediaRecord.contentType,
|
||||||
|
date = it.mediaRecord.date,
|
||||||
|
fileName = it.mediaRecord.attachment.fileName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedDirectory: String? = null
|
||||||
|
var successCount = 0
|
||||||
|
var errorCount = 0
|
||||||
|
|
||||||
|
for (attachment in attachments) {
|
||||||
|
val directory = withContext(Dispatchers.Default) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
SaveAttachmentTask.saveAttachment(application, attachment)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directory == null) {
|
||||||
|
errorCount += 1
|
||||||
|
} else {
|
||||||
|
savedDirectory = directory
|
||||||
|
successCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
mutableEvents.emit(MediaOverviewEvent.ShowSaveAttachmentSuccess(
|
||||||
|
savedDirectory.orEmpty(),
|
||||||
|
successCount
|
||||||
|
))
|
||||||
|
} else if (errorCount > 0) {
|
||||||
|
mutableEvents.emit(MediaOverviewEvent.ShowSaveAttachmentError(errorCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a notification of attachment saved if we are in a 1to1 chat and the
|
||||||
|
// attachments saved are from the other party (a.k.a let other person know
|
||||||
|
// that you saved their attachments, but don't need to let the whole world know as
|
||||||
|
// in groups/communities)
|
||||||
|
if (selectedMedia.any { !it.mediaRecord.isOutgoing } &&
|
||||||
|
successCount > 0 &&
|
||||||
|
!address.isGroup) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val timestamp = SnodeAPI.nowWithOffset
|
||||||
|
val kind = DataExtractionNotification.Kind.MediaSaved(timestamp)
|
||||||
|
val message = DataExtractionNotification(kind)
|
||||||
|
MessageSender.send(message, address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableShowingActionProgress.value = null
|
||||||
|
mutableSelectedItemIDs.value = emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDeleteClicked() {
|
||||||
|
if (!inSelectionMode.value) {
|
||||||
|
// Not in selection mode, so we should not be able to delete
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
mutableShowingActionProgress.value = application.getString(R.string.MediaOverviewActivity_Media_delete_progress_message)
|
||||||
|
|
||||||
|
// Delete the selected media items, and retrieve the thread ID for the address if any
|
||||||
|
val threadId = withContext(Dispatchers.Default) {
|
||||||
|
for (media in selectedMedia) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
AttachmentUtil.deleteAttachment(application, media.mediaRecord.attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
threadDatabase.getThreadIdIfExistsFor(address.serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the content provider that the thread has been updated
|
||||||
|
if (threadId >= 0) {
|
||||||
|
application.contentResolver.notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadId), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableShowingActionProgress.value = null
|
||||||
|
mutableSelectedItemIDs.value = emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSelectAllClicked() {
|
||||||
|
if (!inSelectionMode.value) {
|
||||||
|
// Not in selection mode, so we should not be able to select all
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val allItems = mediaListState.value?.let { content ->
|
||||||
|
when (selectedTab.value) {
|
||||||
|
MediaOverviewTab.Media -> content.mediaContent
|
||||||
|
MediaOverviewTab.Documents -> content.documentContent
|
||||||
|
}
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
mutableSelectedItemIDs.value = allItems
|
||||||
|
.asSequence()
|
||||||
|
.flatMap { it.second }
|
||||||
|
.mapTo(hashSetOf()) { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBackClicked() {
|
||||||
|
if (inSelectionMode.value) {
|
||||||
|
// Clear selection mode by clear selecting items
|
||||||
|
mutableSelectedItemIDs.value = emptySet()
|
||||||
|
} else {
|
||||||
|
viewModelScope.launch {
|
||||||
|
mutableEvents.emit(MediaOverviewEvent.Close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@dagger.assisted.AssistedFactory
|
||||||
|
interface AssistedFactory {
|
||||||
|
fun create(address: Address): Factory
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory @AssistedInject constructor(
|
||||||
|
@Assisted private val address: Address,
|
||||||
|
private val application: Application,
|
||||||
|
private val threadDatabase: ThreadDatabase,
|
||||||
|
private val mediaDatabase: MediaDatabase
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = MediaOverviewViewModel(
|
||||||
|
address,
|
||||||
|
application,
|
||||||
|
threadDatabase,
|
||||||
|
mediaDatabase
|
||||||
|
) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class MediaOverviewTab {
|
||||||
|
Media,
|
||||||
|
Documents,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface MediaOverviewEvent {
|
||||||
|
data object Close : MediaOverviewEvent
|
||||||
|
data class ShowSaveAttachmentError(val errorCount: Int) : MediaOverviewEvent
|
||||||
|
data class ShowSaveAttachmentSuccess(val directory: String, val successCount: Int) : MediaOverviewEvent
|
||||||
|
data class NavigateToActivity(val intent: Intent) : MediaOverviewEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias BucketTitle = String
|
||||||
|
typealias TabContent = List<Pair<BucketTitle, List<MediaOverviewItem>>>
|
||||||
|
|
||||||
|
data class MediaOverviewContent(
|
||||||
|
val mediaContent: TabContent,
|
||||||
|
val documentContent: TabContent
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MediaOverviewItem(
|
||||||
|
val id: Long,
|
||||||
|
val slide: Slide,
|
||||||
|
val date: String,
|
||||||
|
val mediaRecord: MediaRecord,
|
||||||
|
) {
|
||||||
|
val showPlayOverlay: Boolean
|
||||||
|
get() = slide.hasPlayOverlay()
|
||||||
|
|
||||||
|
val thumbnailUri: Uri?
|
||||||
|
get() = slide.thumbnailUri
|
||||||
|
|
||||||
|
val hasPlaceholder: Boolean
|
||||||
|
get() = slide.hasPlaceholder()
|
||||||
|
|
||||||
|
val fileName: String?
|
||||||
|
get() = slide.fileName.orNull()
|
||||||
|
|
||||||
|
val fileSize: Long
|
||||||
|
get() = slide.fileSize
|
||||||
|
|
||||||
|
fun placeholder(context: Context): Int {
|
||||||
|
return slide.getPlaceholderRes(context.theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
225
app/src/main/java/org/thoughtcrime/securesms/media/MediaPage.kt
Normal file
225
app/src/main/java/org/thoughtcrime/securesms/media/MediaPage.kt
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
package org.thoughtcrime.securesms.media
|
||||||
|
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bumptech.glide.integration.compose.CrossFade
|
||||||
|
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||||
|
import com.bumptech.glide.integration.compose.GlideImage
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||||
|
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
private val MEDIA_SPACING = 2.dp
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun MediaPage(
|
||||||
|
nestedScrollConnection: NestedScrollConnection,
|
||||||
|
content: TabContent?,
|
||||||
|
selectedItemIDs: Set<Long>,
|
||||||
|
onItemClicked: (MediaOverviewItem) -> Unit,
|
||||||
|
onItemLongClicked: ((Long) -> Unit)?,
|
||||||
|
) {
|
||||||
|
val columnCount = LocalContext.current.resources.getInteger(R.integer.media_overview_cols)
|
||||||
|
|
||||||
|
Crossfade(content, label = "Media content animation") { state ->
|
||||||
|
when {
|
||||||
|
state == null -> {
|
||||||
|
// Loading state
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isEmpty() -> {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.media_overview_activity__no_media),
|
||||||
|
style = LocalType.current.base,
|
||||||
|
color = LocalColors.current.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(nestedScrollConnection)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(MEDIA_SPACING),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MEDIA_SPACING)
|
||||||
|
) {
|
||||||
|
for ((header, thumbnails) in state) {
|
||||||
|
stickyHeader {
|
||||||
|
AttachmentHeader(text = header)
|
||||||
|
}
|
||||||
|
|
||||||
|
val numRows = ceil(thumbnails.size / columnCount.toFloat()).toInt()
|
||||||
|
|
||||||
|
// Row of thumbnails
|
||||||
|
items(numRows) { rowIndex ->
|
||||||
|
ThumbnailRow(
|
||||||
|
columnCount = columnCount,
|
||||||
|
thumbnails = thumbnails,
|
||||||
|
rowIndex = rowIndex,
|
||||||
|
onItemClicked = onItemClicked,
|
||||||
|
onItemLongClicked = onItemLongClicked,
|
||||||
|
selectedItemIDs = selectedItemIDs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalGlideComposeApi::class, ExperimentalFoundationApi::class)
|
||||||
|
private fun ThumbnailRow(
|
||||||
|
columnCount: Int,
|
||||||
|
thumbnails: List<MediaOverviewItem>,
|
||||||
|
rowIndex: Int,
|
||||||
|
onItemClicked: (MediaOverviewItem) -> Unit,
|
||||||
|
onItemLongClicked: ((Long) -> Unit)?,
|
||||||
|
selectedItemIDs: Set<Long>
|
||||||
|
) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(MEDIA_SPACING)) {
|
||||||
|
repeat(columnCount) { columnIndex ->
|
||||||
|
val item = thumbnails.getOrNull(rowIndex * columnCount + columnIndex)
|
||||||
|
if (item != null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.let {
|
||||||
|
when {
|
||||||
|
onItemLongClicked != null -> {
|
||||||
|
it.combinedClickable(
|
||||||
|
onClick = { onItemClicked(item) },
|
||||||
|
onLongClick = { onItemLongClicked(item.id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
it.clickable { onItemClicked(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
val uri = item.thumbnailUri
|
||||||
|
|
||||||
|
if (uri != null) {
|
||||||
|
GlideImage(
|
||||||
|
DecryptableStreamUriLoader.DecryptableUri(uri),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
contentDescription = null,
|
||||||
|
transition = CrossFade,
|
||||||
|
) {
|
||||||
|
it.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The resource given by the placeholder needs tinting according to our theme.
|
||||||
|
// But the missing thumbnail picture does not.
|
||||||
|
var (placeholder, shouldTint) = if (item.hasPlaceholder) {
|
||||||
|
item.placeholder(LocalContext.current) to true
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_missing_thumbnail_picture to false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeholder == 0) {
|
||||||
|
placeholder = R.drawable.ic_missing_thumbnail_picture
|
||||||
|
shouldTint = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(placeholder),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Inside,
|
||||||
|
colorFilter = if (shouldTint) {
|
||||||
|
ColorFilter.tint(LocalColors.current.textSecondary)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
item.showPlayOverlay -> {
|
||||||
|
// The code below is translated from thumbnail_view.xml:
|
||||||
|
// Trying to show a green play button on a white background.
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(Color.White, shape = CircleShape),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.padding(start = LocalDimensions.current.xxxsSpacing),
|
||||||
|
painter = painterResource(R.drawable.triangle_right),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(LocalColors.current.primary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Crossfade(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
targetState = item.id in selectedItemIDs,
|
||||||
|
label = "Showing selected state"
|
||||||
|
) { selected ->
|
||||||
|
if (selected) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.4f)),
|
||||||
|
contentScale = ContentScale.Inside,
|
||||||
|
painter = painterResource(R.drawable.ic_check_white_48dp),
|
||||||
|
contentDescription = stringResource(R.string.AccessibilityId_select),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@ import network.loki.messenger.R;
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
import org.thoughtcrime.securesms.mediasend.Media;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import com.bumptech.glide.RequestManager;
|
||||||
import org.thoughtcrime.securesms.util.StableIdGenerator;
|
import org.thoughtcrime.securesms.util.StableIdGenerator;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -25,7 +25,7 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
|
|||||||
private static final int TYPE_MEDIA = 1;
|
private static final int TYPE_MEDIA = 1;
|
||||||
private static final int TYPE_BUTTON = 2;
|
private static final int TYPE_BUTTON = 2;
|
||||||
|
|
||||||
private final GlideRequests glideRequests;
|
private final RequestManager glideRequests;
|
||||||
private final List<Media> media;
|
private final List<Media> media;
|
||||||
private final RailItemListener listener;
|
private final RailItemListener listener;
|
||||||
private final boolean editable;
|
private final boolean editable;
|
||||||
@ -34,7 +34,7 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
|
|||||||
private RailItemAddListener addListener;
|
private RailItemAddListener addListener;
|
||||||
private int activePosition;
|
private int activePosition;
|
||||||
|
|
||||||
public MediaRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemListener listener, boolean editable) {
|
public MediaRailAdapter(@NonNull RequestManager glideRequests, @NonNull RailItemListener listener, boolean editable) {
|
||||||
this.glideRequests = glideRequests;
|
this.glideRequests = glideRequests;
|
||||||
this.media = new ArrayList<>();
|
this.media = new ArrayList<>();
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
@ -148,7 +148,7 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
|
|||||||
captionIndicator = itemView.findViewById(R.id.rail_item_caption);
|
captionIndicator = itemView.findViewById(R.id.rail_item_caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
void bind(@NonNull Media media, boolean isActive, @NonNull GlideRequests glideRequests,
|
void bind(@NonNull Media media, boolean isActive, @NonNull RequestManager glideRequests,
|
||||||
@NonNull RailItemListener railItemListener, int distanceFromActive, boolean editable)
|
@NonNull RailItemListener railItemListener, int distanceFromActive, boolean editable)
|
||||||
{
|
{
|
||||||
image.setImageResource(glideRequests, media.getUri());
|
image.setImageResource(glideRequests, media.getUri());
|
||||||
|
@ -36,7 +36,7 @@ import com.bumptech.glide.request.transition.Transition;
|
|||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import com.bumptech.glide.Glide;
|
||||||
import org.session.libsession.utilities.ServiceUtil;
|
import org.session.libsession.utilities.ServiceUtil;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
@ -236,7 +236,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
|
|||||||
Transformation<Bitmap> transformation = frontFacing ? new MultiTransformation<>(new CenterCrop(), new FlipTransformation())
|
Transformation<Bitmap> transformation = frontFacing ? new MultiTransformation<>(new CenterCrop(), new FlipTransformation())
|
||||||
: new CenterCrop();
|
: new CenterCrop();
|
||||||
|
|
||||||
GlideApp.with(this)
|
Glide.with(this)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(jpegData)
|
.load(jpegData)
|
||||||
.transform(transformation)
|
.transform(transformation)
|
||||||
|
@ -14,18 +14,18 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
|||||||
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import com.bumptech.glide.RequestManager;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
class MediaPickerFolderAdapter extends RecyclerView.Adapter<MediaPickerFolderAdapter.FolderViewHolder> {
|
class MediaPickerFolderAdapter extends RecyclerView.Adapter<MediaPickerFolderAdapter.FolderViewHolder> {
|
||||||
|
|
||||||
private final GlideRequests glideRequests;
|
private final RequestManager glideRequests;
|
||||||
private final EventListener eventListener;
|
private final EventListener eventListener;
|
||||||
private final List<MediaFolder> folders;
|
private final List<MediaFolder> folders;
|
||||||
|
|
||||||
MediaPickerFolderAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
MediaPickerFolderAdapter(@NonNull RequestManager glideRequests, @NonNull EventListener eventListener) {
|
||||||
this.glideRequests = glideRequests;
|
this.glideRequests = glideRequests;
|
||||||
this.eventListener = eventListener;
|
this.eventListener = eventListener;
|
||||||
this.folders = new ArrayList<>();
|
this.folders = new ArrayList<>();
|
||||||
@ -74,7 +74,7 @@ class MediaPickerFolderAdapter extends RecyclerView.Adapter<MediaPickerFolderAda
|
|||||||
count = itemView.findViewById(R.id.mediapicker_folder_item_count);
|
count = itemView.findViewById(R.id.mediapicker_folder_item_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
void bind(@NonNull MediaFolder folder, @NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
void bind(@NonNull MediaFolder folder, @NonNull RequestManager glideRequests, @NonNull EventListener eventListener) {
|
||||||
title.setText(folder.getTitle());
|
title.setText(folder.getTitle());
|
||||||
count.setText(String.valueOf(folder.getItemCount()));
|
count.setText(String.valueOf(folder.getItemCount()));
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import com.bumptech.glide.Glide;
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
import org.session.libsignal.utilities.guava.Optional;
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
|
|||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
RecyclerView list = view.findViewById(R.id.mediapicker_folder_list);
|
RecyclerView list = view.findViewById(R.id.mediapicker_folder_list);
|
||||||
MediaPickerFolderAdapter adapter = new MediaPickerFolderAdapter(GlideApp.with(this), this);
|
MediaPickerFolderAdapter adapter = new MediaPickerFolderAdapter(Glide.with(this), this);
|
||||||
|
|
||||||
layoutManager = new GridLayoutManager(requireContext(), 2);
|
layoutManager = new GridLayoutManager(requireContext(), 2);
|
||||||
onScreenWidthChanged(getScreenWidth());
|
onScreenWidthChanged(getScreenWidth());
|
||||||
|
@ -12,7 +12,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import com.bumptech.glide.RequestManager;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.StableIdGenerator;
|
import org.thoughtcrime.securesms.util.StableIdGenerator;
|
||||||
@ -24,7 +24,7 @@ import java.util.List;
|
|||||||
|
|
||||||
public class MediaPickerItemAdapter extends RecyclerView.Adapter<MediaPickerItemAdapter.ItemViewHolder> {
|
public class MediaPickerItemAdapter extends RecyclerView.Adapter<MediaPickerItemAdapter.ItemViewHolder> {
|
||||||
|
|
||||||
private final GlideRequests glideRequests;
|
private final RequestManager glideRequests;
|
||||||
private final EventListener eventListener;
|
private final EventListener eventListener;
|
||||||
private final List<Media> media;
|
private final List<Media> media;
|
||||||
private final List<Media> selected;
|
private final List<Media> selected;
|
||||||
@ -33,7 +33,7 @@ public class MediaPickerItemAdapter extends RecyclerView.Adapter<MediaPickerItem
|
|||||||
|
|
||||||
private boolean forcedMultiSelect;
|
private boolean forcedMultiSelect;
|
||||||
|
|
||||||
public MediaPickerItemAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, int maxSelection) {
|
public MediaPickerItemAdapter(@NonNull RequestManager glideRequests, @NonNull EventListener eventListener, int maxSelection) {
|
||||||
this.glideRequests = glideRequests;
|
this.glideRequests = glideRequests;
|
||||||
this.eventListener = eventListener;
|
this.eventListener = eventListener;
|
||||||
this.media = new ArrayList<>();
|
this.media = new ArrayList<>();
|
||||||
@ -109,7 +109,7 @@ public class MediaPickerItemAdapter extends RecyclerView.Adapter<MediaPickerItem
|
|||||||
selectOrder = itemView.findViewById(R.id.mediapicker_select_order);
|
selectOrder = itemView.findViewById(R.id.mediapicker_select_order);
|
||||||
}
|
}
|
||||||
|
|
||||||
void bind(@NonNull Media media, boolean multiSelect, List<Media> selected, int maxSelection, @NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
void bind(@NonNull Media media, boolean multiSelect, List<Media> selected, int maxSelection, @NonNull RequestManager glideRequests, @NonNull EventListener eventListener) {
|
||||||
glideRequests.load(media.getUri())
|
glideRequests.load(media.getUri())
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.transition(DrawableTransitionOptions.withCrossFade())
|
.transition(DrawableTransitionOptions.withCrossFade())
|
||||||
|
@ -21,7 +21,7 @@ import android.view.ViewGroup;
|
|||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import com.bumptech.glide.Glide;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -91,7 +91,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
|||||||
|
|
||||||
RecyclerView imageList = view.findViewById(R.id.mediapicker_item_list);
|
RecyclerView imageList = view.findViewById(R.id.mediapicker_item_list);
|
||||||
|
|
||||||
adapter = new MediaPickerItemAdapter(GlideApp.with(this), this, maxSelection);
|
adapter = new MediaPickerItemAdapter(Glide.with(this), this, maxSelection);
|
||||||
layoutManager = new GridLayoutManager(requireContext(), 4);
|
layoutManager = new GridLayoutManager(requireContext(), 4);
|
||||||
|
|
||||||
imageList.setLayoutManager(layoutManager);
|
imageList.setLayoutManager(layoutManager);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend;
|
package org.thoughtcrime.securesms.mediasend;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -200,14 +199,12 @@ class MediaRepository {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(16)
|
|
||||||
@SuppressWarnings("SuspiciousNameCombination")
|
@SuppressWarnings("SuspiciousNameCombination")
|
||||||
private String getWidthColumn(int orientation) {
|
private String getWidthColumn(int orientation) {
|
||||||
if (orientation == 0 || orientation == 180) return Images.Media.WIDTH;
|
if (orientation == 0 || orientation == 180) return Images.Media.WIDTH;
|
||||||
else return Images.Media.HEIGHT;
|
else return Images.Media.HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(16)
|
|
||||||
@SuppressWarnings("SuspiciousNameCombination")
|
@SuppressWarnings("SuspiciousNameCombination")
|
||||||
private String getHeightColumn(int orientation) {
|
private String getHeightColumn(int orientation) {
|
||||||
if (orientation == 0 || orientation == 180) return Images.Media.HEIGHT;
|
if (orientation == 0 || orientation == 180) return Images.Media.HEIGHT;
|
||||||
|
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