diff --git a/.editorconfig b/.editorconfig index 75fa24ed2..fbc2f393c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -38,4 +38,11 @@ indent_size=2 [*.{kt,kts}] # IDE does not follow this Ktlint rule strictly, but the default ordering is pretty good anyway, so let's ditch it -ktlint_disabled_rules=import-ordering +ktlint_code_style = android_studio +insert_final_newline = true +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than=unset +ktlint_function_signature_body_expression_wrapping=multiline +ktlint_standard_import-ordering = disabled +ktlint_standard_wrapping = enabled +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index bc9cdb1b3..d08f4181a 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -38,7 +38,7 @@ jobs: repository: ${{ steps.get-vars.outputs.repo }} ref: ${{ steps.get-vars.outputs.branch }} - name: Set up JDK 17 - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 with: distribution: "temurin" java-version: 17 diff --git a/.github/workflows/assembleFlavors.yml b/.github/workflows/assembleFlavors.yml index c6a47155c..8a2845932 100644 --- a/.github/workflows/assembleFlavors.yml +++ b/.github/workflows/assembleFlavors.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: set up JDK 17 - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 with: distribution: "temurin" java-version: 17 diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1c175754b..74d157c97 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up JDK 17 - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 with: distribution: "temurin" java-version: 17 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5e607ef71..1c3fa4639 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -32,11 +32,11 @@ jobs: with: swap-size-gb: 10 - name: Initialize CodeQL - uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/init@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11 with: languages: ${{ matrix.language }} - name: Set up JDK 17 - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 with: distribution: "temurin" java-version: 17 @@ -46,4 +46,4 @@ jobs: echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties" ./gradlew assembleDebug - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/analyze@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11 diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 9f6ea5776..f4c3c0fc1 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 if: ${{ steps.check-secrets.outputs.ok == 'true' }} - name: set up JDK 17 - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 if: ${{ steps.check-secrets.outputs.ok == 'true' }} with: distribution: "temurin" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e6664f003..4fd99cd59 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -37,6 +37,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/upload-sarif@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11 with: sarif_file: results.sarif diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index d996914db..776b467f9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,7 +14,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: days-before-stale: 28 days-before-close: 14 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 142bdd0df..c31d97248 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -20,12 +20,12 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up JDK 17 - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 with: distribution: "temurin" java-version: 17 - name: Run unit tests with coverage - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0 + uses: gradle/gradle-build-action@87a9a15658c426a54dd469d4fc7dc1a73ca9d4a6 # v2.10.0 with: arguments: testGplayDebugUnit - name: Upload test artifacts diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb29055b..048389a5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Types of changes can be: Added/Changed/Deprecated/Removed/Fixed/Security +## [18.0.0] - 2023-12-11 + +### Added +- File captions +- Note To Self +- Recording consent +- Share files by long press context menu (@Smarshal21) +- Save files to storage (@FaribaKhandani) +- Show active call in chat with accept call buttons + +### Fixed +- Not possible to delete voice, video, image, contact and location messages (@Smarshal21) +- Hide "unread mention" bubble in search mode (@sowjanyakch) +- Call notification screen remains open +- Minor bug fixes (@parneet-guraya et al.) + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/75?closed=1 + +## [17.1.3] - 2023-11-17 + +### Fixed +- Login via Active Directory fails when using Umlauts in username +- Crash when guest without name joins a call +- Chat messages disappear on initial configuration change (e.g. screen rotation) + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/78?closed=1 + ## [17.1.2] - 2023-10-19 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 584d24df6..3bf77a53d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -289,7 +289,6 @@ We are using [Dagger 2](https://dagger.dev/) to inject dependencies into major A * `Activity` * `Fragment` - * `Controller` * `Service` * `BroadcastReceiver` * `ContentProvider` diff --git a/app/build.gradle b/app/build.gradle index c9b873e50..436075f3a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,7 +7,7 @@ * @author Tim Krüger * Copyright (C) 2021 Andy Scherzinger * Copyright (C) 2017-2019 Mario Danic - * Copyright (C) 2021 Marcel Hibbe + * Copyright (C) 2021-2023 Marcel Hibbe * Copyright (C) 2022 Tim Krüger * * This program is free software: you can redistribute it and/or modify @@ -24,6 +24,8 @@ * along with this program. If not, see . */ import com.github.spotbugs.snom.SpotBugsTask +import com.github.spotbugs.snom.Confidence +import com.github.spotbugs.snom.Effort apply plugin: 'com.android.application' apply plugin: 'kotlin-android' @@ -46,8 +48,8 @@ android { // mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable) // xx .xxx .xx .xx - versionCode 180000001 - versionName "18.0.0 Alpha 1" + versionCode 180010004 + versionName "18.1.0 Alpha 04" flavorDimensions "default" renderscriptTargetApi 19 @@ -127,6 +129,7 @@ android { buildFeatures { viewBinding true + buildConfig = true } lint { @@ -138,9 +141,9 @@ android { } ext { - androidxCameraVersion = "1.3.0" + androidxCameraVersion = "1.3.1" coilKtVersion = "2.5.0" - daggerVersion = "2.48.1" + daggerVersion = "2.49" emojiVersion = "1.4.0" lifecycleVersion = '2.6.2' okhttpVersion = "4.12.0" @@ -149,13 +152,13 @@ ext { parcelerVersion = "1.1.13" prismVersion = "2.0.0" retrofit2Version = "2.9.0" - roomVersion = "2.6.0" - workVersion = "2.8.1" + roomVersion = "2.6.1" + workVersion = "2.9.0" espressoVersion = "3.5.1" - media3_version = "1.1.1" + media3_version = "1.2.0" } -configurations.all { +configurations.configureEach { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -166,14 +169,14 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.datastore:datastore-core:1.0.0' implementation 'androidx.datastore:datastore-preferences:1.0.0' - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.3") + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.4") implementation fileTree(include: ['*'], dir: 'libs') - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.10.0' + implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.emoji2:emoji2:${emojiVersion}" implementation "androidx.emoji2:emoji2-bundled:${emojiVersion}" @@ -195,7 +198,7 @@ dependencies { implementation "androidx.camera:camera-camera2:${androidxCameraVersion}" implementation "androidx.camera:camera-lifecycle:${androidxCameraVersion}" implementation "androidx.camera:camera-view:${androidxCameraVersion}" - implementation "androidx.exifinterface:exifinterface:1.3.6" + implementation "androidx.exifinterface:exifinterface:1.3.7" implementation "androidx.lifecycle:lifecycle-runtime-ktx:${lifecycleVersion}" implementation "androidx.lifecycle:lifecycle-livedata-ktx:${lifecycleVersion}" @@ -210,8 +213,6 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation "io.reactivex.rxjava2:rxjava:2.2.21" - implementation 'com.bluelinelabs:conductor:3.2.0' - implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}" implementation "com.squareup.okhttp3:okhttp-urlconnection:${okhttpVersion}" implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}" @@ -241,7 +242,7 @@ dependencies { implementation "org.parceler:parceler-api:$parcelerVersion" implementation 'eu.davidea:flexible-adapter:5.1.0' implementation 'eu.davidea:flexible-adapter-ui:1.0.0' - implementation 'org.apache.commons:commons-lang3:3.13.0' + implementation 'org.apache.commons:commons-lang3:3.14.0' implementation 'com.github.wooplr:Spotlight:1.3' implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'com.github.nextcloud-deps:ChatKit:0.4.2' @@ -286,8 +287,8 @@ dependencies { implementation 'androidx.core:core-ktx:1.12.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:5.7.0' - androidTestImplementation 'org.mockito:mockito-android:5.7.0' + testImplementation 'org.mockito:mockito-core:5.8.0' + androidTestImplementation 'org.mockito:mockito-android:5.8.0' testImplementation 'androidx.arch.core:core-testing:2.2.0' androidTestImplementation "androidx.test:core:1.5.0" @@ -302,19 +303,19 @@ dependencies { androidTestImplementation('com.android.support.test.espresso:espresso-intents:3.0.2') spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.12.0' - spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.0' + spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.3' gplayImplementation 'com.google.android.gms:play-services-base:18.2.0' - gplayImplementation "com.google.firebase:firebase-messaging:23.3.1" + gplayImplementation "com.google.firebase:firebase-messaging:23.4.0" - implementation 'androidx.activity:activity-ktx:1.8.0' + implementation 'androidx.activity:activity-ktx:1.8.2' - implementation 'com.github.nextcloud.android-common:ui:0.12.0' + implementation 'com.github.nextcloud.android-common:ui:0.13.0' implementation 'com.github.nextcloud-deps:android-talk-webrtc:110.5481.0' } -task installGitHooks(type: Copy, group: "development") { +tasks.register('installGitHooks', Copy) { description = "Install git hooks" from("../scripts/hooks") { include '*' @@ -324,11 +325,11 @@ task installGitHooks(type: Copy, group: "development") { spotbugs { ignoreFailures = true // should continue checking - effort = "max" - reportLevel = "medium" + effort = Effort.MAX + reportLevel = Confidence.valueOf('MEDIUM') } -tasks.withType(SpotBugsTask) { task -> +tasks.withType(SpotBugsTask).configureEach { task -> String variantNameCap = task.name.replace("spotbugs", "") String variantName = variantNameCap.substring(0, 1).toLowerCase() + variantNameCap.substring(1) @@ -359,6 +360,6 @@ tasks.named("detekt").configure { } detekt { - config = files("../detekt.yml") - input = files("src/") + config.setFrom("../detekt.yml") + source.setFrom("src/") } diff --git a/app/lint.xml b/app/lint.xml index 9015fa6cb..c571055aa 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -32,6 +32,5 @@ - diff --git a/app/src/androidTest/java/com/nextcloud/talk/utils/ShareUtilsIT.kt b/app/src/androidTest/java/com/nextcloud/talk/utils/ShareUtilsIT.kt index eeef9ea8d..2011acda6 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/utils/ShareUtilsIT.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/utils/ShareUtilsIT.kt @@ -20,9 +20,12 @@ class ShareUtilsIT { return DateUtils.parseDate( dateStr, Locale.US, HttpUtils.httpDateFormatStr, - "EEE, dd MMM yyyy HH:mm:ss zzz", // RFC 822, updated by RFC 1123 with any TZ - "EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 850, obsoleted by RFC 1036 with any TZ. - "EEE MMM d HH:mm:ss yyyy", // ANSI C's asctime() format + // RFC 822, updated by RFC 1123 with any TZ + "EEE, dd MMM yyyy HH:mm:ss zzz", + // RFC 850, obsoleted by RFC 1036 with any TZ. + "EEEE, dd-MMM-yy HH:mm:ss zzz", + // ANSI C's asctime() format + "EEE MMM d HH:mm:ss yyyy", // Alternative formats. "EEE, dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MMM-yyyy HH-mm-ss z", @@ -35,7 +38,7 @@ class ShareUtilsIT { "EEE,dd-MMM-yy HH:mm:ss z", "EEE,dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MM-yyyy HH:mm:ss z", - /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */ + // RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com "EEE MMM d yyyy HH:mm:ss z" ) } diff --git a/app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java b/app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java index 19ca5448b..88506eb1c 100644 --- a/app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java +++ b/app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java @@ -38,9 +38,6 @@ public class ClosedInterfaceImpl implements ClosedInterface { @Override public void setUpPushTokenRegistration() { - // no push notifications for generic build flavour :( - // If you want to develop push notifications without google play services, here is a good place to start... - // Also have a look at app/src/gplay/AndroidManifest.xml to see how to include a service that handles push - // notifications. + // no push notifications for generic build variant } } diff --git a/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt b/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt index afc7c7bb7..a2320217e 100644 --- a/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt +++ b/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt @@ -28,20 +28,24 @@ import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager import androidx.work.Worker import androidx.work.WorkerParameters +import autodagger.AutoInjector import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.messaging.FirebaseMessaging +import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.utils.preferences.AppPreferences import javax.inject.Inject +@AutoInjector(NextcloudTalkApplication::class) class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerParameters) : Worker(context, workerParameters) { - @JvmField @Inject - var appPreferences: AppPreferences? = null + lateinit var appPreferences: AppPreferences @SuppressLint("LongLogTag") override fun doWork(): Result { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + FirebaseMessaging.getInstance().token.addOnCompleteListener( OnCompleteListener { task -> if (!task.isSuccessful) { @@ -49,14 +53,13 @@ class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerP return@OnCompleteListener } - val token = task.result + val pushToken = task.result + Log.d(TAG, "Fetched firebase push token is: $pushToken") - appPreferences?.pushToken = token + appPreferences.pushToken = pushToken val data: Data = - Data.Builder() - .putString(PushRegistrationWorker.ORIGIN, "GetFirebasePushTokenWorker") - .build() + Data.Builder().putString(PushRegistrationWorker.ORIGIN, "GetFirebasePushTokenWorker").build() val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java) .setInputData(data) .build() @@ -68,6 +71,6 @@ class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerP } companion object { - const val TAG = "GetFirebasePushTokenWorker" + private val TAG = GetFirebasePushTokenWorker::class.simpleName } } diff --git a/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt index 04f61b052..413b2e797 100644 --- a/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt +++ b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt @@ -79,10 +79,8 @@ class NCFirebaseMessagingService : FirebaseMessagingService() { appPreferences.pushToken = token - val data: Data = Data.Builder().putString( - PushRegistrationWorker.ORIGIN, - "NCFirebaseMessagingService#onNewToken" - ).build() + val data: Data = + Data.Builder().putString(PushRegistrationWorker.ORIGIN, "NCFirebaseMessagingService#onNewToken").build() val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java) .setInputData(data) .build() diff --git a/app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt b/app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt index 7c24a019e..f7562336d 100644 --- a/app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt +++ b/app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt @@ -24,7 +24,7 @@ package com.nextcloud.talk.utils import android.content.Intent -import androidx.work.Data +import android.util.Log import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.PeriodicWorkRequest @@ -36,7 +36,6 @@ import com.google.android.gms.security.ProviderInstaller import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.interfaces.ClosedInterface import com.nextcloud.talk.jobs.GetFirebasePushTokenWorker -import com.nextcloud.talk.jobs.PushRegistrationWorker import java.util.concurrent.TimeUnit @AutoInjector(NextcloudTalkApplication::class) @@ -65,77 +64,43 @@ class ClosedInterfaceImpl : ClosedInterface, ProviderInstaller.ProviderInstallLi val api = GoogleApiAvailability.getInstance() val code = NextcloudTalkApplication.sharedApplication?.let { - api.isGooglePlayServicesAvailable( - it.applicationContext - ) + api.isGooglePlayServicesAvailable(it.applicationContext) } - return code == ConnectionResult.SUCCESS + return if (code == ConnectionResult.SUCCESS) { + true + } else { + Log.w(TAG, "GooglePlayServices are not available. Code:$code") + false + } } override fun setUpPushTokenRegistration() { - registerLocalToken() - setUpPeriodicLocalTokenRegistration() + val firebasePushTokenWorker = OneTimeWorkRequest.Builder(GetFirebasePushTokenWorker::class.java).build() + WorkManager.getInstance().enqueue(firebasePushTokenWorker) + setUpPeriodicTokenRefreshFromFCM() } - private fun registerLocalToken() { - val data: Data = Data.Builder().putString( - PushRegistrationWorker.ORIGIN, - "ClosedInterfaceImpl#registerLocalToken" - ) - .build() - val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java) - .setInputData(data) - .build() - WorkManager.getInstance().enqueue(pushRegistrationWork) - } - - private fun setUpPeriodicLocalTokenRegistration() { - val data: Data = Data.Builder().putString( - PushRegistrationWorker.ORIGIN, - "ClosedInterfaceImpl#setUpPeriodicLocalTokenRegistration" - ) - .build() - - val periodicTokenRegistration = PeriodicWorkRequest.Builder( - PushRegistrationWorker::class.java, - DAILY, - TimeUnit.HOURS, - FLEX_INTERVAL, - TimeUnit.HOURS - ) - .setInputData(data) - .build() - - WorkManager.getInstance() - .enqueueUniquePeriodicWork( - "periodicTokenRegistration", - ExistingPeriodicWorkPolicy.REPLACE, - periodicTokenRegistration - ) - } - private fun setUpPeriodicTokenRefreshFromFCM() { val periodicTokenRefreshFromFCM = PeriodicWorkRequest.Builder( GetFirebasePushTokenWorker::class.java, - MONTHLY, - TimeUnit.DAYS, + DAILY, + TimeUnit.HOURS, FLEX_INTERVAL, - TimeUnit.DAYS - ) - .build() + TimeUnit.HOURS + ).build() WorkManager.getInstance() .enqueueUniquePeriodicWork( "periodicTokenRefreshFromFCM", - ExistingPeriodicWorkPolicy.REPLACE, + ExistingPeriodicWorkPolicy.UPDATE, periodicTokenRefreshFromFCM ) } companion object { + private val TAG = ClosedInterfaceImpl::class.java.simpleName const val DAILY: Long = 24 - const val MONTHLY: Long = 30 const val FLEX_INTERVAL: Long = 10 } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f3ab5958c..f0823c943 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ ~ @author Mario Danic ~ @author Marcel Hibbe ~ Copyright (C) 2017-2019 Mario Danic - ~ Copyright (C) 2021-2022 Marcel Hibbe + ~ Copyright (C) 2021-2023 Marcel Hibbe ~ ~ 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 @@ -131,6 +131,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -215,32 +253,10 @@ android:name=".contacts.ContactsActivity" android:theme="@style/AppTheme" /> - - - - - - - - - - - - - - - - - diff --git a/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.kt b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt similarity index 63% rename from app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.kt rename to app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt index d6ecd5a81..bcbf04dfb 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.kt +++ b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt @@ -3,6 +3,8 @@ * * @author Mario Danic * @author Andy Scherzinger + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe * Copyright (C) 2022 Andy Scherzinger * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com) * @@ -19,7 +21,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.nextcloud.talk.controllers +package com.nextcloud.talk.account import android.annotation.SuppressLint import android.content.Intent @@ -28,27 +30,25 @@ import android.os.Bundle import android.os.Handler import android.text.TextUtils import android.util.Log -import android.view.View +import android.widget.Toast import androidx.work.Data import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo import androidx.work.WorkManager import autodagger.AutoInjector -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.bluelinelabs.logansquare.LoganSquare import com.google.android.material.snackbar.Snackbar import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication -import com.nextcloud.talk.controllers.base.BaseController -import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.conversationlist.ConversationsListActivity import com.nextcloud.talk.data.user.model.User -import com.nextcloud.talk.databinding.ControllerAccountVerificationBinding +import com.nextcloud.talk.databinding.ActivityAccountVerificationBinding import com.nextcloud.talk.events.EventStatus +import com.nextcloud.talk.jobs.AccountRemovalWorker import com.nextcloud.talk.jobs.CapabilitiesWorker -import com.nextcloud.talk.jobs.PushRegistrationWorker import com.nextcloud.talk.jobs.SignalingSettingsWorker import com.nextcloud.talk.jobs.WebsocketConnectionsWorker import com.nextcloud.talk.models.json.capabilities.Capabilities @@ -63,6 +63,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_PASSWORD import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder @@ -71,20 +72,15 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.net.CookieManager import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) -class AccountVerificationController(args: Bundle? = null) : BaseController( - R.layout.controller_account_verification, - args -) { - private val binding: ControllerAccountVerificationBinding? by viewBinding( - ControllerAccountVerificationBinding::bind - ) +class AccountVerificationActivity : BaseActivity() { + + private lateinit var binding: ActivityAccountVerificationBinding @Inject lateinit var ncApi: NcApi @@ -95,9 +91,6 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( @Inject lateinit var cookieManager: CookieManager - @Inject - lateinit var eventBus: EventBus - private var internalAccountId: Long = -1 private val disposables: MutableList = ArrayList() private var baseUrl: String? = null @@ -106,43 +99,53 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( private var isAccountImport = false private var originalProtocol: String? = null - override fun onAttach(view: View) { - super.onAttach(view) - eventBus.register(this) - } - - override fun onDetach(view: View) { - super.onDetach(view) - eventBus.unregister(this) - } - - override fun onViewBound(view: View) { - super.onViewBound(view) - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + binding = ActivityAccountVerificationBinding.inflate(layoutInflater) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + setContentView(binding.root) actionBar?.hide() + setupPrimaryColors() + + handleIntent() + } + + private fun handleIntent() { + val extras = intent.extras!! + baseUrl = extras.getString(KEY_BASE_URL) + username = extras.getString(KEY_USERNAME) + token = extras.getString(KEY_TOKEN) + if (extras.containsKey(KEY_IS_ACCOUNT_IMPORT)) { + isAccountImport = true + } + if (extras.containsKey(KEY_ORIGINAL_PROTOCOL)) { + originalProtocol = extras.getString(KEY_ORIGINAL_PROTOCOL) + } + } + + override fun onResume() { + super.onResume() if ( isAccountImport && !UriUtils.hasHttpProtocolPrefixed(baseUrl!!) || - isSameProtocol(baseUrl!!, originalProtocol!!) + isNotSameProtocol(baseUrl!!, originalProtocol) ) { determineBaseUrlProtocol(true) } else { - checkEverything() + findServerTalkApp() } } - private fun isSameProtocol(baseUrl: String, originalProtocol: String): Boolean { + private fun isNotSameProtocol(baseUrl: String, originalProtocol: String?): Boolean { + if (originalProtocol == null) { + return true + } return !TextUtils.isEmpty(originalProtocol) && !baseUrl.startsWith(originalProtocol) } - private fun checkEverything() { - val credentials = ApiUtils.getCredentials(username, token) - cookieManager.cookieStore.removeAll() - findServerTalkApp(credentials) - } - private fun determineBaseUrlProtocol(checkForcedHttps: Boolean) { cookieManager.cookieStore.removeAll() baseUrl = baseUrl!!.replace("http://", "").replace("https://", "") @@ -166,20 +169,16 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( "http://$baseUrl" } if (isAccountImport) { - router.replaceTopController( - RouterTransaction.with( - WebViewLoginController( - baseUrl, - false, - username, - "" - ) - ) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()) - ) + val bundle = Bundle() + bundle.putString(KEY_BASE_URL, baseUrl) + bundle.putString(KEY_USERNAME, username) + bundle.putString(KEY_PASSWORD, "") + + val intent = Intent(context, WebViewLoginActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) } else { - checkEverything() + findServerTalkApp() } } @@ -197,7 +196,10 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( }) } - private fun findServerTalkApp(credentials: String) { + private fun findServerTalkApp() { + val credentials = ApiUtils.getCredentials(username, token) + cookieManager.cookieStore.removeAll() + ncApi.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl)) .subscribeOn(Schedulers.io()) .subscribe(object : Observer { @@ -214,27 +216,24 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( if (hasTalk) { fetchProfile(credentials, capabilitiesOverall) } else { - if (activity != null && resources != null) { - activity!!.runOnUiThread { - binding?.progressText?.setText( - String.format( - resources!!.getString(R.string.nc_nextcloud_talk_app_not_installed), - resources!!.getString(R.string.nc_app_product_name) - ) + if (resources != null) { + runOnUiThread { + binding.progressText.text = String.format( + resources!!.getString(R.string.nc_nextcloud_talk_app_not_installed), + resources!!.getString(R.string.nc_app_product_name) ) } } - ApplicationWideMessageHolder.getInstance().setMessageType( + ApplicationWideMessageHolder.getInstance().messageType = ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK - ) abortVerification() } } override fun onError(e: Throwable) { - if (activity != null && resources != null) { - activity!!.runOnUiThread { - binding?.progressText?.text = String.format( + if (resources != null) { + runOnUiThread { + binding.progressText.text = String.format( resources!!.getString(R.string.nc_nextcloud_talk_app_not_installed), resources!!.getString(R.string.nc_app_product_name) ) @@ -263,7 +262,7 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( displayName = displayName, pushConfigurationState = null, capabilities = LoganSquare.serialize(capabilities), - certificateAlias = appPreferences!!.temporaryClientCertAlias, + certificateAlias = appPreferences.temporaryClientCertAlias, externalSignalingServer = null ) ) @@ -277,11 +276,12 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( override fun onSuccess(user: User) { internalAccountId = user.id!! if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) { - registerForPush() + ClosedInterfaceImpl().setUpPushTokenRegistration() } else { - activity!!.runOnUiThread { - binding?.progressText?.text = - """ ${binding?.progressText?.text} + Log.w(TAG, "Skipping push registration.") + runOnUiThread { + binding.progressText.text = + """ ${binding.progressText.text} ${resources!!.getString(R.string.nc_push_disabled)} """.trimIndent() } @@ -291,7 +291,7 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( @SuppressLint("SetTextI18n") override fun onError(e: Throwable) { - binding?.progressText?.text = """ ${binding?.progressText?.text}""".trimIndent() + + binding.progressText.text = """ ${binding.progressText.text}""".trimIndent() + resources!!.getString(R.string.nc_display_name_not_stored) abortVerification() } @@ -328,14 +328,12 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( capabilities.ocs!!.data!!.capabilities!! ) } else { - if (activity != null) { - activity!!.runOnUiThread { - binding?.progressText?.text = - """ - ${binding?.progressText?.text} - ${resources!!.getString(R.string.nc_display_name_not_fetched)} - """.trimIndent() - } + runOnUiThread { + binding.progressText.text = + """ + ${binding.progressText.text} + ${resources!!.getString(R.string.nc_display_name_not_fetched)} + """.trimIndent() } abortVerification() } @@ -343,14 +341,12 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( @SuppressLint("SetTextI18n") override fun onError(e: Throwable) { - if (activity != null) { - activity!!.runOnUiThread { - binding?.progressText?.text = - """ - ${binding?.progressText?.text} - ${resources!!.getString(R.string.nc_display_name_not_fetched)} - """.trimIndent() - } + runOnUiThread { + binding.progressText.text = + """ + ${binding.progressText.text} + ${resources!!.getString(R.string.nc_display_name_not_fetched)} + """.trimIndent() } abortVerification() } @@ -361,27 +357,16 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( }) } - private fun registerForPush() { - val data = - Data.Builder() - .putString(PushRegistrationWorker.ORIGIN, "AccountVerificationController#registerForPush") - .build() - val pushRegistrationWork = - OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java) - .setInputData(data) - .build() - WorkManager.getInstance().enqueue(pushRegistrationWork) - } - @SuppressLint("SetTextI18n") @Subscribe(threadMode = ThreadMode.BACKGROUND) fun onMessageEvent(eventStatus: EventStatus) { + Log.d(TAG, "caught EventStatus of type " + eventStatus.eventType.toString()) if (eventStatus.eventType == EventStatus.EventType.PUSH_REGISTRATION) { - if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood && activity != null) { - activity!!.runOnUiThread { - binding?.progressText?.text = + if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) { + runOnUiThread { + binding.progressText.text = """ - ${binding?.progressText?.text} + ${binding.progressText.text} ${resources!!.getString(R.string.nc_push_disabled)} """.trimIndent() } @@ -389,14 +374,12 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( fetchAndStoreCapabilities() } else if (eventStatus.eventType == EventStatus.EventType.CAPABILITIES_FETCH) { if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) { - if (activity != null) { - activity!!.runOnUiThread { - binding?.progressText?.text = - """ - ${binding?.progressText?.text} - ${resources!!.getString(R.string.nc_capabilities_failed)} - """.trimIndent() - } + runOnUiThread { + binding.progressText.text = + """ + ${binding.progressText.text} + ${resources!!.getString(R.string.nc_capabilities_failed)} + """.trimIndent() } abortVerification() } else if (internalAccountId == eventStatus.userId && eventStatus.isAllGood) { @@ -404,14 +387,12 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( } } else if (eventStatus.eventType == EventStatus.EventType.SIGNALING_SETTINGS) { if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) { - if (activity != null) { - activity!!.runOnUiThread { - binding?.progressText?.text = - """ - ${binding?.progressText?.text} - ${resources!!.getString(R.string.nc_external_server_failed)} - """.trimIndent() - } + runOnUiThread { + binding.progressText.text = + """ + ${binding.progressText.text} + ${resources!!.getString(R.string.nc_external_server_failed)} + """.trimIndent() } } proceedWithLogin() @@ -423,11 +404,11 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( Data.Builder() .putLong(KEY_INTERNAL_USER_ID, internalAccountId) .build() - val pushNotificationWork = + val capabilitiesWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java) .setInputData(userData) .build() - WorkManager.getInstance().enqueue(pushNotificationWork) + WorkManager.getInstance().enqueue(capabilitiesWork) } private fun fetchAndStoreExternalSignalingSettings() { @@ -435,19 +416,18 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( Data.Builder() .putLong(KEY_INTERNAL_USER_ID, internalAccountId) .build() - val signalingSettings = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java) + val signalingSettingsWorker = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java) .setInputData(userData) .build() val websocketConnectionsWorker = OneTimeWorkRequest.Builder(WebsocketConnectionsWorker::class.java).build() WorkManager.getInstance(applicationContext!!) - .beginWith(signalingSettings) + .beginWith(signalingSettingsWorker) .then(websocketConnectionsWorker) .enqueue() } private fun proceedWithLogin() { - Log.d(TAG, "proceedWithLogin...") cookieManager.cookieStore.removeAll() if (userManager.users.blockingGet().size == 1 || @@ -457,25 +437,25 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( Log.d(TAG, "userToSetAsActive: " + userToSetAsActive.username) if (userManager.setUserAsActive(userToSetAsActive).blockingGet()) { - if (activity != null) { - activity!!.runOnUiThread { - if (userManager.users.blockingGet().size == 1) { - val intent = Intent(context, ConversationsListActivity::class.java) - startActivity(intent) - } else { - if (isAccountImport) { - ApplicationWideMessageHolder.getInstance().messageType = - ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED - } - val intent = Intent(context, ConversationsListActivity::class.java) - startActivity(intent) + runOnUiThread { + if (userManager.users.blockingGet().size == 1) { + val intent = Intent(context, ConversationsListActivity::class.java) + startActivity(intent) + } else { + if (isAccountImport) { + ApplicationWideMessageHolder.getInstance().messageType = + ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED } + val intent = Intent(context, ConversationsListActivity::class.java) + startActivity(intent) } } } else { Log.e(TAG, "failed to set active user") - Snackbar.make(binding!!.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() } + } else { + Log.d(TAG, "continuing proceedWithLogin was skipped for this user") } } @@ -487,70 +467,70 @@ class AccountVerificationController(args: Bundle? = null) : BaseController( } } - override fun onDestroyView(view: View) { - super.onDestroyView(view) - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR - } - public override fun onDestroy() { dispose() super.onDestroy() } private fun abortVerification() { - if (!isAccountImport) { - if (internalAccountId != -1L) { - val count = userManager.deleteUser(internalAccountId) - if (count > 0) { - activity?.runOnUiThread { Handler().postDelayed({ router.popToRoot() }, DELAY_IN_MILLIS) } - } - } else { - activity?.runOnUiThread { Handler().postDelayed({ router.popToRoot() }, DELAY_IN_MILLIS) } - } - } else { - ApplicationWideMessageHolder.getInstance().setMessageType( - ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT - ) - activity?.runOnUiThread { + if (isAccountImport) { + ApplicationWideMessageHolder.getInstance().messageType = ApplicationWideMessageHolder.MessageType + .FAILED_TO_IMPORT_ACCOUNT + runOnUiThread { Handler().postDelayed({ - if (router.hasRootController()) { - if (activity != null) { - router.popToRoot() - } - } else { - if (userManager.users.blockingGet().isNotEmpty()) { - val intent = Intent(context, ConversationsListActivity::class.java) - startActivity(intent) - } else { - router.setRoot( - RouterTransaction.with(ServerSelectionController()) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()) - ) - } - } + val intent = Intent(this, ServerSelectionActivity::class.java) + startActivity(intent) }, DELAY_IN_MILLIS) } + } else { + if (internalAccountId != -1L) { + runOnUiThread { + deleteUserAndStartServerSelection(internalAccountId) + } + } else { + runOnUiThread { + Handler().postDelayed({ + val intent = Intent(this, ServerSelectionActivity::class.java) + startActivity(intent) + }, DELAY_IN_MILLIS) + } + } } } + @SuppressLint("CheckResult") + private fun deleteUserAndStartServerSelection(userId: Long) { + userManager.scheduleUserForDeletionWithId(userId).blockingGet() + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + .observeForever { workInfo: WorkInfo -> + + when (workInfo.state) { + WorkInfo.State.SUCCEEDED -> { + val intent = Intent(this, ServerSelectionActivity::class.java) + startActivity(intent) + } + + WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "something went wrong when deleting user with id $userId") + val intent = Intent(this, ServerSelectionActivity::class.java) + startActivity(intent) + } + + else -> {} + } + } + } + companion object { - const val TAG = "AccountVerification" + private val TAG = AccountVerificationActivity::class.java.simpleName const val DELAY_IN_MILLIS: Long = 7500 } - - init { - sharedApplication!!.componentApplication.inject(this) - if (args != null) { - baseUrl = args.getString(KEY_BASE_URL) - username = args.getString(KEY_USERNAME) - token = args.getString(KEY_TOKEN) - if (args.containsKey(KEY_IS_ACCOUNT_IMPORT)) { - isAccountImport = true - } - if (args.containsKey(KEY_ORIGINAL_PROTOCOL)) { - originalProtocol = args.getString(KEY_ORIGINAL_PROTOCOL) - } - } - } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ServerSelectionController.kt b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt similarity index 65% rename from app/src/main/java/com/nextcloud/talk/controllers/ServerSelectionController.kt rename to app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt index a05899b52..fb7d2de7a 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ServerSelectionController.kt +++ b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt @@ -3,6 +3,8 @@ * * @author Andy Scherzinger * @author Mario Danic + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe * Copyright (C) 2021 Andy Scherzinger * Copyright (C) 2017 Mario Danic * @@ -19,7 +21,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.nextcloud.talk.controllers +package com.nextcloud.talk.account import android.accounts.Account import android.annotation.SuppressLint @@ -34,24 +36,21 @@ import android.view.KeyEvent import android.view.View import android.view.inputmethod.EditorInfo import android.widget.TextView -import androidx.core.content.res.ResourcesCompat +import androidx.activity.OnBackPressedCallback import autodagger.AutoInjector -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication -import com.nextcloud.talk.controllers.base.BaseController -import com.nextcloud.talk.controllers.util.viewBinding -import com.nextcloud.talk.databinding.ControllerServerSelectionBinding +import com.nextcloud.talk.databinding.ActivityServerSelectionBinding import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall import com.nextcloud.talk.models.json.generic.Status import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.AccountUtils import com.nextcloud.talk.utils.ApiUtils -import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.UriUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.ADD_ADDITIONAL_ACCOUNT import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder @@ -61,12 +60,12 @@ import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import java.security.cert.CertificateException import javax.inject.Inject +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication @AutoInjector(NextcloudTalkApplication::class) -class ServerSelectionController : - BaseController(R.layout.controller_server_selection) { +class ServerSelectionActivity : BaseActivity() { - private val binding: ControllerServerSelectionBinding? by viewBinding(ControllerServerSelectionBinding::bind) + private lateinit var binding: ActivityServerSelectionBinding @Inject lateinit var ncApi: NcApi @@ -76,44 +75,40 @@ class ServerSelectionController : private var statusQueryDisposable: Disposable? = null - fun onCertClick() { - if (activity != null) { - KeyChain.choosePrivateKeyAlias( - activity!!, - { alias: String? -> - if (alias != null) { - appPreferences!!.temporaryClientCertAlias = alias - } else { - appPreferences!!.removeTemporaryClientCertAlias() - } - setCertTextView() - }, - arrayOf("RSA", "EC"), - null, - null, - -1, - null - ) + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (intent.hasExtra(ADD_ADDITIONAL_ACCOUNT) && intent.getBooleanExtra(ADD_ADDITIONAL_ACCOUNT, false)) { + finish() + } else { + finishAffinity() + } } } - override fun onViewBound(view: View) { - super.onViewBound(view) + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) sharedApplication!!.componentApplication.inject(this) - if (activity != null) { - activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - + binding = ActivityServerSelectionBinding.inflate(layoutInflater) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + setContentView(binding.root) actionBar?.hide() + setupPrimaryColors() - binding?.hostUrlInputHelperText?.text = String.format( + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + override fun onResume() { + super.onResume() + + binding.hostUrlInputHelperText.text = String.format( resources!!.getString(R.string.nc_server_helper_text), resources!!.getString(R.string.nc_server_product_name) ) - binding?.serverEntryTextInputLayout?.setEndIconOnClickListener { checkServerAndProceed() } + binding.serverEntryTextInputLayout.setEndIconOnClickListener { checkServerAndProceed() } if (resources!!.getBoolean(R.bool.hide_auth_cert)) { - binding?.certTextView?.visibility = View.GONE + binding.certTextView.visibility = View.GONE } val loggedInUsers = userManager.users.blockingGet() @@ -124,21 +119,54 @@ class ServerSelectionController : } else if (isAbleToShowProviderLink() && loggedInUsers.isEmpty()) { showVisitProvidersInfo() } else { - binding?.importOrChooseProviderText?.visibility = View.INVISIBLE + binding.importOrChooseProviderText.visibility = View.INVISIBLE } - binding?.serverEntryTextInputEditText?.requestFocus() + binding.serverEntryTextInputEditText.requestFocus() if (!TextUtils.isEmpty(resources!!.getString(R.string.weblogin_url))) { - binding?.serverEntryTextInputEditText?.setText(resources!!.getString(R.string.weblogin_url)) + binding.serverEntryTextInputEditText.setText(resources!!.getString(R.string.weblogin_url)) checkServerAndProceed() } - binding?.serverEntryTextInputEditText?.setOnEditorActionListener { _: TextView?, i: Int, _: KeyEvent? -> + binding.serverEntryTextInputEditText.setOnEditorActionListener { _: TextView?, i: Int, _: KeyEvent? -> if (i == EditorInfo.IME_ACTION_DONE) { checkServerAndProceed() } false } - binding?.certTextView?.setOnClickListener { onCertClick() } + binding.certTextView.setOnClickListener { onCertClick() } + + if (ApplicationWideMessageHolder.getInstance().messageType != null) { + if (ApplicationWideMessageHolder.getInstance().messageType + == ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK + ) { + setErrorText(resources!!.getString(R.string.nc_settings_no_talk_installed)) + } else if (ApplicationWideMessageHolder.getInstance().messageType + == ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT + ) { + setErrorText(resources!!.getString(R.string.nc_server_failed_to_import_account)) + } + ApplicationWideMessageHolder.getInstance().messageType = null + } + setCertTextView() + } + + fun onCertClick() { + KeyChain.choosePrivateKeyAlias( + this, + { alias: String? -> + if (alias != null) { + appPreferences.temporaryClientCertAlias = alias + } else { + appPreferences.removeTemporaryClientCertAlias() + } + setCertTextView() + }, + arrayOf("RSA", "EC"), + null, + null, + -1, + null + ) } private fun isAbleToShowProviderLink(): Boolean { @@ -152,41 +180,37 @@ class ServerSelectionController : ) ) { if (availableAccounts.size > 1) { - binding?.importOrChooseProviderText?.text = String.format( + binding.importOrChooseProviderText.text = String.format( resources!!.getString(R.string.nc_server_import_accounts), AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from)) ) } else { - binding?.importOrChooseProviderText?.text = String.format( + binding.importOrChooseProviderText.text = String.format( resources!!.getString(R.string.nc_server_import_account), AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from)) ) } } else { if (availableAccounts.size > 1) { - binding?.importOrChooseProviderText?.text = + binding.importOrChooseProviderText.text = resources!!.getString(R.string.nc_server_import_accounts_plain) } else { - binding?.importOrChooseProviderText?.text = + binding.importOrChooseProviderText.text = resources!!.getString(R.string.nc_server_import_account_plain) } } - binding?.importOrChooseProviderText?.setOnClickListener { + binding.importOrChooseProviderText.setOnClickListener { val bundle = Bundle() bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true) - router.pushController( - RouterTransaction.with( - SwitchAccountController(bundle) - ) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()) - ) + val intent = Intent(context, SwitchAccountActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) } } private fun showVisitProvidersInfo() { - binding?.importOrChooseProviderText?.setText(R.string.nc_get_from_provider) - binding?.importOrChooseProviderText?.setOnClickListener { + binding.importOrChooseProviderText.setText(R.string.nc_get_from_provider) + binding.importOrChooseProviderText.setOnClickListener { val browserIntent = Intent( Intent.ACTION_VIEW, Uri.parse( @@ -206,11 +230,11 @@ class ServerSelectionController : @Suppress("Detekt.TooGenericExceptionCaught") private fun checkServerAndProceed() { dispose() - var url: String = binding?.serverEntryTextInputEditText?.text.toString().trim { it <= ' ' } + var url: String = binding.serverEntryTextInputEditText.text.toString().trim { it <= ' ' } showserverEntryProgressBar() - if (binding?.importOrChooseProviderText?.visibility != View.INVISIBLE) { - binding?.importOrChooseProviderText?.visibility = View.INVISIBLE - binding?.certTextView?.visibility = View.INVISIBLE + if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) { + binding.importOrChooseProviderText.visibility = View.INVISIBLE + binding.certTextView.visibility = View.INVISIBLE } if (url.endsWith("/")) { url = url.substring(0, url.length - 1) @@ -278,17 +302,17 @@ class ServerSelectionController : hideserverEntryProgressBar() } - if (binding?.importOrChooseProviderText?.visibility != View.INVISIBLE) { - binding?.importOrChooseProviderText?.visibility = View.VISIBLE - binding?.certTextView?.visibility = View.VISIBLE + if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) { + binding.importOrChooseProviderText.visibility = View.VISIBLE + binding.certTextView.visibility = View.VISIBLE } dispose() } }) { hideserverEntryProgressBar() - if (binding?.importOrChooseProviderText?.visibility != View.INVISIBLE) { - binding?.importOrChooseProviderText?.visibility = View.VISIBLE - binding?.certTextView?.visibility = View.VISIBLE + if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) { + binding.importOrChooseProviderText.visibility = View.VISIBLE + binding.certTextView.visibility = View.VISIBLE } dispose() } @@ -311,29 +335,25 @@ class ServerSelectionController : capabilities.spreedCapability?.features?.isNotEmpty() == true if (hasTalk) { - activity?.runOnUiThread { + runOnUiThread { if (CapabilitiesUtilNew.isServerEOL(capabilities)) { if (resources != null) { - activity!!.runOnUiThread { + runOnUiThread { setErrorText(resources!!.getString(R.string.nc_settings_server_eol)) } } } else { - router.pushController( - RouterTransaction.with( - WebViewLoginController( - queryUrl.replace("/status.php", ""), - false - ) - ) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()) - ) + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_BASE_URL, queryUrl.replace("/status.php", "")) + + val intent = Intent(context, WebViewLoginActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) } } } else { - if (activity != null && resources != null) { - activity!!.runOnUiThread { + if (resources != null) { + runOnUiThread { setErrorText(resources!!.getString(R.string.nc_server_unsupported)) } } @@ -342,8 +362,8 @@ class ServerSelectionController : override fun onError(e: Throwable) { Log.e(TAG, "Error while checking capabilities", e) - if (activity != null && resources != null) { - activity!!.runOnUiThread { + if (resources != null) { + runOnUiThread { setErrorText(resources!!.getString(R.string.nc_common_error_sorry)) } } @@ -360,70 +380,29 @@ class ServerSelectionController : } private fun setErrorText(text: String) { - binding?.errorWrapper?.visibility = View.VISIBLE - binding?.errorText?.text = text + binding.errorWrapper.visibility = View.VISIBLE + binding.errorText.text = text hideserverEntryProgressBar() } private fun showserverEntryProgressBar() { - binding?.errorWrapper?.visibility = View.INVISIBLE - binding?.serverEntryProgressBar?.visibility = View.VISIBLE + binding.errorWrapper.visibility = View.INVISIBLE + binding.serverEntryProgressBar.visibility = View.VISIBLE } private fun hideserverEntryProgressBar() { - binding?.serverEntryProgressBar?.visibility = View.INVISIBLE - } - - override fun onAttach(view: View) { - super.onAttach(view) - if (ApplicationWideMessageHolder.getInstance().messageType != null) { - if (ApplicationWideMessageHolder.getInstance().messageType - == ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION - ) { - setErrorText(resources!!.getString(R.string.nc_account_scheduled_for_deletion)) - ApplicationWideMessageHolder.getInstance().messageType = null - } else if (ApplicationWideMessageHolder.getInstance().messageType - == ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK - ) { - setErrorText(resources!!.getString(R.string.nc_settings_no_talk_installed)) - } else if (ApplicationWideMessageHolder.getInstance().messageType - == ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT - ) { - setErrorText(resources!!.getString(R.string.nc_server_failed_to_import_account)) - } - ApplicationWideMessageHolder.getInstance().messageType = null - } - if (activity != null && resources != null) { - DisplayUtils.applyColorToStatusBar( - activity, - ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null) - ) - DisplayUtils.applyColorToNavigationBar( - activity!!.window, - ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null) - ) - } - setCertTextView() + binding.serverEntryProgressBar.visibility = View.INVISIBLE } @SuppressLint("LongLogTag") private fun setCertTextView() { - if (activity != null) { - activity!!.runOnUiThread { - if (!TextUtils.isEmpty(appPreferences!!.temporaryClientCertAlias)) { - binding?.certTextView?.setText(R.string.nc_change_cert_auth) - } else { - binding?.certTextView?.setText(R.string.nc_configure_cert_auth) - } - hideserverEntryProgressBar() + runOnUiThread { + if (!TextUtils.isEmpty(appPreferences.temporaryClientCertAlias)) { + binding.certTextView.setText(R.string.nc_change_cert_auth) + } else { + binding.certTextView.setText(R.string.nc_configure_cert_auth) } - } - } - - override fun onDestroyView(view: View) { - super.onDestroyView(view) - if (activity != null) { - activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR + hideserverEntryProgressBar() } } @@ -443,7 +422,7 @@ class ServerSelectionController : get() = AppBarLayoutType.EMPTY companion object { - const val TAG = "ServerSelectionController" + private val TAG = ServerSelectionActivity::class.java.simpleName const val MIN_SERVER_MAJOR_VERSION = 13 } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.kt b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt similarity index 74% rename from app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.kt rename to app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt index 36bf20a06..c19c3223f 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.kt +++ b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt @@ -3,6 +3,8 @@ * * @author Mario Danic * @author Andy Scherzinger + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe * Copyright (C) 2022 Andy Scherzinger * Copyright (C) 2017 Mario Danic * @@ -22,25 +24,23 @@ * Parts related to account import were either copied from or inspired by the great work done by David Luhmer at: * https://github.com/nextcloud/ownCloud-Account-Importer */ -package com.nextcloud.talk.controllers +package com.nextcloud.talk.account import android.accounts.Account +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.ActivityInfo +import android.graphics.drawable.ColorDrawable import android.os.Bundle -import android.view.MenuItem -import android.view.View import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import autodagger.AutoInjector -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.adapters.items.AdvancedUserItem import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication -import com.nextcloud.talk.controllers.base.BaseController -import com.nextcloud.talk.controllers.util.viewBinding import com.nextcloud.talk.data.user.model.User -import com.nextcloud.talk.databinding.ControllerGenericRvBinding +import com.nextcloud.talk.databinding.ActivitySwitchAccountBinding import com.nextcloud.talk.models.ImportAccount import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.users.UserManager @@ -56,14 +56,11 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import org.osmdroid.config.Configuration import java.net.CookieManager import javax.inject.Inject +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication @AutoInjector(NextcloudTalkApplication::class) -class SwitchAccountController(args: Bundle? = null) : - BaseController( - R.layout.controller_generic_rv, - args - ) { - private val binding: ControllerGenericRvBinding? by viewBinding(ControllerGenericRvBinding::bind) +class SwitchAccountActivity : BaseActivity() { + private lateinit var binding: ActivitySwitchAccountBinding @Inject lateinit var userManager: UserManager @@ -89,41 +86,52 @@ class SwitchAccountController(args: Bundle? = null) : if (userManager.setUserAsActive(user).blockingGet()) { cookieManager.cookieStore.removeAll() - if (activity != null) { - activity!!.runOnUiThread { router.popCurrentController() } - } + finish() } } true } - init { - setHasOptionsMenu(true) + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) sharedApplication!!.componentApplication.inject(this) + binding = ActivitySwitchAccountBinding.inflate(layoutInflater) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + setContentView(binding.root) + setupActionBar() + setupPrimaryColors() + Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context)) - if (args?.containsKey(KEY_IS_ACCOUNT_IMPORT) == true) { - isAccountImport = true - } + + handleIntent() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - router.popCurrentController() - true + private fun handleIntent() { + intent.extras?.let { + if (it.containsKey(KEY_IS_ACCOUNT_IMPORT)) { + isAccountImport = true } - else -> super.onOptionsItemSelected(item) } } - override fun onViewBound(view: View) { - super.onViewBound(view) - binding?.swipeRefreshLayout?.isEnabled = false + private fun setupActionBar() { + setSupportActionBar(binding.toolbar) + binding.toolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(ColorDrawable(resources!!.getColor(R.color.transparent, null))) + supportActionBar?.title = resources!!.getString(R.string.nc_select_an_account) + } - actionBar?.show() + @Suppress("Detekt.NestedBlockDepth") + override fun onResume() { + super.onResume() if (adapter == null) { - adapter = FlexibleAdapter(userItems, activity, false) + adapter = FlexibleAdapter(userItems, this, false) var participant: Participant if (!isAccountImport) { @@ -166,11 +174,10 @@ class SwitchAccountController(args: Bundle? = null) : } private fun prepareViews() { - val layoutManager: LinearLayoutManager = SmoothScrollLinearLayoutManager(activity) - binding?.recyclerView?.layoutManager = layoutManager - binding?.recyclerView?.setHasFixedSize(true) - binding?.recyclerView?.adapter = adapter - binding?.swipeRefreshLayout?.isEnabled = false + val layoutManager: LinearLayoutManager = SmoothScrollLinearLayoutManager(this) + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.adapter = adapter } private fun reauthorizeFromImport(account: Account?) { @@ -180,14 +187,9 @@ class SwitchAccountController(args: Bundle? = null) : bundle.putString(KEY_USERNAME, importAccount.getUsername()) bundle.putString(KEY_TOKEN, importAccount.getToken()) bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true) - router.pushController( - RouterTransaction.with(AccountVerificationController(bundle)) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()) - ) - } - override val title: String - get() = - resources!!.getString(R.string.nc_select_an_account) + val intent = Intent(context, AccountVerificationActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt b/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt similarity index 64% rename from app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt rename to app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt index 7bddc38c3..fb9874573 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt +++ b/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt @@ -3,6 +3,8 @@ * * @author Mario Danic * @author Andy Scherzinger + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe * Copyright (C) 2022 Andy Scherzinger * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com) * @@ -19,9 +21,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.nextcloud.talk.controllers +package com.nextcloud.talk.account import android.annotation.SuppressLint +import android.content.Intent import android.content.pm.ActivityInfo import android.graphics.Bitmap import android.net.http.SslError @@ -40,34 +43,30 @@ import android.webkit.WebResourceResponse import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.res.ResourcesCompat -import androidx.work.Data +import androidx.activity.OnBackPressedCallback import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo import androidx.work.WorkManager import autodagger.AutoInjector -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler +import com.google.android.material.snackbar.Snackbar import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.activities.MainActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication -import com.nextcloud.talk.controllers.base.BaseController -import com.nextcloud.talk.controllers.util.viewBinding -import com.nextcloud.talk.databinding.ControllerWebViewLoginBinding +import com.nextcloud.talk.databinding.ActivityWebViewLoginBinding import com.nextcloud.talk.events.CertificateEvent -import com.nextcloud.talk.jobs.PushRegistrationWorker +import com.nextcloud.talk.jobs.AccountRemovalWorker import com.nextcloud.talk.models.LoginData import com.nextcloud.talk.users.UserManager -import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME -import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder import com.nextcloud.talk.utils.ssl.TrustManager import de.cotech.hw.fido.WebViewFidoBridge import io.reactivex.disposables.Disposable -import org.greenrobot.eventbus.EventBus import java.lang.reflect.Field import java.net.CookieManager import java.net.URLDecoder @@ -78,11 +77,9 @@ import java.util.Locale import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) -class WebViewLoginController(args: Bundle? = null) : BaseController( - R.layout.controller_web_view_login, - args -) { - private val binding: ControllerWebViewLoginBinding? by viewBinding(ControllerWebViewLoginBinding::bind) +class WebViewLoginActivity : BaseActivity() { + + private lateinit var binding: ActivityWebViewLoginBinding @Inject lateinit var userManager: UserManager @@ -90,34 +87,26 @@ class WebViewLoginController(args: Bundle? = null) : BaseController( @Inject lateinit var trustManager: TrustManager - @Inject - lateinit var eventBus: EventBus - @Inject lateinit var cookieManager: CookieManager private var assembledPrefix: String? = null private var userQueryDisposable: Disposable? = null private var baseUrl: String? = null - private var isPasswordUpdate = false + private var reauthorizeAccount = false private var username: String? = null private var password: String? = null private var loginStep = 0 private var automatedLoginAttempted = false private var webViewFidoBridge: WebViewFidoBridge? = null - constructor(baseUrl: String?, isPasswordUpdate: Boolean) : this() { - this.baseUrl = baseUrl - this.isPasswordUpdate = isPasswordUpdate + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } } - - constructor(baseUrl: String?, isPasswordUpdate: Boolean, username: String?, password: String?) : this() { - this.baseUrl = baseUrl - this.isPasswordUpdate = isPasswordUpdate - this.username = username - this.password = password - } - private val webLoginUserAgent: String get() = ( Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) + @@ -129,33 +118,57 @@ class WebViewLoginController(args: Bundle? = null) : BaseController( ")" ) - @SuppressLint("SetJavaScriptEnabled") - override fun onViewBound(view: View) { - super.onViewBound(view) - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + binding = ActivityWebViewLoginBinding.inflate(layoutInflater) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + setContentView(binding.root) actionBar?.hide() + setupPrimaryColors() + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + handleIntent() + setupWebView() + } + + private fun handleIntent() { + val extras = intent.extras!! + baseUrl = extras.getString(KEY_BASE_URL) + username = extras.getString(KEY_USERNAME) + + if (extras.containsKey(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)) { + reauthorizeAccount = extras.getBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT) + } + + if (extras.containsKey(BundleKeys.KEY_PASSWORD)) { + password = extras.getString(BundleKeys.KEY_PASSWORD) + } + } + + @SuppressLint("SetJavaScriptEnabled") + private fun setupWebView() { assembledPrefix = resources!!.getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/" - binding?.webview?.settings?.allowFileAccess = false - binding?.webview?.settings?.allowFileAccessFromFileURLs = false - binding?.webview?.settings?.javaScriptEnabled = true - binding?.webview?.settings?.javaScriptCanOpenWindowsAutomatically = false - binding?.webview?.settings?.domStorageEnabled = true - binding?.webview?.settings?.setUserAgentString(webLoginUserAgent) - binding?.webview?.settings?.saveFormData = false - binding?.webview?.settings?.savePassword = false - binding?.webview?.settings?.setRenderPriority(WebSettings.RenderPriority.HIGH) - binding?.webview?.clearCache(true) - binding?.webview?.clearFormData() - binding?.webview?.clearHistory() + binding.webview.settings.allowFileAccess = false + binding.webview.settings.allowFileAccessFromFileURLs = false + binding.webview.settings.javaScriptEnabled = true + binding.webview.settings.javaScriptCanOpenWindowsAutomatically = false + binding.webview.settings.domStorageEnabled = true + binding.webview.settings.userAgentString = webLoginUserAgent + binding.webview.settings.saveFormData = false + binding.webview.settings.savePassword = false + binding.webview.settings.setRenderPriority(WebSettings.RenderPriority.HIGH) + binding.webview.clearCache(true) + binding.webview.clearFormData() + binding.webview.clearHistory() WebView.clearClientCertPreferences(null) - webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView(activity as AppCompatActivity?, binding?.webview) - CookieSyncManager.createInstance(activity) + webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView(this, binding.webview) + CookieSyncManager.createInstance(this) android.webkit.CookieManager.getInstance().removeAllCookies(null) val headers: MutableMap = HashMap() - headers.put("OCS-APIRequest", "true") - binding?.webview?.webViewClient = object : WebViewClient() { + headers["OCS-APIRequest"] = "true" + binding.webview.webViewClient = object : WebViewClient() { private var basePageLoaded = false override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { webViewFidoBridge?.delegateShouldInterceptRequest(view, request) @@ -180,24 +193,24 @@ class WebViewLoginController(args: Bundle? = null) : BaseController( override fun onPageFinished(view: WebView, url: String) { loginStep++ if (!basePageLoaded) { - binding?.progressBar?.visibility = View.GONE - binding?.webview?.visibility = View.VISIBLE + binding.progressBar.visibility = View.GONE + binding.webview.visibility = View.VISIBLE basePageLoaded = true } if (!TextUtils.isEmpty(username)) { if (loginStep == 1) { - binding?.webview?.loadUrl( + binding.webview.loadUrl( "javascript: {document.getElementsByClassName('login')[0].click(); };" ) } else if (!automatedLoginAttempted) { automatedLoginAttempted = true if (TextUtils.isEmpty(password)) { - binding?.webview?.loadUrl( + binding.webview.loadUrl( "javascript:var justStore = document.getElementById('user').value = '$username';" ) } else { - binding?.webview?.loadUrl( + binding.webview.loadUrl( "javascript: {" + "document.getElementById('user').value = '" + username + "';" + "document.getElementById('password').value = '" + password + "';" + @@ -213,8 +226,8 @@ class WebViewLoginController(args: Bundle? = null) : BaseController( override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest) { val user = userManager.currentUser.blockingGet() var alias: String? = null - if (!isPasswordUpdate) { - alias = appPreferences!!.temporaryClientCertAlias + if (!reauthorizeAccount) { + alias = appPreferences.temporaryClientCertAlias } if (TextUtils.isEmpty(alias) && user != null) { alias = user.clientCertificate @@ -223,9 +236,9 @@ class WebViewLoginController(args: Bundle? = null) : BaseController( val finalAlias = alias Thread { try { - val privateKey = KeyChain.getPrivateKey(activity!!, finalAlias!!) + val privateKey = KeyChain.getPrivateKey(applicationContext, finalAlias!!) val certificates = KeyChain.getCertificateChain( - activity!!, + applicationContext, finalAlias ) if (privateKey != null && certificates != null) { @@ -241,16 +254,16 @@ class WebViewLoginController(args: Bundle? = null) : BaseController( }.start() } else { KeyChain.choosePrivateKeyAlias( - activity!!, + this@WebViewLoginActivity, { chosenAlias: String? -> if (chosenAlias != null) { appPreferences!!.temporaryClientCertAlias = chosenAlias Thread { var privateKey: PrivateKey? = null try { - privateKey = KeyChain.getPrivateKey(activity!!, chosenAlias) + privateKey = KeyChain.getPrivateKey(applicationContext, chosenAlias) val certificates = KeyChain.getCertificateChain( - activity!!, + applicationContext, chosenAlias ) if (privateKey != null && certificates != null) { @@ -304,7 +317,7 @@ class WebViewLoginController(args: Bundle? = null) : BaseController( super.onReceivedError(view, errorCode, description, failingUrl) } } - binding?.webview?.loadUrl("$baseUrl/index.php/login/flow", headers) + binding.webview.loadUrl("$baseUrl/index.php/login/flow", headers) } private fun dispose() { @@ -318,82 +331,79 @@ class WebViewLoginController(args: Bundle? = null) : BaseController( val loginData = parseLoginData(assembledPrefix, dataString) if (loginData != null) { dispose() - val currentUser = userManager.currentUser.blockingGet() - var messageType: ApplicationWideMessageHolder.MessageType? = null - if (!isPasswordUpdate && - userManager.checkIfUserExists(loginData.username!!, baseUrl!!).blockingGet() - ) { - messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED - } - if (userManager.checkIfUserIsScheduledForDeletion(loginData.username!!, baseUrl!!).blockingGet()) { - ApplicationWideMessageHolder.getInstance().messageType = - ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION - if (!isPasswordUpdate) { - router.popToRoot() - } else { - router.popCurrentController() - } - } - val finalMessageType = messageType cookieManager.cookieStore.removeAll() - if (!isPasswordUpdate && finalMessageType == null) { - val bundle = Bundle() - bundle.putString(KEY_USERNAME, loginData.username) - bundle.putString(KEY_TOKEN, loginData.token) - bundle.putString(KEY_BASE_URL, loginData.serverUrl) - var protocol = "" - if (baseUrl!!.startsWith("http://")) { - protocol = "http://" - } else if (baseUrl!!.startsWith("https://")) { - protocol = "https://" - } - if (!TextUtils.isEmpty(protocol)) { - bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol) - } - router.pushController( - RouterTransaction.with(AccountVerificationController(bundle)) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()) - ) - } else { - if (isPasswordUpdate) { - if (currentUser != null) { - currentUser.clientCertificate = appPreferences!!.temporaryClientCertAlias - currentUser.token = loginData.token - val rowsUpdated = userManager.updateOrCreateUser(currentUser).blockingGet() - Log.d(TAG, "User rows updated: $rowsUpdated") - if (finalMessageType != null) { - ApplicationWideMessageHolder.getInstance().messageType = finalMessageType - } - - val data = Data.Builder().putString( - PushRegistrationWorker.ORIGIN, - "WebViewLoginController#parseAndLoginFromWebView" - ).build() - - val pushRegistrationWork = OneTimeWorkRequest.Builder( - PushRegistrationWorker::class.java - ) - .setInputData(data) - .build() - - WorkManager.getInstance().enqueue(pushRegistrationWork) - router.popCurrentController() - } + if (userManager.checkIfUserIsScheduledForDeletion(loginData.username!!, baseUrl!!).blockingGet()) { + Log.e(TAG, "Tried to add already existing user who is scheduled for deletion.") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + // however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it. + startAccountRemovalWorkerAndRestartApp() + } else if (userManager.checkIfUserExists(loginData.username!!, baseUrl!!).blockingGet()) { + if (reauthorizeAccount) { + updateUserAndRestartApp(loginData) } else { - if (finalMessageType != null) { - // FIXME when the user registers a new account that was setup before (aka - // ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED) - // The token is not updated in the database and therefore the account not visible/usable - ApplicationWideMessageHolder.getInstance().messageType = finalMessageType - } - router.popToRoot() + Log.w(TAG, "It was tried to add an account that account already exists. Skipped user creation.") + restartApp() } + } else { + startAccountVerification(loginData) } } } + private fun startAccountVerification(loginData: LoginData) { + val bundle = Bundle() + bundle.putString(KEY_USERNAME, loginData.username) + bundle.putString(KEY_TOKEN, loginData.token) + bundle.putString(KEY_BASE_URL, loginData.serverUrl) + var protocol = "" + if (baseUrl!!.startsWith("http://")) { + protocol = "http://" + } else if (baseUrl!!.startsWith("https://")) { + protocol = "https://" + } + if (!TextUtils.isEmpty(protocol)) { + bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol) + } + val intent = Intent(context, AccountVerificationActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } + + private fun restartApp() { + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } + + private fun updateUserAndRestartApp(loginData: LoginData) { + val currentUser = userManager.currentUser.blockingGet() + if (currentUser != null) { + currentUser.clientCertificate = appPreferences.temporaryClientCertAlias + currentUser.token = loginData.token + val rowsUpdated = userManager.updateOrCreateUser(currentUser).blockingGet() + Log.d(TAG, "User rows updated: $rowsUpdated") + restartApp() + } + } + + private fun startAccountRemovalWorkerAndRestartApp() { + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + .observeForever { workInfo: WorkInfo -> + + when (workInfo.state) { + WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { + restartApp() + } + + else -> {} + } + } + } + private fun parseLoginData(prefix: String?, dataString: String): LoginData? { if (dataString.length < prefix!!.length) { return null @@ -432,30 +442,11 @@ class WebViewLoginController(args: Bundle? = null) : BaseController( } } - override fun onAttach(view: View) { - super.onAttach(view) - if (activity != null && resources != null) { - DisplayUtils.applyColorToStatusBar( - activity, - ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null) - ) - DisplayUtils.applyColorToNavigationBar( - activity!!.window, - ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null) - ) - } - } - public override fun onDestroy() { super.onDestroy() dispose() } - override fun onDestroyView(view: View) { - super.onDestroyView(view) - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR - } - init { sharedApplication!!.componentApplication.inject(this) } @@ -464,7 +455,7 @@ class WebViewLoginController(args: Bundle? = null) : BaseController( get() = AppBarLayoutType.EMPTY companion object { - const val TAG = "WebViewLoginController" + private val TAG = WebViewLoginActivity::class.java.simpleName private const val PROTOCOL_SUFFIX = "://" private const val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":" private const val PARAMETER_COUNT = 3 diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/providers/ActionBarProvider.java b/app/src/main/java/com/nextcloud/talk/activities/ActionBarProvider.java similarity index 93% rename from app/src/main/java/com/nextcloud/talk/controllers/base/providers/ActionBarProvider.java rename to app/src/main/java/com/nextcloud/talk/activities/ActionBarProvider.java index ceb393e67..76d7ce1fd 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/base/providers/ActionBarProvider.java +++ b/app/src/main/java/com/nextcloud/talk/activities/ActionBarProvider.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.nextcloud.talk.controllers.base.providers; +package com.nextcloud.talk.activities; import androidx.appcompat.app.ActionBar; diff --git a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt index 05792f941..a33027f16 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt @@ -24,17 +24,26 @@ package com.nextcloud.talk.activities import android.annotation.SuppressLint import android.content.Context +import android.os.Build import android.os.Bundle import android.util.Log import android.view.View +import android.view.ViewGroup import android.view.WindowManager +import android.view.inputmethod.EditorInfo import android.webkit.SslErrorHandler +import android.widget.EditText +import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.content.res.ResourcesCompat import autodagger.AutoInjector import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.nextcloud.talk.R +import com.nextcloud.talk.account.AccountVerificationActivity +import com.nextcloud.talk.account.ServerSelectionActivity +import com.nextcloud.talk.account.SwitchAccountActivity +import com.nextcloud.talk.account.WebViewLoginActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.events.CertificateEvent import com.nextcloud.talk.ui.theme.ViewThemeUtils @@ -53,7 +62,9 @@ import javax.inject.Inject open class BaseActivity : AppCompatActivity() { enum class AppBarLayoutType { - TOOLBAR, SEARCH_BAR, EMPTY + TOOLBAR, + SEARCH_BAR, + EMPTY } @Inject @@ -77,6 +88,8 @@ open class BaseActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) super.onCreate(savedInstanceState) + + cleanTempCertPreference() } public override fun onStart() { @@ -87,6 +100,11 @@ open class BaseActivity : AppCompatActivity() { public override fun onResume() { super.onResume() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences.isKeyboardIncognito) { + val viewGroup = (findViewById(android.R.id.content) as ViewGroup).getChildAt(0) as ViewGroup + disableKeyboardPersonalisedLearning(viewGroup) + } + if (appPreferences.isScreenSecured) { window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) } else { @@ -104,6 +122,19 @@ open class BaseActivity : AppCompatActivity() { colorizeNavigationBar() } + fun setupPrimaryColors() { + if (resources != null) { + DisplayUtils.applyColorToStatusBar( + this, + ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null) + ) + DisplayUtils.applyColorToNavigationBar( + window, + ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null) + ) + } + } + open fun colorizeStatusBar() { if (resources != null) { if (appBarLayoutType == AppBarLayoutType.SEARCH_BAR) { @@ -123,7 +154,23 @@ open class BaseActivity : AppCompatActivity() { } } - fun showCertificateDialog( + @RequiresApi(api = Build.VERSION_CODES.O) + private fun disableKeyboardPersonalisedLearning(viewGroup: ViewGroup) { + var view: View? + var editText: EditText + for (i in 0 until viewGroup.childCount) { + view = viewGroup.getChildAt(i) + if (view is EditText) { + editText = view + editText.imeOptions = editText.imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } else if (view is ViewGroup) { + disableKeyboardPersonalisedLearning(view) + } + } + } + + @Suppress("Detekt.NestedBlockDepth") + private fun showCertificateDialog( cert: X509Certificate, trustManager: TrustManager, sslErrorHandler: SslErrorHandler? @@ -160,15 +207,17 @@ open class BaseActivity : AppCompatActivity() { validUntil ) - val dialogBuilder = MaterialAlertDialogBuilder(this) - .setIcon(viewThemeUtils.dialog.colorMaterialAlertDialogIcon(context, R.drawable.ic_security_white_24dp)) - .setTitle(R.string.nc_certificate_dialog_title) + val dialogBuilder = MaterialAlertDialogBuilder(this).setIcon( + viewThemeUtils.dialog.colorMaterialAlertDialogIcon( + context, + R.drawable.ic_security_white_24dp + ) + ).setTitle(R.string.nc_certificate_dialog_title) .setMessage(dialogText) .setPositiveButton(R.string.nc_yes) { _, _ -> trustManager.addCertInTrustStore(cert) sslErrorHandler?.proceed() - } - .setNegativeButton(R.string.nc_no) { _, _ -> + }.setNegativeButton(R.string.nc_no) { _, _ -> sslErrorHandler?.cancel() } @@ -185,12 +234,23 @@ open class BaseActivity : AppCompatActivity() { } } + private fun cleanTempCertPreference() { + val temporaryClassNames: MutableList = ArrayList() + temporaryClassNames.add(ServerSelectionActivity::class.java.name) + temporaryClassNames.add(AccountVerificationActivity::class.java.name) + temporaryClassNames.add(WebViewLoginActivity::class.java.name) + temporaryClassNames.add(SwitchAccountActivity::class.java.name) + if (!temporaryClassNames.contains(javaClass.name)) { + appPreferences.removeTemporaryClientCertAlias() + } + } + @Subscribe(threadMode = ThreadMode.MAIN) fun onMessageEvent(event: CertificateEvent) { showCertificateDialog(event.x509Certificate, event.magicTrustManager, event.sslErrorHandler) } companion object { - private val TAG = "BaseActivity" + private val TAG = BaseActivity::class.java.simpleName } } diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index d39ad36f9..2439940de 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -1050,10 +1050,7 @@ class CallActivity : CallBaseActivity() { private val isConnectionEstablished: Boolean get() = currentCallStatus === CallStatus.JOINED || currentCallStatus === CallStatus.IN_CONVERSATION - private fun onAudioManagerDevicesChanged( - currentDevice: AudioDevice, - availableDevices: Set - ) { + private fun onAudioManagerDevicesChanged(currentDevice: AudioDevice, availableDevices: Set) { Log.d(TAG, "onAudioManagerDevicesChanged: $availableDevices, currentDevice: $currentDevice") val shouldDisableProximityLock = currentDevice == AudioDevice.WIRED_HEADSET || @@ -1529,10 +1526,7 @@ class CallActivity : CallBaseActivity() { }) } - private fun addIceServers( - signalingSettingsOverall: SignalingSettingsOverall, - apiVersion: Int - ) { + private fun addIceServers(signalingSettingsOverall: SignalingSettingsOverall, apiVersion: Int) { if (signalingSettingsOverall.ocs!!.settings!!.stunServers != null) { val stunServers = signalingSettingsOverall.ocs!!.settings!!.stunServers if (apiVersion == ApiUtils.APIv3) { @@ -2049,7 +2043,7 @@ class CallActivity : CallBaseActivity() { } override fun onNext(genericOverall: GenericOverall) { - if (!switchToRoomToken.isEmpty()) { + if (switchToRoomToken.isNotEmpty()) { val intent = Intent(context, ChatActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) val bundle = Bundle() @@ -2070,8 +2064,8 @@ class CallActivity : CallBaseActivity() { } override fun onError(e: Throwable) { - Snackbar.make(binding!!.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() - Log.e(TAG, "Error while leaving the call", e) + Log.w(TAG, "Something went wrong when leaving the call", e) + finish() } override fun onComplete() { @@ -3035,11 +3029,7 @@ class CallActivity : CallBaseActivity() { } } - private fun updatePictureInPictureActions( - @DrawableRes iconId: Int, - title: String?, - requestCode: Int - ) { + private fun updatePictureInPictureActions(@DrawableRes iconId: Int, title: String?, requestCode: Int) { if (isGreaterEqualOreo && isPipModePossible) { val actions = ArrayList() val icon = Icon.createWithResource(this, iconId) @@ -3096,7 +3086,7 @@ class CallActivity : CallBaseActivity() { } override fun suppressFitsSystemWindows() { - binding!!.controllerCallLayout.fitsSystemWindows = false + binding!!.callLayout.fitsSystemWindows = false } override fun onConfigurationChanged(newConfig: Configuration) { diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallStatus.kt b/app/src/main/java/com/nextcloud/talk/activities/CallStatus.kt index d717ea02d..268e2ac68 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallStatus.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallStatus.kt @@ -24,5 +24,12 @@ import kotlinx.parcelize.Parcelize @Parcelize enum class CallStatus : Parcelable { - CONNECTING, CALLING_TIMEOUT, JOINED, IN_CONVERSATION, RECONNECTING, OFFLINE, LEAVING, PUBLISHER_FAILED + CONNECTING, + CALLING_TIMEOUT, + JOINED, + IN_CONVERSATION, + RECONNECTING, + OFFLINE, + LEAVING, + PUBLISHER_FAILED } diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index 4d7462e54..ce631df93 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -32,26 +32,20 @@ import android.os.Bundle import android.provider.ContactsContract import android.text.TextUtils import android.util.Log +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import autodagger.AutoInjector -import com.bluelinelabs.conductor.Conductor -import com.bluelinelabs.conductor.Router -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler -import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler import com.google.android.material.snackbar.Snackbar -import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R +import com.nextcloud.talk.account.ServerSelectionActivity +import com.nextcloud.talk.account.WebViewLoginActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.callnotification.CallNotificationActivity import com.nextcloud.talk.chat.ChatActivity -import com.nextcloud.talk.controllers.ServerSelectionController -import com.nextcloud.talk.controllers.WebViewLoginController -import com.nextcloud.talk.controllers.base.providers.ActionBarProvider import com.nextcloud.talk.conversationlist.ConversationsListActivity import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivityMainBinding @@ -59,9 +53,9 @@ import com.nextcloud.talk.lock.LockedActivity import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ClosedInterfaceImpl import com.nextcloud.talk.utils.SecurityUtils import com.nextcloud.talk.utils.bundle.BundleKeys -import com.nextcloud.talk.utils.bundle.BundleKeys.ADD_ACCOUNT import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import io.reactivex.Observer import io.reactivex.SingleObserver @@ -80,17 +74,12 @@ class MainActivity : BaseActivity(), ActionBarProvider { @Inject lateinit var userManager: UserManager - private var router: Router? = null - private val onBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - if (!router!!.handleBack()) { - finish() - } + finish() } } - @Suppress("Detekt.TooGenericExceptionCaught") override fun onCreate(savedInstanceState: Bundle?) { Log.d(TAG, "onCreate: Activity: " + System.identityHashCode(this).toString()) @@ -111,8 +100,6 @@ class MainActivity : BaseActivity(), ActionBarProvider { setSupportActionBar(binding.toolbar) - router = Conductor.attachRouter(this, binding.controllerContainer, savedInstanceState) - handleIntent(intent) onBackPressedDispatcher.addCallback(this, onBackPressedCallback) @@ -128,28 +115,24 @@ class MainActivity : BaseActivity(), ActionBarProvider { } } - private fun launchLoginScreen() { - if (!TextUtils.isEmpty(resources.getString(R.string.weblogin_url))) { - router!!.pushController( - RouterTransaction.with( - WebViewLoginController(resources.getString(R.string.weblogin_url), false) - ) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()) - ) + private fun launchServerSelection() { + if (isBrandingUrlSet()) { + val intent = Intent(context, WebViewLoginActivity::class.java) + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_BASE_URL, resources.getString(R.string.weblogin_url)) + intent.putExtras(bundle) + startActivity(intent) } else { - router!!.setRoot( - RouterTransaction.with(ServerSelectionController()) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()) - ) + val intent = Intent(context, ServerSelectionActivity::class.java) + startActivity(intent) } } + private fun isBrandingUrlSet() = !TextUtils.isEmpty(resources.getString(R.string.weblogin_url)) + override fun onStart() { Log.d(TAG, "onStart: Activity: " + System.identityHashCode(this).toString()) super.onStart() - logRouterBackStack(router!!) } override fun onResume() { @@ -178,14 +161,6 @@ class MainActivity : BaseActivity(), ActionBarProvider { startActivity(intent) } - fun addAccount() { - router!!.pushController( - RouterTransaction.with(ServerSelectionController()) - .pushChangeHandler(VerticalChangeHandler()) - .popChangeHandler(VerticalChangeHandler()) - ) - } - private fun handleActionFromContact(intent: Intent) { if (intent.action == Intent.ACTION_VIEW && intent.data != null) { val cursor = contentResolver.query(intent.data!!, null, null, null, null) @@ -209,7 +184,7 @@ class MainActivity : BaseActivity(), ActionBarProvider { startConversation(user) } else { Snackbar.make( - binding.controllerContainer, + binding.root, R.string.nc_phone_book_integration_account_not_found, Snackbar.LENGTH_LONG ).show() @@ -283,28 +258,18 @@ class MainActivity : BaseActivity(), ActionBarProvider { } if (user != null && userManager.setUserAsActive(user).blockingGet()) { - // this should be avoided (it's still from conductor architecture). activities should be opened directly. if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { if (intent.getBooleanExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false)) { - if (!router!!.hasRootController()) { - openConversationList() - } val callNotificationIntent = Intent(this, CallNotificationActivity::class.java) intent.extras?.let { callNotificationIntent.putExtras(it) } startActivity(callNotificationIntent) } else { - logRouterBackStack(router!!) - val chatIntent = Intent(context, ChatActivity::class.java) chatIntent.putExtras(intent.extras!!) startActivity(chatIntent) - - logRouterBackStack(router!!) } } - } else if (intent.hasExtra(ADD_ACCOUNT) && intent.getBooleanExtra(ADD_ACCOUNT, false)) { - addAccount() - } else if (!router!!.hasRootController()) { + } else { if (!appPreferences.isDbRoomMigrated) { appPreferences.isDbRoomMigrated = true } @@ -316,36 +281,30 @@ class MainActivity : BaseActivity(), ActionBarProvider { override fun onSuccess(users: List) { if (users.isNotEmpty()) { + ClosedInterfaceImpl().setUpPushTokenRegistration() runOnUiThread { openConversationList() } } else { runOnUiThread { - launchLoginScreen() + launchServerSelection() } } } override fun onError(e: Throwable) { Log.e(TAG, "Error loading existing users", e) + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_SHORT + ).show() } }) } } - private fun logRouterBackStack(router: Router) { - if (BuildConfig.DEBUG) { - val backstack = router.backstack - var routerTransaction: RouterTransaction? - Log.d(TAG, " backstack size: " + router.backstackSize) - for (i in 0 until router.backstackSize) { - routerTransaction = backstack[i] - Log.d(TAG, " controller: " + routerTransaction.controller) - } - } - } - companion object { - private const val TAG = "MainActivity" + private val TAG = MainActivity::class.java.simpleName } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt index 4906bb7bc..3dbf39f16 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt @@ -203,9 +203,7 @@ class ConversationItem( } } - private fun shouldLoadAvatar( - holder: ConversationItemViewHolder - ): Boolean { + private fun shouldLoadAvatar(holder: ConversationItemViewHolder): Boolean { return when (model.objectType) { Conversation.ObjectType.SHARE_PASSWORD -> { holder.binding.dialogAvatar.setImageDrawable( @@ -237,10 +235,7 @@ class ConversationItem( } } - private fun setLastMessage( - holder: ConversationItemViewHolder, - appContext: Context - ) { + private fun setLastMessage(holder: ConversationItemViewHolder, appContext: Context) { if (model.lastMessage != null) { holder.binding.dialogDate.visibility = View.VISIBLE holder.binding.dialogDate.text = DateUtils.getRelativeTimeSpanString( diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt index c9384ce2a..ff989d160 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt @@ -95,10 +95,11 @@ data class MessageResultItem constructor( const val VIEW_TYPE = FlexibleItemViewType.MESSAGE_RESULT_ITEM } - override fun getHeader(): GenericTextHeaderItem = MessagesTextHeaderItem(context, viewThemeUtils) - .apply { - isHidden = showHeader // FlexibleAdapter needs this hack for some reason - } + override fun getHeader(): GenericTextHeaderItem = + MessagesTextHeaderItem(context, viewThemeUtils) + .apply { + isHidden = showHeader // FlexibleAdapter needs this hack for some reason + } override fun setHeader(header: GenericTextHeaderItem?) { // nothing, header is always the same diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt index c6154ddaa..8aa0665cf 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt @@ -124,6 +124,6 @@ class CallStartedViewHolder(incomingView: View, payload: Any) : } companion object { - var TAG: String? = CallStartedViewHolder::class.simpleName + val TAG: String? = CallStartedViewHolder::class.simpleName } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt index 15364af13..148675646 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt @@ -231,6 +231,6 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : } companion object { - private val TAG = NextcloudTalkApplication::class.java.simpleName + private val TAG = IncomingPollMessageViewHolder::class.java.simpleName } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java index 507944ce6..06e7084af 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java @@ -24,6 +24,8 @@ package com.nextcloud.talk.adapters.messages; +import android.text.Spanned; +import android.util.TypedValue; import android.view.View; import android.widget.ImageView; import android.widget.ProgressBar; @@ -33,6 +35,10 @@ import com.nextcloud.talk.R; import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding; import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding; import com.nextcloud.talk.models.json.chat.ChatMessage; +import com.nextcloud.talk.utils.TextMatchers; + +import java.util.HashMap; +import java.util.Objects; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; @@ -49,7 +55,49 @@ public class IncomingPreviewMessageViewHolder extends PreviewMessageViewHolder { @Override public void onBind(@NonNull ChatMessage message) { super.onBind(message); + if(!message.isVoiceMessage() + && !Objects.equals(message.getMessage(), "{file}") + ) { + Spanned processedMessageText = null; + binding.incomingPreviewMessageBubble.setBackgroundResource(R.drawable.shape_grouped_incoming_message); + if (viewThemeUtils != null ) { + processedMessageText = messageUtils.enrichChatMessageText( + binding.messageCaption.getContext(), + message, + true, + viewThemeUtils); + viewThemeUtils.talk.themeIncomingMessageBubble(binding.incomingPreviewMessageBubble, true, false); + } + if (processedMessageText != null) { + processedMessageText = messageUtils.processMessageParameters( + binding.messageCaption.getContext(), + viewThemeUtils, + processedMessageText, + message, + binding.incomingPreviewMessageBubble); + } + binding.incomingPreviewMessageBubble.setOnClickListener(null); + + float textSize = 0; + if (context != null) { + textSize = context.getResources().getDimension(R.dimen.chat_text_size); + } + HashMap> messageParameters = message.getMessageParameters(); + if ( + (messageParameters == null || messageParameters.size() <= 0) && + TextMatchers.isMessageWithSingleEmoticonOnly(message.getText()) + ) { + textSize = (float) (textSize * IncomingTextMessageViewHolder.TEXT_SIZE_MULTIPLIER); + itemView.setSelected(true); + } + binding.messageCaption.setVisibility(View.VISIBLE); + binding.messageCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + binding.messageCaption.setText(processedMessageText); + } else { + binding.incomingPreviewMessageBubble.setBackground(null); + binding.messageCaption.setVisibility(View.GONE); + } binding.messageAuthor.setText(message.getActorDisplayName()); binding.messageText.setTextColor(ContextCompat.getColor(binding.messageText.getContext(), R.color.no_emphasis_text)); @@ -63,6 +111,12 @@ public class IncomingPreviewMessageViewHolder extends PreviewMessageViewHolder { return binding.messageText; } + @NonNull + @Override + public EmojiTextView getMessageCaption() { + return binding.messageCaption; + } + @Override public ProgressBar getProgressBar() { return binding.progressBar; @@ -99,5 +153,4 @@ public class IncomingPreviewMessageViewHolder extends PreviewMessageViewHolder { @Override public ReactionsInsideMessageBinding getReactionsBinding(){ return binding.reactions; } - } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt index 53b89be85..d13a69fba 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt @@ -89,6 +89,15 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : this.message = message sharedApplication!!.componentApplication.inject(this) + val filename = message.selectedIndividualHashMap!!["name"] + val retrieved = appPreferences!!.getWaveFormFromFile(filename) + if (retrieved.isNotEmpty() && + message.voiceMessageFloatArray == null || + message.voiceMessageFloatArray?.isEmpty() == true + ) { + message.voiceMessageFloatArray = retrieved.toFloatArray() + binding.seekbar.setWaveData(message.voiceMessageFloatArray!!) + } binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) setAvatarAndAuthorOnMessageItem(message) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt index cfba665d5..8ddd8a52e 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt @@ -38,12 +38,7 @@ import io.reactivex.schedulers.Schedulers class LinkPreview { - fun showLink( - message: ChatMessage, - ncApi: NcApi, - binding: ReferenceInsideMessageBinding, - context: Context - ) { + fun showLink(message: ChatMessage, ncApi: NcApi, binding: ReferenceInsideMessageBinding, context: Context) { binding.referenceName.text = "" binding.referenceDescription.text = "" binding.referenceLink.text = "" diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt index f850c82de..dcc0ca7c7 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt @@ -209,6 +209,6 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : } companion object { - private val TAG = NextcloudTalkApplication::class.java.simpleName + private val TAG = OutcomingPollMessageViewHolder::class.java.simpleName } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java index c6f746427..618d757b2 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java @@ -22,6 +22,8 @@ package com.nextcloud.talk.adapters.messages; +import android.text.Spanned; +import android.util.TypedValue; import android.view.View; import android.widget.ImageView; import android.widget.ProgressBar; @@ -31,7 +33,12 @@ import com.nextcloud.talk.R; import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding; import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding; import com.nextcloud.talk.models.json.chat.ChatMessage; +import com.nextcloud.talk.utils.TextMatchers; +import java.util.HashMap; +import java.util.Objects; + +import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.emoji2.widget.EmojiTextView; @@ -45,8 +52,51 @@ public class OutcomingPreviewMessageViewHolder extends PreviewMessageViewHolder } @Override - public void onBind(ChatMessage message) { + public void onBind(@NonNull ChatMessage message) { super.onBind(message); + if(!message.isVoiceMessage() + && !Objects.equals(message.getMessage(), "{file}") + ) { + Spanned processedMessageText = null; + binding.outgoingPreviewMessageBubble.setBackgroundResource(R.drawable.shape_grouped_outcoming_message); + if (viewThemeUtils != null) { + processedMessageText = messageUtils.enrichChatMessageText( + binding.messageCaption.getContext(), + message, + false, + viewThemeUtils); + viewThemeUtils.talk.themeOutgoingMessageBubble(binding.outgoingPreviewMessageBubble, true, false); + } + + if (processedMessageText != null) { + processedMessageText = messageUtils.processMessageParameters( + binding.messageCaption.getContext(), + viewThemeUtils, + processedMessageText, + message, + binding.outgoingPreviewMessageBubble); + } + binding.outgoingPreviewMessageBubble.setOnClickListener(null); + + float textSize = 0; + if (context != null) { + textSize = context.getResources().getDimension(R.dimen.chat_text_size); + } + HashMap> messageParameters = message.getMessageParameters(); + if ( + (messageParameters == null || messageParameters.size() <= 0) && + TextMatchers.isMessageWithSingleEmoticonOnly(message.getText()) + ) { + textSize = (float)(textSize * IncomingTextMessageViewHolder.TEXT_SIZE_MULTIPLIER); + itemView.setSelected(true); + } + binding.messageCaption.setVisibility(View.VISIBLE); + binding.messageCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + binding.messageCaption.setText(processedMessageText); + } else { + binding.outgoingPreviewMessageBubble.setBackground(null); + binding.messageCaption.setVisibility(View.GONE); + } binding.messageText.setTextColor(ContextCompat.getColor(binding.messageText.getContext(), R.color.no_emphasis_text)); @@ -54,6 +104,7 @@ public class OutcomingPreviewMessageViewHolder extends PreviewMessageViewHolder R.color.no_emphasis_text)); } + @NonNull @Override public EmojiTextView getMessageText() { return binding.messageText; @@ -64,21 +115,25 @@ public class OutcomingPreviewMessageViewHolder extends PreviewMessageViewHolder return binding.progressBar; } + @NonNull @Override public View getPreviewContainer() { return binding.previewContainer; } + @NonNull @Override public MaterialCardView getPreviewContactContainer() { return binding.contactContainer; } + @NonNull @Override public ImageView getPreviewContactPhoto() { return binding.contactPhoto; } + @NonNull @Override public EmojiTextView getPreviewContactName() { return binding.contactName; @@ -91,4 +146,8 @@ public class OutcomingPreviewMessageViewHolder extends PreviewMessageViewHolder @Override public ReactionsInsideMessageBinding getReactionsBinding() { return binding.reactions; } + + @NonNull + @Override + public EmojiTextView getMessageCaption() { return binding.messageCaption; } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt index 2fe5ff6af..1effa576a 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt @@ -86,6 +86,17 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : this.message = message sharedApplication!!.componentApplication.inject(this) viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT) + + val filename = message.selectedIndividualHashMap!!["name"] + val retrieved = appPreferences!!.getWaveFormFromFile(filename) + if (retrieved.isNotEmpty() && + message.voiceMessageFloatArray == null || + message.voiceMessageFloatArray?.isEmpty() == true + ) { + message.voiceMessageFloatArray = retrieved.toFloatArray() + binding.seekbar.setWaveData(message.voiceMessageFloatArray!!) + } + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) colorizeMessageBubble(message) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt index edc737866..6674240fb 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt @@ -51,11 +51,13 @@ import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding import com.nextcloud.talk.extensions.loadChangelogBotAvatar import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType import com.nextcloud.talk.utils.FileViewerUtils import com.nextcloud.talk.utils.FileViewerUtils.ProgressUi +import com.nextcloud.talk.utils.message.MessageUtils import com.stfalcon.chatkit.messages.MessageHolders.IncomingImageMessageViewHolder import io.reactivex.Single import io.reactivex.SingleObserver @@ -80,6 +82,12 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : @Inject lateinit var dateUtils: DateUtils + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var userManager: UserManager + @JvmField @Inject var okHttpClient: OkHttpClient? = null @@ -111,6 +119,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) { fileViewerUtils = FileViewerUtils(context!!, message.activeUser!!) val fileName = message.selectedIndividualHashMap!![KEY_NAME] + messageText.text = fileName if (message.activeUser != null && @@ -123,7 +132,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : ProgressUi(progressBar, messageText, image) ) } - clickView!!.setOnLongClickListener { l: View? -> + clickView!!.setOnLongClickListener { previewMessageInterface!!.onPreviewMessageLongClick(message) true } @@ -188,6 +197,12 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : } } } + + messageCaption.setOnClickListener(null) + messageCaption.setOnLongClickListener { + previewMessageInterface!!.onPreviewMessageLongClick(message) + true + } } private fun longClickOnReaction(chatMessage: ChatMessage) { @@ -312,6 +327,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : } abstract val messageText: EmojiTextView + abstract val messageCaption: EmojiTextView abstract val previewContainer: View abstract val previewContactContainer: MaterialCardView abstract val previewContactPhoto: ImageView diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 7e8f3451a..674443ee6 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -49,6 +49,7 @@ import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchOverall; import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall; import com.nextcloud.talk.models.json.userprofile.UserProfileOverall; import com.nextcloud.talk.polls.repositories.model.PollOverall; +import com.nextcloud.talk.translate.repositories.model.LanguagesOverall; import com.nextcloud.talk.translate.repositories.model.TranslationsOverall; import java.util.List; @@ -56,6 +57,7 @@ import java.util.Map; import androidx.annotation.Nullable; import io.reactivex.Observable; +import kotlin.Unit; import okhttp3.MultipartBody; import okhttp3.RequestBody; import okhttp3.ResponseBody; @@ -332,7 +334,7 @@ public interface NcApi { @FormUrlEncoded @POST - Observable registerDeviceForNotificationsWithPushProxy(@Url String url, + Observable registerDeviceForNotificationsWithPushProxy(@Url String url, @FieldMap Map fields); @@ -675,6 +677,10 @@ public interface NcApi { @Query("toLanguage") String toLanguage, @Nullable @Query("fromLanguage") String fromLanguage); + @GET + Observable getLanguages(@Header("Authorization") String authorization, + @Url String url); + @GET Observable getReminder(@Header("Authorization") String authorization, @Url String url); diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/BasicListItemWithImage.kt b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/BasicListItemWithImage.kt similarity index 95% rename from app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/BasicListItemWithImage.kt rename to app/src/main/java/com/nextcloud/talk/bottomsheet/items/BasicListItemWithImage.kt index 80399135b..247494f0b 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/BasicListItemWithImage.kt +++ b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/BasicListItemWithImage.kt @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.controllers.bottomsheet.items +package com.nextcloud.talk.bottomsheet.items import android.widget.ImageView import androidx.annotation.DrawableRes diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/MagicBottomSheets.kt b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/BottomSheets.kt similarity index 97% rename from app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/MagicBottomSheets.kt rename to app/src/main/java/com/nextcloud/talk/bottomsheet/items/BottomSheets.kt index ede849cdd..fa11981de 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/MagicBottomSheets.kt +++ b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/BottomSheets.kt @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.controllers.bottomsheet.items +package com.nextcloud.talk.bottomsheet.items import androidx.annotation.CheckResult import androidx.recyclerview.widget.LinearLayoutManager diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/ListIconDialogAdapter.kt b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/ListIconDialogAdapter.kt similarity index 93% rename from app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/ListIconDialogAdapter.kt rename to app/src/main/java/com/nextcloud/talk/bottomsheet/items/ListIconDialogAdapter.kt index 075cae600..434df1b23 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/ListIconDialogAdapter.kt +++ b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/ListIconDialogAdapter.kt @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.controllers.bottomsheet.items +package com.nextcloud.talk.bottomsheet.items import android.view.View import android.view.ViewGroup @@ -79,10 +79,7 @@ internal class ListIconDialogAdapter( } } - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): ListItemViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemViewHolder { val listItemView: View = parent.inflate(dialog.windowContext, R.layout.menu_item_sheet) val viewHolder = ListItemViewHolder( itemView = listItemView, @@ -94,10 +91,7 @@ internal class ListIconDialogAdapter( override fun getItemCount() = items.size - override fun onBindViewHolder( - holder: ListItemViewHolder, - position: Int - ) { + override fun onBindViewHolder(holder: ListItemViewHolder, position: Int) { holder.itemView.isEnabled = !disabledIndices.contains(position) val currentItem = items[position] @@ -120,10 +114,7 @@ internal class ListIconDialogAdapter( } } - override fun replaceItems( - items: List, - listener: ListItemListener - ) { + override fun replaceItems(items: List, listener: ListItemListener) { this.items = items if (listener != null) { this.selection = listener diff --git a/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt b/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt index 336cbced2..e4228d0b2 100644 --- a/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt +++ b/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt @@ -46,10 +46,7 @@ class ReactionAnimator( ) { private val reactionsList: MutableList = ArrayList() - fun addReaction( - emoji: String, - displayName: String - ) { + fun addReaction(emoji: String, displayName: String) { val callReaction = CallReaction(emoji, displayName) reactionsList.add(callReaction) @@ -58,9 +55,7 @@ class ReactionAnimator( } } - private fun animateReaction( - callReaction: CallReaction - ) { + private fun animateReaction(callReaction: CallReaction) { val reactionWrapper = getReactionWrapperView(callReaction) val params = RelativeLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, diff --git a/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt index eb5da933a..5d88fdc64 100644 --- a/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt @@ -317,7 +317,7 @@ class CallNotificationActivity : CallBaseActivity() { } override fun suppressFitsSystemWindows() { - binding!!.controllerCallNotificationLayout.fitsSystemWindows = false + binding!!.callNotificationLayout.fitsSystemWindows = false } companion object { diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 10a69b2ff..ee3ea6962 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -34,7 +34,6 @@ import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager import android.content.res.AssetFileDescriptor @@ -86,7 +85,6 @@ import android.widget.RelativeLayout.LayoutParams import android.widget.SeekBar import android.widget.TextView import androidx.activity.OnBackPressedCallback -import androidx.appcompat.app.AlertDialog import androidx.appcompat.view.ContextThemeWrapper import androidx.core.content.ContextCompat import androidx.core.content.FileProvider @@ -116,7 +114,6 @@ import coil.target.Target import coil.transform.CircleCropTransformation import com.google.android.flexbox.FlexboxLayout import com.google.android.material.button.MaterialButton -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.BuildConfig @@ -159,7 +156,6 @@ import com.nextcloud.talk.events.UserMentionClickEvent import com.nextcloud.talk.events.WebSocketCommunicationEvent import com.nextcloud.talk.extensions.loadAvatarOrImagePreview import com.nextcloud.talk.jobs.DownloadFileToCacheWorker -import com.nextcloud.talk.jobs.SaveFileToStorageWorker import com.nextcloud.talk.jobs.ShareOperationWorker import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.location.LocationPickerActivity @@ -192,7 +188,9 @@ import com.nextcloud.talk.ui.StatusDrawable import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet import com.nextcloud.talk.ui.dialog.AttachmentDialog import com.nextcloud.talk.ui.dialog.DateTimePickerFragment +import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment import com.nextcloud.talk.ui.dialog.MessageActionsDialog +import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment import com.nextcloud.talk.ui.dialog.ShowReactionsDialog import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback @@ -261,6 +259,8 @@ import java.util.Objects import java.util.concurrent.ExecutionException import javax.inject.Inject import kotlin.collections.set +import kotlin.math.abs +import kotlin.math.log10 import kotlin.math.roundToInt @AutoInjector(NextcloudTalkApplication::class) @@ -342,17 +342,32 @@ class ChatActivity : private val filesToUpload: MutableList = ArrayList() private lateinit var sharedText: String - var isVoiceRecordingInProgress: Boolean = false var currentVoiceRecordFile: String = "" var isVoiceRecordingLocked: Boolean = false private var isVoicePreviewPlaying: Boolean = false + private var recorder: MediaRecorder? = null + private enum class MediaRecorderState { + INITIAL, + INITIALIZED, + CONFIGURED, + PREPARED, + RECORDING, + RELEASED, + ERROR + } + private var mediaRecorderState: MediaRecorderState = MediaRecorderState.INITIAL + private var voicePreviewMediaPlayer: MediaPlayer? = null private var voicePreviewObjectAnimator: ObjectAnimator? = null + var mediaPlayer: MediaPlayer? = null lateinit var mediaPlayerHandler: Handler + private var isEmojiPickerVisible = false + private var currentlyPlayedVoiceMessage: ChatMessage? = null + private lateinit var micInputAudioRecorder: AudioRecord private var micInputAudioRecordThread: Thread? = null private var isMicInputAudioThreadRunning: Boolean = false @@ -361,7 +376,9 @@ class ChatActivity : AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT ) + private var voiceRecordDuration = 0L + private var voiceRecordPauseTime = 0L // messy workaround for a mediaPlayer bug, don't delete private var lastRecordMediaPosition: Int = 0 @@ -519,7 +536,7 @@ class ChatActivity : if (isMicInputAudioThreadRunning) { stopMicInputRecordingAnimation() } - if (isVoiceRecordingInProgress) { + if (mediaRecorderState == MediaRecorderState.RECORDING) { stopAudioRecording() } if (currentlyPlayedVoiceMessage != null) { @@ -894,7 +911,12 @@ class ChatActivity : if (message.isPlayingVoiceMessage) { pausePlayback(message) } else { - setUpWaveform(message) + val retrieved = appPreferences.getWaveFormFromFile(filename) + if (retrieved.isEmpty()) { + setUpWaveform(message) + } else { + startPlayback(message) + } } } else { Log.d(TAG, "Downloaded to cache") @@ -913,6 +935,7 @@ class ChatActivity : adapter?.update(message) CoroutineScope(Dispatchers.Default).launch { val r = AudioUtils.audioFileToFloatArray(file) + appPreferences.saveWaveFormForFile(filename, r.toTypedArray()) message.voiceMessageFloatArray = r withContext(Dispatchers.Main) { startPlayback(message) @@ -925,7 +948,7 @@ class ChatActivity : private fun initMessageHolders(): MessageHolders { val messageHolders = MessageHolders() - val profileBottomSheet = ProfileBottomSheet(ncApi, conversationUser!!) + val profileBottomSheet = ProfileBottomSheet(ncApi, conversationUser!!, viewThemeUtils) val payload = MessagePayload( roomToken, @@ -1037,10 +1060,13 @@ class ChatActivity : } else { showMicrophoneButton(true) } - } else if (isVoiceRecordingInProgress) { + } else if (mediaRecorderState == MediaRecorderState.RECORDING) { binding.messageInputView.playPauseBtn.visibility = View.GONE binding.messageInputView.seekBar.visibility = View.GONE } else { + showVoiceRecordingLockedInterface(true) + showPreviewVoiceRecording(true) + stopMicInputRecordingAnimation() binding.messageInputView.micInputCloud.setState(MicInputCloud.ViewState.PAUSED_STATE) } @@ -1061,57 +1087,22 @@ class ChatActivity : var voiceRecordStartTime = 0L var voiceRecordEndTime = 0L - var voiceRecordPauseTime = 0L - val micInputCloudLayoutParams: LayoutParams = binding.messageInputView.micInputCloud - .layoutParams as LayoutParams - - val deleteVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.deleteVoiceRecording - .layoutParams as LayoutParams - - val sendVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.sendVoiceRecording - .layoutParams as LayoutParams // this is so that the seekbar is no longer draggable binding.messageInputView.seekBar.setOnTouchListener(OnTouchListener { _, _ -> true }) binding.messageInputView.micInputCloud.setOnClickListener { - if (isVoiceRecordingInProgress) { + if (mediaRecorderState == MediaRecorderState.RECORDING) { recorder?.stop() + mediaRecorderState = MediaRecorderState.INITIAL stopMicInputRecordingAnimation() - voiceRecordPauseTime = binding.messageInputView.audioRecordDuration.base - SystemClock.elapsedRealtime() - binding.messageInputView.audioRecordDuration.stop() - binding.messageInputView.audioRecordDuration.visibility = View.GONE - binding.messageInputView.playPauseBtn.visibility = View.VISIBLE - binding.messageInputView.playPauseBtn.icon = ContextCompat.getDrawable( - context, - R.drawable.ic_baseline_play_arrow_voice_message_24 - ) - binding.messageInputView.seekBar.visibility = View.VISIBLE - binding.messageInputView.seekBar.progress = 0 - binding.messageInputView.seekBar.max = 0 - micInputCloudLayoutParams.removeRule(BELOW) - micInputCloudLayoutParams.addRule(BELOW, R.id.voice_preview_container) - deleteVoiceRecordingLayoutParams.removeRule(BELOW) - deleteVoiceRecordingLayoutParams.addRule(BELOW, R.id.voice_preview_container) - sendVoiceRecordingLayoutParams.removeRule(BELOW) - sendVoiceRecordingLayoutParams.addRule(BELOW, R.id.voice_preview_container) + showPreviewVoiceRecording(true) } else { - restartAudio() + stopPreviewVoicePlaying() + initMediaRecorder(currentVoiceRecordFile) startMicInputRecordingAnimation() - binding.messageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime() - binding.messageInputView.audioRecordDuration.start() - binding.messageInputView.playPauseBtn.visibility = View.GONE - binding.messageInputView.seekBar.visibility = View.GONE - binding.messageInputView.audioRecordDuration.visibility = View.VISIBLE - micInputCloudLayoutParams.removeRule(BELOW) - micInputCloudLayoutParams.addRule(BELOW, R.id.audioRecordDuration) - deleteVoiceRecordingLayoutParams.removeRule(BELOW) - deleteVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration) - sendVoiceRecordingLayoutParams.removeRule(BELOW) - sendVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration) + showPreviewVoiceRecording(false) } - - isVoiceRecordingInProgress = !isVoiceRecordingInProgress } binding.messageInputView.deleteVoiceRecording.setOnClickListener { @@ -1176,7 +1167,7 @@ class ChatActivity : MotionEvent.ACTION_CANCEL -> { Log.d(TAG, "ACTION_CANCEL. same as for UP") - if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) { + if (mediaRecorderState != MediaRecorderState.RECORDING || !isRecordAudioPermissionGranted()) { return true } @@ -1187,7 +1178,7 @@ class ChatActivity : MotionEvent.ACTION_UP -> { Log.d(TAG, "ACTION_UP. stop recording??") - if (!isVoiceRecordingInProgress || + if (mediaRecorderState != MediaRecorderState.RECORDING || !isRecordAudioPermissionGranted() || isVoiceRecordingLocked ) { @@ -1218,7 +1209,7 @@ class ChatActivity : MotionEvent.ACTION_MOVE -> { Log.d(TAG, "ACTION_MOVE.") - if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) { + if (mediaRecorderState != MediaRecorderState.RECORDING || !isRecordAudioPermissionGranted()) { return true } @@ -1235,6 +1226,7 @@ class ChatActivity : isVoiceRecordingLocked = true showVoiceRecordingLocked(true) showVoiceRecordingLockedInterface(true) + startMicInputRecordingAnimation() } else if (deltaY < 0f) { binding.voiceRecordingLock.translationY = deltaY } @@ -1271,9 +1263,51 @@ class ChatActivity : }) } + private fun showPreviewVoiceRecording(value: Boolean) { + val micInputCloudLayoutParams: LayoutParams = binding.messageInputView.micInputCloud + .layoutParams as LayoutParams + + val deleteVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.deleteVoiceRecording + .layoutParams as LayoutParams + + val sendVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.sendVoiceRecording + .layoutParams as LayoutParams + + if (value) { + voiceRecordPauseTime = binding.messageInputView.audioRecordDuration.base - SystemClock.elapsedRealtime() + binding.messageInputView.audioRecordDuration.stop() + binding.messageInputView.audioRecordDuration.visibility = View.GONE + binding.messageInputView.playPauseBtn.visibility = View.VISIBLE + binding.messageInputView.playPauseBtn.icon = ContextCompat.getDrawable( + context, + R.drawable.ic_baseline_play_arrow_voice_message_24 + ) + binding.messageInputView.seekBar.visibility = View.VISIBLE + binding.messageInputView.seekBar.progress = 0 + binding.messageInputView.seekBar.max = 0 + micInputCloudLayoutParams.removeRule(BELOW) + micInputCloudLayoutParams.addRule(BELOW, R.id.voice_preview_container) + deleteVoiceRecordingLayoutParams.removeRule(BELOW) + deleteVoiceRecordingLayoutParams.addRule(BELOW, R.id.voice_preview_container) + sendVoiceRecordingLayoutParams.removeRule(BELOW) + sendVoiceRecordingLayoutParams.addRule(BELOW, R.id.voice_preview_container) + } else { + binding.messageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime() + binding.messageInputView.audioRecordDuration.start() + binding.messageInputView.playPauseBtn.visibility = View.GONE + binding.messageInputView.seekBar.visibility = View.GONE + binding.messageInputView.audioRecordDuration.visibility = View.VISIBLE + micInputCloudLayoutParams.removeRule(BELOW) + micInputCloudLayoutParams.addRule(BELOW, R.id.audioRecordDuration) + deleteVoiceRecordingLayoutParams.removeRule(BELOW) + deleteVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration) + sendVoiceRecordingLayoutParams.removeRule(BELOW) + sendVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration) + } + } + private fun initPreviewVoiceRecording() { voicePreviewMediaPlayer = MediaPlayer().apply { - Log.e(TAG, currentVoiceRecordFile) setDataSource(currentVoiceRecordFile) prepare() setOnPreparedListener { @@ -1327,20 +1361,6 @@ class ChatActivity : } } - private fun restartAudio() { - recorder = MediaRecorder().apply { - setAudioSource(MediaRecorder.AudioSource.MIC) - setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) - setOutputFile(currentVoiceRecordFile) - setAudioEncoder(MediaRecorder.AudioEncoder.AAC) - setAudioSamplingRate(VOICE_MESSAGE_SAMPLING_RATE) - setAudioEncodingBitRate(VOICE_MESSAGE_ENCODING_BIT_RATE) - setAudioChannels(VOICE_MESSAGE_CHANNELS) - prepare() - start() - } - } - private fun endVoiceRecordingUI() { stopPreviewVoicePlaying() showRecordAudioUi(false) @@ -1348,6 +1368,7 @@ class ChatActivity : isVoiceRecordingLocked = false showVoiceRecordingLocked(false) showVoiceRecordingLockedInterface(false) + stopMicInputRecordingAnimation() } private fun showVoiceRecordingLocked(value: Boolean) { @@ -1408,10 +1429,7 @@ class ChatActivity : audioDurationLayoutParams.removeRule(RelativeLayout.END_OF) audioDurationLayoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, R.bool.value_true) audioDurationLayoutParams.setMargins(0, standardQuarterMargin, 0, 0) - startMicInputRecordingAnimation() - Log.d(TAG, "MicInputRecording Started") } else { - stopMicInputRecordingAnimation() binding.messageInputView.deleteVoiceRecording.visibility = View.GONE binding.messageInputView.micInputCloud.visibility = View.GONE binding.messageInputView.recordAudioButton.visibility = View.VISIBLE @@ -1615,7 +1633,7 @@ class ChatActivity : participantPermissions.hasChatPermission() && !isReadOnlyConversation() ) { - val messageSwipeController = MessageSwipeCallback( + val messageSwipeCallback = MessageSwipeCallback( this, object : MessageSwipeActions { override fun showReplyUI(position: Int) { @@ -1627,7 +1645,7 @@ class ChatActivity : } ) - val itemTouchHelper = ItemTouchHelper(messageSwipeController) + val itemTouchHelper = ItemTouchHelper(messageSwipeCallback) itemTouchHelper.attachToRecyclerView(binding.messagesListView) } } @@ -1705,14 +1723,17 @@ class ChatActivity : } } - fun isOneToOneConversation() = currentConversation != null && currentConversation?.type != null && - currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + fun isOneToOneConversation() = + currentConversation != null && currentConversation?.type != null && + currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL - private fun isGroupConversation() = currentConversation != null && currentConversation?.type != null && - currentConversation?.type == ConversationType.ROOM_GROUP_CALL + private fun isGroupConversation() = + currentConversation != null && currentConversation?.type != null && + currentConversation?.type == ConversationType.ROOM_GROUP_CALL - private fun isPublicConversation() = currentConversation != null && currentConversation?.type != null && - currentConversation?.type == ConversationType.ROOM_PUBLIC_CALL + private fun isPublicConversation() = + currentConversation != null && currentConversation?.type != null && + currentConversation?.type == ConversationType.ROOM_PUBLIC_CALL private fun switchToRoom(token: String, startCallAfterRoomSwitch: Boolean, isVoiceOnlyCall: Boolean) { if (conversationUser != null) { @@ -2028,44 +2049,6 @@ class ChatActivity : } } - @SuppressLint("LongLogTag") - private fun saveImageToStorage( - message: ChatMessage - ) { - message.openWhenDownloaded = false - adapter?.update(message) - - val fileName = message.selectedIndividualHashMap!!["name"] - val sourceFilePath = applicationContext.cacheDir.path - val fileId = message.selectedIndividualHashMap!!["id"] - - val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId!!) - try { - for (workInfo in workers.get()) { - if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { - Log.d(TAG, "SaveFileToStorageWorker for $fileId is already running or scheduled") - return - } - } - } catch (e: ExecutionException) { - Log.e(TAG, "Error when checking if worker already exists", e) - } catch (e: InterruptedException) { - Log.e(TAG, "Error when checking if worker already exists", e) - } - - val data: Data = Data.Builder() - .putString(SaveFileToStorageWorker.KEY_FILE_NAME, fileName) - .putString(SaveFileToStorageWorker.KEY_SOURCE_FILE_PATH, "$sourceFilePath/$fileName") - .build() - - val saveWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SaveFileToStorageWorker::class.java) - .setInputData(data) - .addTag(fileId) - .build() - - WorkManager.getInstance().enqueue(saveWorker) - } - @SuppressLint("SimpleDateFormat") private fun setVoiceRecordFileName() { val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN) @@ -2123,36 +2106,41 @@ class ChatActivity : ) isMicInputAudioThreadRunning = true micInputAudioRecorder.startRecording() - micInputAudioRecordThread = Thread( - Runnable { - while (isMicInputAudioThreadRunning) { - val byteArr = ByteArray(bufferSize / 2) - micInputAudioRecorder.read(byteArr, 0, byteArr.size) - val d = Math.abs(byteArr[0].toDouble()) - if (d > AUDIO_VALUE_MAX) { - binding.messageInputView.micInputCloud.setRotationSpeed( - Math.log10(d).toFloat(), - MicInputCloud.MAXIMUM_RADIUS - ) - } else if (d > AUDIO_VALUE_MIN) { - binding.messageInputView.micInputCloud.setRotationSpeed( - Math.log10(d).toFloat(), - MicInputCloud.EXTENDED_RADIUS - ) - } else { - binding.messageInputView.micInputCloud.setRotationSpeed( - 1f, - MicInputCloud.DEFAULT_RADIUS - ) - } - Thread.sleep(AUDIO_VALUE_SLEEP) - } - } - ) + initMicInputAudioRecordThread() micInputAudioRecordThread!!.start() + binding.messageInputView.micInputCloud.startAnimators() } } + private fun initMicInputAudioRecordThread() { + micInputAudioRecordThread = Thread( + Runnable { + while (isMicInputAudioThreadRunning) { + val byteArr = ByteArray(bufferSize / 2) + micInputAudioRecorder.read(byteArr, 0, byteArr.size) + val d = abs(byteArr[0].toDouble()) + if (d > AUDIO_VALUE_MAX) { + binding.messageInputView.micInputCloud.setRotationSpeed( + log10(d).toFloat(), + MicInputCloud.MAXIMUM_RADIUS + ) + } else if (d > AUDIO_VALUE_MIN) { + binding.messageInputView.micInputCloud.setRotationSpeed( + log10(d).toFloat(), + MicInputCloud.EXTENDED_RADIUS + ) + } else { + binding.messageInputView.micInputCloud.setRotationSpeed( + 1f, + MicInputCloud.DEFAULT_RADIUS + ) + } + Thread.sleep(AUDIO_VALUE_SLEEP) + } + } + ) + } + private fun stopMicInputRecordingAnimation() { if (micInputAudioRecordThread != null) { Log.d(TAG, "Mic Animation Ended") @@ -2181,10 +2169,19 @@ class ChatActivity : animation.repeatMode = Animation.REVERSE binding.messageInputView.microphoneEnabledInfo.startAnimation(animation) + initMediaRecorder(file) + VibrationUtils.vibrateShort(context) + } + + private fun initMediaRecorder(file: String) { recorder = MediaRecorder().apply { setAudioSource(MediaRecorder.AudioSource.MIC) - setOutputFile(file) + mediaRecorderState = MediaRecorderState.INITIALIZED + setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) + mediaRecorderState = MediaRecorderState.CONFIGURED + + setOutputFile(file) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setAudioSamplingRate(VOICE_MESSAGE_SAMPLING_RATE) setAudioEncodingBitRate(VOICE_MESSAGE_ENCODING_BIT_RATE) @@ -2192,35 +2189,44 @@ class ChatActivity : try { prepare() + mediaRecorderState = MediaRecorderState.PREPARED } catch (e: IOException) { + mediaRecorderState = MediaRecorderState.ERROR Log.e(TAG, "prepare for audio recording failed") } try { start() + mediaRecorderState = MediaRecorderState.RECORDING Log.d(TAG, "recording started") - isVoiceRecordingInProgress = true } catch (e: IllegalStateException) { + mediaRecorderState = MediaRecorderState.ERROR Log.e(TAG, "start for audio recording failed") } - - VibrationUtils.vibrateShort(context) } } private fun stopAndSendAudioRecording() { stopAudioRecording() Log.d(TAG, "stopped and sent audio recording") - val uri = Uri.fromFile(File(currentVoiceRecordFile)) - uploadFile(uri.toString(), true) + + if (mediaRecorderState != MediaRecorderState.ERROR) { + val uri = Uri.fromFile(File(currentVoiceRecordFile)) + uploadFile(uri.toString(), true) + } else { + mediaRecorderState = MediaRecorderState.INITIAL + } } private fun stopAndDiscardAudioRecording() { stopAudioRecording() Log.d(TAG, "stopped and discarded audio recording") - val cachedFile = File(currentVoiceRecordFile) cachedFile.delete() + + if (mediaRecorderState == MediaRecorderState.ERROR) { + mediaRecorderState = MediaRecorderState.INITIAL + } } @Suppress("Detekt.TooGenericExceptionCaught") @@ -2230,17 +2236,22 @@ class ChatActivity : recorder?.apply { try { - Log.d(TAG, "recording stopped with $voiceRecordDuration") - if (voiceRecordDuration > MINIMUM_VOICE_RECORD_TO_STOP) { + if (mediaRecorderState == MediaRecorderState.RECORDING) { stop() + reset() + mediaRecorderState = MediaRecorderState.INITIAL + Log.d(TAG, "stopped recorder") } release() - isVoiceRecordingInProgress = false - Log.d(TAG, "stopped recorder. isVoiceRecordingInProgress = false") - } catch (e: java.lang.IllegalStateException) { - error("error while stopping recorder!" + e) - } catch (e: java.lang.RuntimeException) { - error("error while stopping recorder!" + e) + mediaRecorderState = MediaRecorderState.RELEASED + } catch (e: Exception) { + when (e) { + is java.lang.IllegalStateException, + is java.lang.RuntimeException -> { + mediaRecorderState = MediaRecorderState.ERROR + Log.e(TAG, "error while stopping recorder! with state $mediaRecorderState $e") + } + } } VibrationUtils.vibrateShort(context) @@ -2404,7 +2415,9 @@ class ChatActivity : } else { binding.lobby.lobbyView.visibility = View.GONE binding.messagesListView.visibility = View.VISIBLE - binding.messageInputView.inputEditText?.visibility = View.VISIBLE + if (!isVoiceRecordingLocked) { + binding.messageInputView.inputEditText?.visibility = View.VISIBLE + } } } @@ -2459,40 +2472,12 @@ class ChatActivity : filenamesWithLineBreaks.append(filename).append("\n") } - val confirmationQuestion = when (filesToUpload.size) { - 1 -> context.resources?.getString(R.string.nc_upload_confirm_send_single)?.let { - String.format(it, title.trim()) - } - - else -> context.resources?.getString(R.string.nc_upload_confirm_send_multiple)?.let { - String.format(it, title.trim()) - } - } - - binding.messageInputView.context?.let { - val materialAlertDialogBuilder = MaterialAlertDialogBuilder(it) - .setTitle(confirmationQuestion) - .setMessage(filenamesWithLineBreaks.toString()) - .setPositiveButton(R.string.nc_yes) { _, _ -> - if (permissionUtil.isFilesPermissionGranted()) { - uploadFiles(filesToUpload) - } else { - UploadAndShareFilesWorker.requestStoragePermission(this) - } - } - .setNegativeButton(R.string.nc_no) { _, _ -> - // unused atm - } - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(it, materialAlertDialogBuilder) - - val dialog = materialAlertDialogBuilder.show() - - viewThemeUtils.platform.colorTextButtons( - dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE) - ) - } + val newFragment: DialogFragment = FileAttachmentPreviewFragment.newInstance( + filenamesWithLineBreaks.toString(), + filesToUpload, + this::uploadFiles + ) + newFragment.show(supportFragmentManager, FileAttachmentPreviewFragment.TAG) } catch (e: IllegalStateException) { context.resources?.getString(R.string.nc_upload_failed)?.let { Snackbar.make( @@ -2554,7 +2539,19 @@ class ChatActivity : } if (permissionUtil.isFilesPermissionGranted()) { - uploadFiles(filesToUpload) + val filenamesWithLineBreaks = StringBuilder("\n") + + for (file in filesToUpload) { + val filename = FileUtils.getFileName(Uri.parse(file), context) + filenamesWithLineBreaks.append(filename).append("\n") + } + + val newFragment: DialogFragment = FileAttachmentPreviewFragment.newInstance( + filenamesWithLineBreaks.toString(), + filesToUpload, + this::uploadFiles + ) + newFragment.show(supportFragmentManager, FileAttachmentPreviewFragment.TAG) } else { UploadAndShareFilesWorker.requestStoragePermission(this) } @@ -2625,7 +2622,7 @@ class ChatActivity : super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Log.d(ConversationsListActivity.TAG, "upload starting after permissions were granted") + Log.d(TAG, "upload starting after permissions were granted") if (filesToUpload.isNotEmpty()) { uploadFiles(filesToUpload) } @@ -2678,13 +2675,17 @@ class ChatActivity : } } - private fun uploadFiles(files: MutableList) { - for (file in files) { - uploadFile(file, false) + private fun uploadFiles(files: MutableList, caption: String = "") { + for (i in 0 until files.size) { + if (i == files.size - 1) { + uploadFile(files[i], false, caption) + } else { + uploadFile(files[i], false) + } } } - private fun uploadFile(fileUri: String, isVoiceMessage: Boolean) { + private fun uploadFile(fileUri: String, isVoiceMessage: Boolean, caption: String = "") { var metaData = "" if (!participantPermissions.hasChatPermission()) { @@ -2696,6 +2697,10 @@ class ChatActivity : metaData = VOICE_MESSAGE_META_DATA } + if (caption != "") { + metaData = "{\"caption\":\"$caption\"}" + } + try { require(fileUri.isNotEmpty()) UploadAndShareFilesWorker.upload( @@ -2986,9 +2991,7 @@ class ChatActivity : } } - fun leaveRoom( - funToCallWhenLeaveSuccessful: (() -> Unit)? - ) { + fun leaveRoom(funToCallWhenLeaveSuccessful: (() -> Unit)?) { logConversationInfos("leaveRoom") var apiVersion = 1 @@ -3154,11 +3157,7 @@ class ChatActivity : signalingMessageSender = webSocketInstance?.signalingMessageSender } - fun pullChatMessages( - lookIntoFuture: Boolean, - setReadMarker: Boolean = true, - xChatLastCommonRead: Int? = null - ) { + fun pullChatMessages(lookIntoFuture: Boolean, setReadMarker: Boolean = true, xChatLastCommonRead: Int? = null) { if (!validSessionId()) { return } @@ -3258,30 +3257,7 @@ class ChatActivity : Integer.parseInt(it) } - try { - val mostRecentCallSystemMessage = adapter?.items?.first { - it.item is ChatMessage && - (it.item as ChatMessage).systemMessageType in - listOf( - ChatMessage.SystemMessageType.CALL_STARTED, - ChatMessage.SystemMessageType.CALL_JOINED, - ChatMessage.SystemMessageType.CALL_LEFT, - ChatMessage.SystemMessageType.CALL_ENDED, - ChatMessage.SystemMessageType.CALL_TRIED, - ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE, - ChatMessage.SystemMessageType.CALL_MISSED - ) - }?.item - - if (mostRecentCallSystemMessage != null) { - processMostRecentMessage( - mostRecentCallSystemMessage as ChatMessage, - chatMessageList - ) - } - } catch (e: java.util.NoSuchElementException) { - Log.d(TAG, "No System messages found $e") - } + processCallStartedMessages(chatMessageList) updateReadStatusOfAllMessages(newXChatLastCommonRead) adapter?.notifyDataSetChanged() @@ -3316,6 +3292,33 @@ class ChatActivity : }) } + private fun processCallStartedMessages(chatMessageList: List) { + try { + val mostRecentCallSystemMessage = adapter?.items?.first { + it.item is ChatMessage && + (it.item as ChatMessage).systemMessageType in + listOf( + ChatMessage.SystemMessageType.CALL_STARTED, + ChatMessage.SystemMessageType.CALL_JOINED, + ChatMessage.SystemMessageType.CALL_LEFT, + ChatMessage.SystemMessageType.CALL_ENDED, + ChatMessage.SystemMessageType.CALL_TRIED, + ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE, + ChatMessage.SystemMessageType.CALL_MISSED + ) + }?.item + + if (mostRecentCallSystemMessage != null) { + processMostRecentMessage( + mostRecentCallSystemMessage as ChatMessage, + chatMessageList + ) + } + } catch (e: NoSuchElementException) { + Log.d(TAG, "No System messages found $e") + } + } + private fun setupFieldsForPullChatMessages( lookIntoFuture: Boolean, xChatLastCommonRead: Int?, @@ -3474,10 +3477,7 @@ class ChatActivity : } } - private fun addMessagesToAdapter( - shouldAddNewMessagesNotice: Boolean, - chatMessageList: List - ) { + private fun addMessagesToAdapter(shouldAddNewMessagesNotice: Boolean, chatMessageList: List) { val isThereANewNotice = shouldAddNewMessagesNotice || adapter?.getMessagePositionByIdInReverse("-1") != -1 for (chatMessage in chatMessageList) { @@ -3730,19 +3730,15 @@ class ChatActivity : chatMessageMap[currentMessage.value.parentMessage!!.id]!!.isDeleted = true } chatMessageIterator.remove() - } - - // delete reactions system messages - else if (isReactionsMessage(currentMessage)) { + } else if (isReactionsMessage(currentMessage)) { + // delete reactions system messages if (!chatMessageMap.containsKey(currentMessage.value.parentMessage!!.id)) { updateAdapterForReaction(currentMessage.value.parentMessage) } chatMessageIterator.remove() - } - - // delete poll system messages - else if (isPollVotedMessage(currentMessage)) { + } else if (isPollVotedMessage(currentMessage)) { + // delete poll system messages chatMessageIterator.remove() } } @@ -4155,27 +4151,14 @@ class ChatActivity : } } - private fun saveImage(message: ChatMessage) { - if (permissionUtil.isFilesPermissionGranted()) { - saveImageToStorage(message) - } else { - UploadAndShareFilesWorker.requestStoragePermission(this@ChatActivity) - } - } - private fun showSaveToStorageWarning(message: ChatMessage) { - val builder = AlertDialog.Builder(this) - builder.setTitle(R.string.nc_dialog_save_to_storage_title) - builder.setMessage(R.string.nc_dialog_save_to_storage_content) - builder.setPositiveButton(R.string.nc_dialog_save_to_storage_yes) { dialog: DialogInterface, _: Int -> - saveImage(message) - dialog.dismiss() - } - builder.setNegativeButton(R.string.nc_dialog_save_to_storage_no) { dialog: DialogInterface, _: Int -> - dialog.dismiss() - } - val dialog = builder.create() - dialog.show() + val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance( + message.selectedIndividualHashMap!!["name"]!! + ) + saveFragment.show( + supportFragmentManager, + SaveToStorageDialogFragment.TAG + ) } fun checkIfSaveable(message: ChatMessage) { @@ -4616,5 +4599,6 @@ class ChatActivity : private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping" private const val CALL_STARTED_ID = -2 private const val MILISEC_15: Long = 15 + private const val LINEBREAK = "\n" } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt index 81327cf74..488aaece3 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt @@ -29,10 +29,7 @@ import com.nextcloud.talk.utils.ApiUtils import io.reactivex.Observable class ChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository { - override fun getRoom( - user: User, - roomToken: String - ): Observable { + override fun getRoom(user: User, roomToken: String): Observable { val credentials: String = ApiUtils.getCredentials(user.username, user.token) val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv3, 1)) @@ -42,11 +39,7 @@ class ChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository { ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } } - override fun joinRoom( - user: User, - roomToken: String, - roomPassword: String - ): Observable { + override fun joinRoom(user: User, roomToken: String, roomPassword: String): Observable { val credentials: String = ApiUtils.getCredentials(user.username, user.token) val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.APIv4, 1)) diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index 09074cab2..a6d9db80f 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -72,7 +72,7 @@ class ChatViewModel @Inject constructor(private val repository: ChatRepository) } fun joinRoom(user: User, token: String, roomPassword: String) { - _getRoomViewState.value = JoinRoomStartState + _joinRoomViewState.value = JoinRoomStartState repository.joinRoom(user, token, roomPassword) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt index a318d20d7..c32f748ff 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt @@ -488,17 +488,13 @@ class ContactsActivity : } else { adapter?.filterItems() } - - binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false } override fun onError(e: Throwable) { - binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false dispose(contactsQueryDisposable) } override fun onComplete() { - binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false dispose(contactsQueryDisposable) alreadyFetching = false disengageProgressBar() @@ -656,12 +652,9 @@ class ContactsActivity : private fun prepareViews() { layoutManager = SmoothScrollLinearLayoutManager(this) - binding.controllerGenericRv.recyclerView.layoutManager = layoutManager - binding.controllerGenericRv.recyclerView.setHasFixedSize(true) - binding.controllerGenericRv.recyclerView.adapter = adapter - binding.controllerGenericRv.swipeRefreshLayout.setOnRefreshListener { fetchData() } - - binding.controllerGenericRv.let { viewThemeUtils.androidx.themeSwipeRefreshLayout(it.swipeRefreshLayout) } + binding.contactsRv.layoutManager = layoutManager + binding.contactsRv.setHasFixedSize(true) + binding.contactsRv.adapter = adapter binding.listOpenConversationsImage.background?.setColorFilter( ResourcesCompat.getColor(resources!!, R.color.colorBackgroundDarker, null), @@ -677,7 +670,7 @@ class ContactsActivity : private fun disengageProgressBar() { if (!alreadyFetching) { binding.loadingContent.visibility = View.GONE - binding.controllerGenericRv.root.visibility = View.VISIBLE + binding.root.visibility = View.VISIBLE if (isNewConversationView) { binding.callHeaderLayout.visibility = View.VISIBLE } @@ -713,8 +706,6 @@ class ContactsActivity : adapter?.updateDataSet(contactItems as List?) } - binding.controllerGenericRv?.swipeRefreshLayout?.isEnabled = !adapter!!.hasFilter() - return true } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.kt b/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.kt deleted file mode 100644 index 2c5e81193..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.kt +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * @author Andy Scherzinger - * Copyright (C) 2022 Andy Scherzinger - * Copyright (C) 2017-2018 Mario Danic - * - * 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 . - */ -package com.nextcloud.talk.controllers - -import android.annotation.SuppressLint -import android.media.MediaPlayer -import android.media.RingtoneManager -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.text.TextUtils -import android.util.Log -import android.view.MenuItem -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import autodagger.AutoInjector -import com.bluelinelabs.logansquare.LoganSquare -import com.nextcloud.talk.R -import com.nextcloud.talk.adapters.items.NotificationSoundItem -import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication -import com.nextcloud.talk.controllers.base.BaseController -import com.nextcloud.talk.controllers.util.viewBinding -import com.nextcloud.talk.databinding.ControllerGenericRvBinding -import com.nextcloud.talk.models.RingtoneSettings -import com.nextcloud.talk.utils.NotificationUtils -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ARE_CALL_SOUNDS -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.SelectableAdapter -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import java.io.IOException - -@AutoInjector(NextcloudTalkApplication::class) -class RingtoneSelectionController(args: Bundle) : - BaseController( - R.layout.controller_generic_rv, - args - ), - FlexibleAdapter.OnItemClickListener { - private val binding: ControllerGenericRvBinding? by viewBinding(ControllerGenericRvBinding::bind) - - private var adapter: FlexibleAdapter<*>? = null - private var adapterDataObserver: RecyclerView.AdapterDataObserver? = null - private val abstractFlexibleItemList: MutableList> = ArrayList() - private val callNotificationSounds: Boolean - private var mediaPlayer: MediaPlayer? = null - private var cancelMediaPlayerHandler: Handler? = null - - override fun onViewBound(view: View) { - super.onViewBound(view) - if (adapter == null) { - adapter = FlexibleAdapter(abstractFlexibleItemList, activity, false) - adapter!!.setNotifyChangeOfUnfilteredItems(true).mode = SelectableAdapter.Mode.SINGLE - adapter!!.addListener(this) - cancelMediaPlayerHandler = Handler() - } - adapter!!.addListener(this) - prepareViews() - fetchNotificationSounds() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return if (item.itemId == android.R.id.home) { - router.popCurrentController() - } else { - super.onOptionsItemSelected(item) - } - } - - private fun prepareViews() { - val layoutManager: RecyclerView.LayoutManager = SmoothScrollLinearLayoutManager(activity) - binding?.recyclerView?.layoutManager = layoutManager - binding?.recyclerView?.setHasFixedSize(true) - binding?.recyclerView?.adapter = adapter - adapterDataObserver = object : RecyclerView.AdapterDataObserver() { - override fun onChanged() { - super.onChanged() - findSelectedSound() - } - } - adapter!!.registerAdapterDataObserver(adapterDataObserver!!) - binding?.swipeRefreshLayout?.isEnabled = false - } - - @SuppressLint("LongLogTag") - private fun findSelectedSound() { - var foundDefault = false - var preferencesString: String? = null - val callsEnabledButNoRingtone = callNotificationSounds && - TextUtils.isEmpty(appPreferences.callRingtoneUri.also { preferencesString = it }) - val noCallsAndNoMessageTone = !callNotificationSounds && - TextUtils.isEmpty(appPreferences.messageRingtoneUri.also { preferencesString = it }) - if (callsEnabledButNoRingtone || noCallsAndNoMessageTone) { - adapter!!.toggleSelection(1) - foundDefault = true - } - if (!TextUtils.isEmpty(preferencesString) && !foundDefault) { - try { - val ringtoneSettings: RingtoneSettings = - LoganSquare.parse(preferencesString, RingtoneSettings::class.java) - if (ringtoneSettings.ringtoneUri == null) { - adapter!!.toggleSelection(0) - } else if (ringtoneSettings.ringtoneUri!!.toString() == ringtoneString) { - adapter!!.toggleSelection(1) - } else { - var notificationSoundItem: NotificationSoundItem? - for (i in 2 until adapter!!.itemCount) { - notificationSoundItem = adapter!!.getItem(i) as NotificationSoundItem? - if ( - notificationSoundItem!!.notificationSoundUri == ringtoneSettings.ringtoneUri!!.toString() - ) { - adapter!!.toggleSelection(i) - break - } - } - } - } catch (e: IOException) { - Log.e(TAG, "Failed to parse ringtone settings") - } - } - adapter!!.unregisterAdapterDataObserver(adapterDataObserver!!) - adapterDataObserver = null - } - - private val ringtoneString: String - get() = if (callNotificationSounds) { - NotificationUtils.DEFAULT_CALL_RINGTONE_URI - } else { - NotificationUtils.DEFAULT_MESSAGE_RINGTONE_URI - } - - private fun fetchNotificationSounds() { - abstractFlexibleItemList.add( - NotificationSoundItem( - resources!!.getString(R.string.nc_settings_no_ringtone), - null - ) - ) - abstractFlexibleItemList.add( - NotificationSoundItem( - resources!!.getString(R.string.nc_settings_default_ringtone), - ringtoneString - ) - ) - if (activity != null) { - val manager = RingtoneManager(activity) - if (callNotificationSounds) { - manager.setType(RingtoneManager.TYPE_RINGTONE) - } else { - manager.setType(RingtoneManager.TYPE_NOTIFICATION) - } - val cursor = manager.cursor - var notificationSoundItem: NotificationSoundItem - while (cursor.moveToNext()) { - val notificationTitle = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX) - val notificationUri = cursor.getString(RingtoneManager.URI_COLUMN_INDEX) - val completeNotificationUri = notificationUri + "/" + cursor.getString(RingtoneManager.ID_COLUMN_INDEX) - notificationSoundItem = NotificationSoundItem(notificationTitle, completeNotificationUri) - abstractFlexibleItemList.add(notificationSoundItem) - } - } - adapter!!.updateDataSet(abstractFlexibleItemList as List?, false) - } - - override fun onItemClick(view: View, position: Int): Boolean { - val notificationSoundItem = adapter!!.getItem(position) as NotificationSoundItem? - var ringtoneUri: Uri? = null - if (!TextUtils.isEmpty(notificationSoundItem!!.notificationSoundUri)) { - ringtoneUri = Uri.parse(notificationSoundItem.notificationSoundUri) - endMediaPlayer() - mediaPlayer = MediaPlayer.create(activity, ringtoneUri) - cancelMediaPlayerHandler = Handler() - cancelMediaPlayerHandler!!.postDelayed( - { endMediaPlayer() }, - (mediaPlayer!!.duration + DURATION_EXTENSION).toLong() - ) - mediaPlayer!!.start() - } - if (adapter!!.selectedPositions.size == 0 || adapter!!.selectedPositions[0] != position) { - val ringtoneSettings = RingtoneSettings() - ringtoneSettings.ringtoneName = notificationSoundItem.notificationSoundName - ringtoneSettings.ringtoneUri = ringtoneUri - if (callNotificationSounds) { - try { - appPreferences!!.callRingtoneUri = LoganSquare.serialize(ringtoneSettings) - adapter!!.toggleSelection(position) - adapter!!.notifyDataSetChanged() - } catch (e: IOException) { - Log.e(TAG, "Failed to store selected ringtone for calls") - } - } else { - try { - appPreferences!!.messageRingtoneUri = LoganSquare.serialize(ringtoneSettings) - adapter!!.toggleSelection(position) - adapter!!.notifyDataSetChanged() - } catch (e: IOException) { - Log.e(TAG, "Failed to store selected ringtone for calls") - } - } - } - return true - } - - private fun endMediaPlayer() { - if (cancelMediaPlayerHandler != null) { - cancelMediaPlayerHandler!!.removeCallbacksAndMessages(null) - } - if (mediaPlayer != null) { - if (mediaPlayer!!.isPlaying) { - mediaPlayer!!.stop() - } - mediaPlayer!!.release() - mediaPlayer = null - } - } - - public override fun onDestroy() { - endMediaPlayer() - super.onDestroy() - } - - companion object { - private const val TAG = "RingtoneSelection" - private const val DURATION_EXTENSION = 25 - } - - init { - setHasOptionsMenu(true) - sharedApplication!!.componentApplication.inject(this) - callNotificationSounds = args.getBoolean(KEY_ARE_CALL_SOUNDS, false) - } - - override val title: String - get() = - resources!!.getString(R.string.nc_settings_notification_sounds) -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt b/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt deleted file mode 100644 index 18737bc5a..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Andy Scherzinger - * @author BlueLine Labs, Inc. - * @author Mario Danic - * Copyright (C) 2021 Andy Scherzinger (info@andy-scherzinger.de) - * Copyright (C) 2021 BlueLine Labs, Inc. - * Copyright (C) 2020 Mario Danic (mario@lovelyhq.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.nextcloud.talk.controllers.base - -import android.content.Context -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import android.widget.EditText -import androidx.annotation.LayoutRes -import androidx.annotation.RequiresApi -import androidx.appcompat.app.ActionBar -import autodagger.AutoInjector -import com.bluelinelabs.conductor.Controller -import com.bluelinelabs.conductor.ControllerChangeHandler -import com.bluelinelabs.conductor.ControllerChangeType -import com.nextcloud.talk.R -import com.nextcloud.talk.activities.MainActivity -import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication -import com.nextcloud.talk.controllers.AccountVerificationController -import com.nextcloud.talk.controllers.ServerSelectionController -import com.nextcloud.talk.controllers.SwitchAccountController -import com.nextcloud.talk.controllers.WebViewLoginController -import com.nextcloud.talk.controllers.base.providers.ActionBarProvider -import com.nextcloud.talk.ui.theme.ViewThemeUtils -import com.nextcloud.talk.utils.preferences.AppPreferences -import javax.inject.Inject -import kotlin.jvm.internal.Intrinsics - -// TODO: check what needs to be migrated from this class to BaseActivity etc when conductor is removed -@AutoInjector(NextcloudTalkApplication::class) -abstract class BaseController(@LayoutRes var layoutRes: Int, args: Bundle? = null) : Controller(args) { - enum class AppBarLayoutType { - TOOLBAR, SEARCH_BAR, EMPTY - } - - @Inject - lateinit var appPreferences: AppPreferences - - @Inject - lateinit var context: Context - - @Inject - lateinit var viewThemeUtils: ViewThemeUtils - - protected open val title: String? - get() = null - - @Suppress("Detekt.TooGenericExceptionCaught") - protected val actionBar: ActionBar? - get() { - var actionBarProvider: ActionBarProvider? = null - if (this.activity is ActionBarProvider) { - try { - actionBarProvider = this.activity as ActionBarProvider? - } catch (e: Exception) { - Log.d(TAG, "Failed to fetch the action bar provider", e) - } - } - return actionBarProvider?.supportActionBar - } - - init { - @Suppress("LeakingThis") - sharedApplication!!.componentApplication.inject(this) - addLifecycleListener(object : LifecycleListener() { - override fun postCreateView(controller: Controller, view: View) { - onViewBound(view) - actionBar?.let { setTitle() } - } - }) - cleanTempCertPreference() - } - - fun isAlive(): Boolean { - return !isDestroyed && !isBeingDestroyed - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup, - savedViewState: Bundle? - ): View { - return inflater.inflate(layoutRes, container, false) - } - - protected open fun onViewBound(view: View) { - var activity: MainActivity? = null - - // if (getActivity() != null && getActivity() is MainActivity) { - // activity = getActivity() as MainActivity? - // viewThemeUtils.material.themeCardView(activity!!.binding.searchToolbar) - // viewThemeUtils.material.themeToolbar(activity.binding.toolbar) - // viewThemeUtils.material.themeSearchBarText(activity.binding.searchText) - // } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences.isKeyboardIncognito) { - disableKeyboardPersonalisedLearning((view as ViewGroup)) - if (activity != null) { - disableKeyboardPersonalisedLearning(activity.binding.appBar) - } - } - } - - override fun onAttach(view: View) { - // showSearchOrToolbar() - setTitle() - if (actionBar != null) { - actionBar!!.setDisplayHomeAsUpEnabled(parentController != null || router.backstackSize >= 1) - } - super.onAttach(view) - } - - // open fun showSearchOrToolbar() { - // if (isValidActivity(activity)) { - // val showSearchBar = appBarLayoutType == AppBarLayoutType.SEARCH_BAR - // val activity = activity as MainActivity - // - // if (appBarLayoutType == AppBarLayoutType.EMPTY) { - // hideBars(activity.binding) - // } else { - // if (showSearchBar) { - // showSearchBar(activity.binding) - // } else { - // showToolbar(activity.binding) - // } - // colorizeStatusBar(showSearchBar, activity, resources) - // } - // - // colorizeNavigationBar(activity, resources) - // } - // } - // - // private fun isValidActivity(activity: Activity?): Boolean { - // return activity != null && activity is MainActivity - // } - // - // private fun showSearchBar(binding: ActivityMainBinding) { - // val layoutParams = binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams - // binding.searchToolbar.visibility = View.VISIBLE - // binding.searchText.hint = searchHint - // binding.toolbar.visibility = View.GONE - // // layoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout - // // .LayoutParams.SCROLL_FLAG_SNAP | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS); - // layoutParams.scrollFlags = 0 - // binding.appBar.stateListAnimator = AnimatorInflater.loadStateListAnimator( - // binding.appBar.context, - // R.animator.appbar_elevation_off - // ) - // binding.searchToolbar.layoutParams = layoutParams - // } - // - // private fun showToolbar(binding: ActivityMainBinding) { - // val layoutParams = binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams - // binding.searchToolbar.visibility = View.GONE - // binding.toolbar.visibility = View.VISIBLE - // viewThemeUtils.material.colorToolbarOverflowIcon(binding.toolbar) - // layoutParams.scrollFlags = 0 - // binding.appBar.stateListAnimator = AnimatorInflater.loadStateListAnimator( - // binding.appBar.context, - // R.animator.appbar_elevation_on - // ) - // binding.searchToolbar.layoutParams = layoutParams - // } - // - // private fun hideBars(binding: ActivityMainBinding) { - // binding.toolbar.visibility = View.GONE - // binding.searchToolbar.visibility = View.GONE - // } - // - // fun hideSearchBar() { - // val activity = activity as MainActivity? - // val layoutParams = activity!!.binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams - // activity.binding.searchToolbar.visibility = View.GONE - // activity.binding.toolbar.visibility = View.VISIBLE - // layoutParams.scrollFlags = 0 - // activity.binding.appBar.stateListAnimator = AnimatorInflater.loadStateListAnimator( - // activity.binding.appBar.context, - // R.animator.appbar_elevation_on - // ) - // } - // - // private fun colorizeStatusBar(showSearchBar: Boolean, activity: Activity?, resources: Resources?) { - // if (activity != null && resources != null) { - // if (showSearchBar) { - // view?.let { viewThemeUtils.platform.resetStatusBar(activity) } - // } else { - // view?.let { viewThemeUtils.platform.themeStatusBar(activity, it) } - // } - // } - // } - // - // private fun colorizeNavigationBar(activity: Activity?, resources: Resources?) { - // if (activity != null && resources != null) { - // DisplayUtils.applyColorToNavigationBar( - // activity.window, - // ResourcesCompat.getColor(resources, R.color.bg_default, null) - // ) - // } - // } - - override fun onDetach(view: View) { - super.onDetach(view) - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(view.windowToken, 0) - } - - protected fun setTitle() { - if (isTitleSetable()) { - run { - calculateValidParentController() - } - actionBar!!.title = title - } - } - - private fun calculateValidParentController() { - var parentController = parentController - while (parentController != null) { - parentController = parentController.parentController - } - } - - private fun isTitleSetable(): Boolean { - return title != null && actionBar != null - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - router.popCurrentController() - return true - } - return super.onOptionsItemSelected(item) - } - - override fun onChangeStarted(changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) { - super.onChangeStarted(changeHandler, changeType) - if (changeType.isEnter && actionBar != null) { - configureMenu(actionBar!!) - } - } - - fun configureMenu(toolbar: ActionBar) { - Intrinsics.checkNotNullParameter(toolbar, "toolbar") - } - - // TODO: check if this must be migrated when using activities instead of conductor - private fun cleanTempCertPreference() { - val temporaryClassNames: MutableList = ArrayList() - temporaryClassNames.add(ServerSelectionController::class.java.name) - temporaryClassNames.add(AccountVerificationController::class.java.name) - temporaryClassNames.add(WebViewLoginController::class.java.name) - temporaryClassNames.add(SwitchAccountController::class.java.name) - if (!temporaryClassNames.contains(javaClass.name)) { - appPreferences.removeTemporaryClientCertAlias() - } - } - - @RequiresApi(api = Build.VERSION_CODES.O) - private fun disableKeyboardPersonalisedLearning(viewGroup: ViewGroup) { - var view: View? - var editText: EditText - for (i in 0 until viewGroup.childCount) { - view = viewGroup.getChildAt(i) - if (view is EditText) { - editText = view - editText.imeOptions = editText.imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING - } else if (view is ViewGroup) { - disableKeyboardPersonalisedLearning(view) - } - } - } - - open val appBarLayoutType: AppBarLayoutType - get() = AppBarLayoutType.TOOLBAR - val searchHint: String - get() = context.getString(R.string.appbar_search_in, context.getString(R.string.nc_app_product_name)) - - companion object { - private val TAG = BaseController::class.java.simpleName - } -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/util/ControllerViewBindingDelegate.kt b/app/src/main/java/com/nextcloud/talk/controllers/util/ControllerViewBindingDelegate.kt deleted file mode 100644 index 70d6247f1..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/util/ControllerViewBindingDelegate.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author BlueLine Labs, Inc. - * Copyright (C) 2016 BlueLine Labs, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.nextcloud.talk.controllers.util - -import android.view.View -import androidx.lifecycle.LifecycleObserver -import androidx.viewbinding.ViewBinding -import com.bluelinelabs.conductor.Controller -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty - -fun Controller.viewBinding(bindingFactory: (View) -> T) = - ControllerViewBindingDelegate(this, bindingFactory) - -class ControllerViewBindingDelegate( - controller: Controller, - private val viewBinder: (View) -> T -) : ReadOnlyProperty, LifecycleObserver { - - private var binding: T? = null - - init { - controller.addLifecycleListener(object : Controller.LifecycleListener() { - override fun postDestroyView(controller: Controller) { - binding = null - } - }) - } - - override fun getValue(thisRef: Controller, property: KProperty<*>): T? { - if (binding == null) { - binding = thisRef.view?.let { viewBinder(it) } - } - return binding - } -} diff --git a/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepository.kt b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepository.kt index 4e085b556..fd8760d49 100644 --- a/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepository.kt @@ -27,13 +27,7 @@ import io.reactivex.Observable interface ConversationRepository { - fun renameConversation( - roomToken: String, - roomNameNew: String - ): Observable + fun renameConversation(roomToken: String, roomNameNew: String): Observable - fun createConversation( - roomName: String, - conversationType: Conversation.ConversationType? - ): Observable + fun createConversation(roomName: String, conversationType: Conversation.ConversationType?): Observable } diff --git a/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt index 6db12b27f..adc6f88eb 100644 --- a/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt @@ -38,10 +38,7 @@ class ConversationRepositoryImpl(private val ncApi: NcApi, currentUserProvider: val currentUser: User = currentUserProvider.currentUser.blockingGet() val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token) - override fun renameConversation( - roomToken: String, - roomNameNew: String - ): Observable { + override fun renameConversation(roomToken: String, roomNameNew: String): Observable { val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1)) return ncApi.renameRoom( diff --git a/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/ConversationViewModel.kt index c35f31ced..69e230310 100644 --- a/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversation/viewmodel/ConversationViewModel.kt @@ -54,10 +54,7 @@ class ConversationViewModel @Inject constructor(private val repository: Conversa disposable?.dispose() } - fun createConversation( - roomName: String, - conversationType: Conversation.ConversationType? - ) { + fun createConversation(roomName: String, conversationType: Conversation.ConversationType?) { _viewState.value = CreatingState repository.createConversation( diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index 42c5ef67e..e04bd2ac6 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -57,9 +57,9 @@ import com.nextcloud.talk.activities.MainActivity import com.nextcloud.talk.adapters.items.ParticipantItem import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.bottomsheet.items.BasicListItemWithImage +import com.nextcloud.talk.bottomsheet.items.listItemsWithImage import com.nextcloud.talk.contacts.ContactsActivity -import com.nextcloud.talk.controllers.bottomsheet.items.BasicListItemWithImage -import com.nextcloud.talk.controllers.bottomsheet.items.listItemsWithImage import com.nextcloud.talk.conversationinfoedit.ConversationInfoEditActivity import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivityConversationInfoBinding @@ -394,7 +394,8 @@ class ConversationInfoActivity : } override fun onError(e: Throwable) { - // unused atm + Log.e(TAG, "Failed to setLobbyForConversation", e) + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() } }) } @@ -663,7 +664,15 @@ class ConversationInfoActivity : showOptionsMenu() } else { binding.addParticipantsAction.visibility = GONE - binding.clearConversationHistory.visibility = GONE + + if (ConversationUtils.isNoteToSelfConversation( + ConversationModel.mapToConversationModel(conversation!!) + ) + ) { + binding.notificationSettingsView.notificationSettings.visibility = VISIBLE + } else { + binding.clearConversationHistory.visibility = GONE + } } if (!isDestroyed) { @@ -823,7 +832,6 @@ class ConversationInfoActivity : private fun initExpiringMessageOption() { if (conversation!!.isParticipantOwnerOrModerator && - !ConversationUtils.isNoteToSelfConversation(ConversationModel.mapToConversationModel(conversation!!)) && CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "message-expiration") ) { databaseStorageModule?.setMessageExpiration(conversation!!.messageExpiration) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 0aa943d05..c473ecc6d 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -50,6 +50,7 @@ import android.view.MotionEvent import android.view.View import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView @@ -71,8 +72,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R +import com.nextcloud.talk.account.ServerSelectionActivity +import com.nextcloud.talk.account.WebViewLoginActivity import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.activities.CallActivity +import com.nextcloud.talk.activities.MainActivity import com.nextcloud.talk.adapters.items.ConversationItem import com.nextcloud.talk.adapters.items.GenericTextHeaderItem import com.nextcloud.talk.adapters.items.LoadMoreResultsItem @@ -84,7 +88,7 @@ import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.contacts.ContactsActivity import com.nextcloud.talk.data.user.model.User -import com.nextcloud.talk.databinding.ControllerConversationsRvBinding +import com.nextcloud.talk.databinding.ActivityConversationsBinding import com.nextcloud.talk.events.ConversationsListFetchDataEvent import com.nextcloud.talk.events.EventStatus import com.nextcloud.talk.jobs.AccountRemovalWorker @@ -103,12 +107,12 @@ import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog import com.nextcloud.talk.ui.dialog.FilterConversationFragment import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils -import com.nextcloud.talk.utils.ClosedInterfaceImpl import com.nextcloud.talk.utils.FileUtils import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.ParticipantPermissions import com.nextcloud.talk.utils.UserIdUtils import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.ADD_ADDITIONAL_ACCOUNT import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_FLAG import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_TEXT @@ -146,7 +150,7 @@ class ConversationsListActivity : FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener { - private lateinit var binding: ControllerConversationsRvBinding + private lateinit var binding: ActivityConversationsBinding @Inject lateinit var userManager: UserManager @@ -202,7 +206,6 @@ class ConversationsListActivity : private val onBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - // TODO: replace this when conductor is removed. For now it avoids to load the MainActiviy which has no UI. finishAffinity() } } @@ -211,7 +214,7 @@ class ConversationsListActivity : super.onCreate(savedInstanceState) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) - binding = ControllerConversationsRvBinding.inflate(layoutInflater) + binding = ActivityConversationsBinding.inflate(layoutInflater) setupActionBar() setContentView(binding.root) setupSystemColors() @@ -250,7 +253,6 @@ class ConversationsListActivity : showShareToScreen = hasActivityActionSendIntent() - ClosedInterfaceImpl().setUpPushTokenRegistration() if (!eventBus.isRegistered(this)) { eventBus.register(this) } @@ -350,9 +352,7 @@ class ConversationsListActivity : viewThemeUtils.material.themeToolbar(binding.conversationListToolbar) } - private fun loadUserAvatar( - target: Target - ) { + private fun loadUserAvatar(target: Target) { if (currentUser != null) { val url = ApiUtils.getUrlForAvatar( currentUser!!.baseUrl, @@ -740,11 +740,20 @@ class ConversationsListActivity : } } + if (resources!!.getBoolean(R.bool.multiaccount_support)) { + dialogBuilder.setNeutralButton(R.string.nc_account_chooser_add_account) { _, _ -> + val intent = Intent(this, ServerSelectionActivity::class.java) + intent.putExtra(ADD_ADDITIONAL_ACCOUNT, true) + startActivity(intent) + } + } + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(it.context, dialogBuilder) val dialog = dialogBuilder.show() viewThemeUtils.platform.colorTextButtons( dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + dialog.getButton(AlertDialog.BUTTON_NEGATIVE), + dialog.getButton(AlertDialog.BUTTON_NEUTRAL) ) } } @@ -819,10 +828,10 @@ class ConversationsListActivity : @SuppressLint("ClickableViewAccessibility") private fun prepareViews() { layoutManager = SmoothScrollLinearLayoutManager(this) - binding?.recyclerView?.layoutManager = layoutManager - binding?.recyclerView?.setHasFixedSize(true) - binding?.recyclerView?.adapter = adapter - binding?.recyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() { + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.adapter = adapter + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) if (newState == RecyclerView.SCROLL_STATE_IDLE) { @@ -1131,7 +1140,7 @@ class ConversationsListActivity : selectedConversation!!.displayName ) } - binding?.floatingActionButton?.let { + binding.floatingActionButton.let { val dialogBuilder = MaterialAlertDialogBuilder(it.context) .setIcon(viewThemeUtils.dialog.colorMaterialAlertDialogIcon(context, R.drawable.upload)) .setTitle(confirmationQuestion) @@ -1358,30 +1367,17 @@ class ConversationsListActivity : .setTitle(R.string.nc_dialog_invalid_password) .setMessage(R.string.nc_dialog_reauth_or_delete) .setCancelable(false) - .setPositiveButton(R.string.nc_delete) { _, _ -> - val otherUserExists = userManager - .scheduleUserForDeletionWithId(currentUser!!.id!!) - .blockingGet() - val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() - WorkManager.getInstance().enqueue(accountRemovalWork) - if (otherUserExists) { - finish() - startActivity(intent) - } else if (!otherUserExists) { - Log.d(TAG, "No other users found. AccountRemovalWorker will restart the app.") - } + .setPositiveButton(R.string.nc_settings_remove_account) { _, _ -> + deleteUserAndRestartApp() + } + .setNegativeButton(R.string.nc_settings_reauthorize) { _, _ -> + val intent = Intent(context, WebViewLoginActivity::class.java) + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_BASE_URL, currentUser!!.baseUrl) + bundle.putBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT, true) + intent.putExtras(bundle) + startActivity(intent) } - - // TODO: show negative button again when conductor is removed - // .setNegativeButton(R.string.nc_settings_reauthorize) { _, _ -> - // // router.pushController( - // // RouterTransaction.with( - // // WebViewLoginController(currentUser!!.baseUrl, true) - // // ) - // // .pushChangeHandler(VerticalChangeHandler()) - // // .popChangeHandler(VerticalChangeHandler()) - // // ) - // } viewThemeUtils.dialog.colorMaterialAlertDialogBackground(it.context, dialogBuilder) val dialog = dialogBuilder.show() @@ -1392,6 +1388,50 @@ class ConversationsListActivity : } } + @SuppressLint("CheckResult") + private fun deleteUserAndRestartApp() { + userManager.scheduleUserForDeletionWithId(currentUser!!.id!!).blockingGet() + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + .observeForever { workInfo: WorkInfo -> + + when (workInfo.state) { + WorkInfo.State.SUCCEEDED -> { + val text = String.format( + context.resources.getString(R.string.nc_deleted_user), + currentUser!!.displayName + ) + Toast.makeText( + context, + text, + Toast.LENGTH_LONG + ).show() + restartApp() + } + + WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "something went wrong when deleting user with id " + currentUser!!.userId) + restartApp() + } + + else -> {} + } + } + } + + private fun restartApp() { + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } + private fun showOutdatedClientDialog() { binding.floatingActionButton.let { val dialogBuilder = MaterialAlertDialogBuilder(it.context) @@ -1423,11 +1463,20 @@ class ConversationsListActivity : } } + if (resources!!.getBoolean(R.bool.multiaccount_support)) { + dialogBuilder.setNeutralButton(R.string.nc_account_chooser_add_account) { _, _ -> + val intent = Intent(this, ServerSelectionActivity::class.java) + intent.putExtra(ADD_ADDITIONAL_ACCOUNT, true) + startActivity(intent) + } + } + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(it.context, dialogBuilder) val dialog = dialogBuilder.show() viewThemeUtils.platform.colorTextButtons( dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + dialog.getButton(AlertDialog.BUTTON_NEGATIVE), + dialog.getButton(AlertDialog.BUTTON_NEUTRAL) ) } } @@ -1445,23 +1494,31 @@ class ConversationsListActivity : .setTitle(R.string.nc_dialog_maintenance_mode) .setMessage(R.string.nc_dialog_maintenance_mode_description) .setCancelable(false) + .setNegativeButton(R.string.nc_settings_remove_account) { _, _ -> + deleteUserAndRestartApp() + } if (resources!!.getBoolean(R.bool.multiaccount_support) && userManager.users.blockingGet().size > 1) { dialogBuilder.setPositiveButton(R.string.nc_switch_account) { _, _ -> val newFragment: DialogFragment = ChooseAccountDialogFragment.newInstance() newFragment.show(supportFragmentManager, ChooseAccountDialogFragment.TAG) } - } else { - dialogBuilder.setPositiveButton(R.string.nc_close_app) { _, _ -> - finishAffinity() - finish() + } + + if (resources!!.getBoolean(R.bool.multiaccount_support)) { + dialogBuilder.setNeutralButton(R.string.nc_account_chooser_add_account) { _, _ -> + val intent = Intent(this, ServerSelectionActivity::class.java) + intent.putExtra(ADD_ADDITIONAL_ACCOUNT, true) + startActivity(intent) } } viewThemeUtils.dialog.colorMaterialAlertDialogBackground(it.context, dialogBuilder) val dialog = dialogBuilder.show() viewThemeUtils.platform.colorTextButtons( - dialog.getButton(AlertDialog.BUTTON_POSITIVE) + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE), + dialog.getButton(AlertDialog.BUTTON_NEUTRAL) ) } } else { @@ -1470,55 +1527,41 @@ class ConversationsListActivity : } private fun showServerEOLDialog() { - binding?.floatingActionButton?.let { + binding.floatingActionButton.let { val dialogBuilder = MaterialAlertDialogBuilder(it.context) .setIcon(viewThemeUtils.dialog.colorMaterialAlertDialogIcon(context, R.drawable.ic_warning_white)) .setTitle(R.string.nc_settings_server_eol_title) .setMessage(R.string.nc_settings_server_eol) .setCancelable(false) .setPositiveButton(R.string.nc_settings_remove_account) { _, _ -> - val otherUserExists = userManager - .scheduleUserForDeletionWithId(currentUser!!.id!!) - .blockingGet() - val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() - WorkManager.getInstance().enqueue(accountRemovalWork) - if (otherUserExists) { - finish() - startActivity(intent) - } else if (!otherUserExists) { - restartApp(this) - } + deleteUserAndRestartApp() } - .setNegativeButton(R.string.nc_cancel) { _, _ -> - if (userManager.users.blockingGet().isNotEmpty()) { - // TODO show SwitchAccount screen again when conductor is removed instead to close app - // router.pushController(RouterTransaction.with(SwitchAccountController())) - finishAffinity() - finish() - } else { - finishAffinity() - finish() - } + + if (resources!!.getBoolean(R.bool.multiaccount_support) && userManager.users.blockingGet().size > 1) { + dialogBuilder.setNegativeButton(R.string.nc_switch_account) { _, _ -> + val newFragment: DialogFragment = ChooseAccountDialogFragment.newInstance() + newFragment.show(supportFragmentManager, ChooseAccountDialogFragment.TAG) } + } + + if (resources!!.getBoolean(R.bool.multiaccount_support)) { + dialogBuilder.setNeutralButton(R.string.nc_account_chooser_add_account) { _, _ -> + val intent = Intent(this, ServerSelectionActivity::class.java) + intent.putExtra(ADD_ADDITIONAL_ACCOUNT, true) + startActivity(intent) + } + } viewThemeUtils.dialog.colorMaterialAlertDialogBackground(it.context, dialogBuilder) val dialog = dialogBuilder.show() viewThemeUtils.platform.colorTextButtons( dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + dialog.getButton(AlertDialog.BUTTON_NEGATIVE), + dialog.getButton(AlertDialog.BUTTON_NEUTRAL) ) } } - fun restartApp(context: Context) { - val packageManager = context.packageManager - val intent = packageManager.getLaunchIntentForPackage(context.packageName) - val componentName = intent!!.component - val mainIntent = Intent.makeRestartActivityTask(componentName) - context.startActivity(mainIntent) - Runtime.getRuntime().exit(0) - } - private fun deleteConversation(conversation: Conversation) { val data = Data.Builder() data.putLong( @@ -1613,10 +1656,10 @@ class ConversationsListActivity : } companion object { - const val TAG = "ConvListController" + private val TAG = ConversationsListActivity::class.java.simpleName const val UNREAD_BUBBLE_DELAY = 2500 const val BOTTOM_SHEET_DELAY: Long = 2500 - private const val KEY_SEARCH_QUERY = "ContactsController.searchQuery" + private const val KEY_SEARCH_QUERY = "ConversationsListActivity.searchQuery" const val SEARCH_DEBOUNCE_INTERVAL_MS = 300 const val SEARCH_MIN_CHARS = 2 const val HTTP_UNAUTHORIZED = 401 diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index 9d93b2a55..19694599a 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -87,8 +87,10 @@ class RepositoryModule { } @Provides - fun provideRemoteFileBrowserItemsRepository(okHttpClient: OkHttpClient, userProvider: CurrentUserProviderNew): - RemoteFileBrowserItemsRepository { + fun provideRemoteFileBrowserItemsRepository( + okHttpClient: OkHttpClient, + userProvider: CurrentUserProviderNew + ): RemoteFileBrowserItemsRepository { return RemoteFileBrowserItemsRepositoryImpl(okHttpClient, userProvider) } @@ -113,38 +115,41 @@ class RepositoryModule { } @Provides - fun provideRequestAssistanceRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): - RequestAssistanceRepository { + fun provideRequestAssistanceRepository( + ncApi: NcApi, + userProvider: CurrentUserProviderNew + ): RequestAssistanceRepository { return RequestAssistanceRepositoryImpl(ncApi, userProvider) } @Provides - fun provideOpenConversationsRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): - OpenConversationsRepository { + fun provideOpenConversationsRepository( + ncApi: NcApi, + userProvider: CurrentUserProviderNew + ): OpenConversationsRepository { return OpenConversationsRepositoryImpl(ncApi, userProvider) } @Provides - fun translateRepository(ncApi: NcApi): - TranslateRepository { + fun translateRepository(ncApi: NcApi): TranslateRepository { return TranslateRepositoryImpl(ncApi) } @Provides - fun provideChatRepository(ncApi: NcApi): - ChatRepository { + fun provideChatRepository(ncApi: NcApi): ChatRepository { return ChatRepositoryImpl(ncApi) } @Provides - fun provideConversationInfoEditRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): - ConversationInfoEditRepository { + fun provideConversationInfoEditRepository( + ncApi: NcApi, + userProvider: CurrentUserProviderNew + ): ConversationInfoEditRepository { return ConversationInfoEditRepositoryImpl(ncApi, userProvider) } @Provides - fun provideConversationRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): - ConversationRepository { + fun provideConversationRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ConversationRepository { return ConversationRepositoryImpl(ncApi, userProvider) } } diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt index 4db51a3e7..65eb7ee5f 100644 --- a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt @@ -64,12 +64,12 @@ abstract class TalkDatabase : RoomDatabase() { const val TAG = "TalkDatabase" @Volatile - private var INSTANCE: TalkDatabase? = null + private var instance: TalkDatabase? = null @JvmStatic fun getInstance(context: Context, appPreferences: AppPreferences): TalkDatabase = - INSTANCE ?: synchronized(this) { - INSTANCE ?: build(context, appPreferences).also { INSTANCE = it } + instance ?: synchronized(this) { + instance ?: build(context, appPreferences).also { instance = it } } private fun build(context: Context, appPreferences: AppPreferences): TalkDatabase { diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenImageActivity.kt similarity index 75% rename from app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt rename to app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenImageActivity.kt index 6c6365e63..ccdfec5a9 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenImageActivity.kt @@ -24,10 +24,8 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.activities +package com.nextcloud.talk.fullscreenfile -import android.annotation.SuppressLint -import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.util.Log @@ -35,7 +33,6 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup.MarginLayoutParams -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import androidx.core.view.ViewCompat @@ -44,28 +41,25 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding -import androidx.work.Data -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkInfo -import androidx.work.WorkManager +import androidx.fragment.app.DialogFragment +import autodagger.AutoInjector import com.google.android.material.snackbar.Snackbar import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.databinding.ActivityFullScreenImageBinding -import com.nextcloud.talk.jobs.SaveFileToStorageWorker -import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment import com.nextcloud.talk.utils.BitmapShrinker import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC import pl.droidsonroids.gif.GifDrawable import java.io.File -import java.util.concurrent.ExecutionException +@AutoInjector(NextcloudTalkApplication::class) class FullScreenImageActivity : AppCompatActivity() { lateinit var binding: ActivityFullScreenImageBinding private lateinit var windowInsetsController: WindowInsetsControllerCompat private lateinit var path: String private var showFullscreen = false - lateinit var viewThemeUtils: ViewThemeUtils override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_preview, menu) @@ -98,7 +92,13 @@ class FullScreenImageActivity : AppCompatActivity() { } R.id.save -> { - showWarningDialog() + val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance( + intent.getStringExtra("FILE_NAME").toString() + ) + saveFragment.show( + supportFragmentManager, + SaveToStorageDialogFragment.TAG + ) true } @@ -108,24 +108,9 @@ class FullScreenImageActivity : AppCompatActivity() { } } - private fun showWarningDialog() { - val builder = AlertDialog.Builder(this) - builder.setTitle(R.string.nc_dialog_save_to_storage_title) - builder.setMessage(R.string.nc_dialog_save_to_storage_content) - builder.setPositiveButton(R.string.nc_dialog_save_to_storage_yes) { dialog: DialogInterface, which: Int -> - val fileName = intent.getStringExtra("FILE_NAME").toString() - saveImageToStorage(fileName) - dialog.dismiss() - } - builder.setNegativeButton(R.string.nc_dialog_save_to_storage_no) { dialog: DialogInterface, which: Int -> - dialog.dismiss() - } - val dialog = builder.create() - dialog.show() - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) binding = ActivityFullScreenImageBinding.inflate(layoutInflater) setContentView(binding.root) @@ -222,38 +207,6 @@ class FullScreenImageActivity : AppCompatActivity() { } } - @SuppressLint("LongLogTag") - private fun saveImageToStorage( - fileName: String - ) { - val sourceFilePath = applicationContext.cacheDir.path - - val workers = WorkManager.getInstance(this).getWorkInfosByTag(fileName) - try { - for (workInfo in workers.get()) { - if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { - return - } - } - } catch (e: ExecutionException) { - Log.e(TAG, "Error when checking if worker already exists", e) - } catch (e: InterruptedException) { - Log.e(TAG, "Error when checking if worker already exists", e) - } - - val data: Data = Data.Builder() - .putString(SaveFileToStorageWorker.KEY_FILE_NAME, fileName) - .putString(SaveFileToStorageWorker.KEY_SOURCE_FILE_PATH, "$sourceFilePath/$fileName") - .build() - - val saveWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SaveFileToStorageWorker::class.java) - .setInputData(data) - .addTag(fileName) - .build() - - WorkManager.getInstance().enqueue(saveWorker) - } - companion object { private const val TAG = "FullScreenImageActivity" private const val HUNDRED_MB = 100 * 1024 * 1024 diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt similarity index 93% rename from app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt rename to app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt index 6d9f9262d..1d6796b52 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt @@ -24,7 +24,7 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.activities +package com.nextcloud.talk.fullscreenfile import android.content.Intent import android.os.Bundle @@ -43,6 +43,7 @@ import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.marginBottom import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding +import androidx.fragment.app.DialogFragment import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer @@ -53,6 +54,7 @@ import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.databinding.ActivityFullScreenMediaBinding +import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment import com.nextcloud.talk.utils.Mimetype.VIDEO_PREFIX_GENERIC import java.io.File @@ -78,6 +80,7 @@ class FullScreenMediaActivity : AppCompatActivity() { onBackPressedDispatcher.onBackPressed() true } + R.id.share -> { val shareUri = FileProvider.getUriForFile( this, @@ -95,6 +98,18 @@ class FullScreenMediaActivity : AppCompatActivity() { true } + + R.id.save -> { + val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance( + intent.getStringExtra("FILE_NAME").toString() + ) + saveFragment.show( + supportFragmentManager, + SaveToStorageDialogFragment.TAG + ) + true + } + else -> { super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt similarity index 71% rename from app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt rename to app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt index 00d17b7d5..d78ff54c3 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt @@ -22,7 +22,7 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.activities +package com.nextcloud.talk.fullscreenfile import android.content.Intent import android.os.Bundle @@ -31,11 +31,13 @@ import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.DialogFragment import autodagger.AutoInjector import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.databinding.ActivityFullScreenTextBinding +import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.Mimetype.TEXT_PREFIX_GENERIC @@ -58,27 +60,44 @@ class FullScreenTextViewerActivity : AppCompatActivity() { } override fun onOptionsItemSelected(item: MenuItem): Boolean { - return if (item.itemId == android.R.id.home) { - onBackPressedDispatcher.onBackPressed() - true - } else if (item.itemId == R.id.share) { - val shareUri = FileProvider.getUriForFile( - this, - BuildConfig.APPLICATION_ID, - File(path) - ) - - val shareIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_STREAM, shareUri) - type = TEXT_PREFIX_GENERIC - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + return when (item.itemId) { + android.R.id.home -> { + onBackPressedDispatcher.onBackPressed() + true } - startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to))) - true - } else { - super.onOptionsItemSelected(item) + R.id.share -> { + val shareUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID, + File(path) + ) + + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, shareUri) + type = TEXT_PREFIX_GENERIC + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to))) + + true + } + + R.id.save -> { + val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance( + intent.getStringExtra("FILE_NAME").toString() + ) + saveFragment.show( + supportFragmentManager, + SaveToStorageDialogFragment.TAG + ) + true + } + + else -> { + super.onOptionsItemSelected(item) + } } } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java index 940d6780d..18b739503 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java +++ b/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java @@ -23,10 +23,7 @@ package com.nextcloud.talk.jobs; import android.app.NotificationManager; -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; import android.os.Build; import android.util.Log; @@ -129,6 +126,7 @@ public class AccountRemovalWorker extends Worker { @Override public void onError(@io.reactivex.annotations.NonNull Throwable e) { Log.e(TAG, "error while trying to unregister Device For Notifications", e); + initiateUserDeletion(user); } @Override @@ -137,7 +135,7 @@ public class AccountRemovalWorker extends Worker { } }); } else { - deleteUser(user); + initiateUserDeletion(user); } } @@ -172,15 +170,13 @@ public class AccountRemovalWorker extends Worker { } } - if (user.getId() != null) { - WebSocketConnectionHelper.deleteExternalSignalingInstanceForUserEntity(user.getId()); - } - deleteAllEntriesForAccountIdentifier(user); + initiateUserDeletion(user); } @Override public void onError(Throwable e) { Log.e(TAG, "error while trying to unregister Device For Notification With Proxy", e); + initiateUserDeletion(user); } @Override @@ -190,8 +186,10 @@ public class AccountRemovalWorker extends Worker { }); } - private void deleteAllEntriesForAccountIdentifier(User user) { + private void initiateUserDeletion(User user) { if (user.getId() != null) { + WebSocketConnectionHelper.deleteExternalSignalingInstanceForUserEntity(user.getId()); + try { arbitraryStorageManager.deleteAllEntriesForAccountIdentifier(user.getId()); deleteUser(user); @@ -211,17 +209,5 @@ public class AccountRemovalWorker extends Worker { Log.e(TAG, "error while trying to delete user", e); } } - if (userManager.getUsers().blockingGet().isEmpty()) { - restartApp(getApplicationContext()); - } - } - - public static void restartApp(Context context) { - PackageManager packageManager = context.getPackageManager(); - Intent intent = packageManager.getLaunchIntentForPackage(context.getPackageName()); - ComponentName componentName = intent.getComponent(); - Intent mainIntent = Intent.makeRestartActivityTask(componentName); - context.startActivity(mainIntent); - Runtime.getRuntime().exit(0); } } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index 9c4cfcae9..cb4c3871f 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -42,7 +42,7 @@ import android.service.notification.StatusBarNotification import android.text.TextUtils import android.util.Base64 import android.util.Log -import android.view.View +import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -56,7 +56,6 @@ import androidx.work.Worker import androidx.work.WorkerParameters import autodagger.AutoInjector import com.bluelinelabs.logansquare.LoganSquare -import com.google.android.material.snackbar.Snackbar import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R import com.nextcloud.talk.activities.MainActivity @@ -329,11 +328,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor Log.e(TAG, "Failed to get NC notification", e) if (BuildConfig.DEBUG) { Handler(Looper.getMainLooper()).post { - Snackbar.make( - View(applicationContext), - "Failed to get NC notification", - Snackbar.LENGTH_LONG - ).show() + Toast.makeText(context, "Failed to get NC notification", Toast.LENGTH_LONG).show() } } } @@ -857,6 +852,10 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor override fun onError(e: Throwable) { Log.e(TAG, "Error in getPeersForCall", e) + if (isCallNotificationVisible) { + showMissedCallNotification() + } + removeNotification(pushMessage.timestamp.toInt()) } override fun onComplete() { diff --git a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java index 2fe18bb2a..7869a6e7f 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java +++ b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java @@ -64,7 +64,7 @@ public class PushRegistrationWorker extends Worker { @Override public Result doWork() { NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); - if(new ClosedInterfaceImpl().isGooglePlayServicesAvailable()){ + if (new ClosedInterfaceImpl().isGooglePlayServicesAvailable()) { Data data = getInputData(); String origin = data.getString("origin"); Log.d(TAG, "PushRegistrationWorker called via " + origin); diff --git a/app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt index 29f702955..c2c90c551 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt @@ -1,10 +1,10 @@ /* * Nextcloud Talk application * - * @author Andy Scherzinger + * @author Fariba Khandani * @author Marcel Hibbe - * Copyright (C) 2022 Andy Scherzinger - * Copyright (C) 2021 Marcel Hibbe + * Copyright (C) 2023 Fariba Khandani + * Copyright (C) 2023 Marcel Hibbe * * 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 @@ -25,14 +25,23 @@ package com.nextcloud.talk.jobs import android.content.ContentValues import android.content.Context import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Build import android.os.Environment +import android.os.Handler +import android.os.Looper import android.provider.MediaStore import android.provider.MediaStore.Files.FileColumns import android.util.Log +import android.widget.Toast import androidx.work.Worker import androidx.work.WorkerParameters import autodagger.AutoInjector +import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.utils.Mimetype.AUDIO_PREFIX +import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX +import com.nextcloud.talk.utils.Mimetype.VIDEO_PREFIX import java.io.File import java.io.IOException import java.io.OutputStream @@ -50,16 +59,22 @@ class SaveFileToStorageWorker(val context: Context, workerParameters: WorkerPara val contentResolver = context.contentResolver val mimeType = URLConnection.guessContentTypeFromName(cacheFile.name) + val appName = applicationContext.resources!!.getString(R.string.nc_app_product_name) + val values = ContentValues().apply { + if (mimeType.startsWith(IMAGE_PREFIX) || mimeType.startsWith(VIDEO_PREFIX)) { + put(FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/" + appName) + } put(FileColumns.DISPLAY_NAME, cacheFile.name) - put(FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + if (mimeType != null) { - put(FileColumns.MIME_TYPE, URLConnection.guessContentTypeFromName(cacheFile.name)) + put(FileColumns.MIME_TYPE, mimeType) } } - val collection = MediaStore.Files.getContentUri("external") - val uri = contentResolver.insert(collection, values) + val collectionUri = getUriByType(mimeType) + + val uri = contentResolver.insert(collectionUri, values) uri?.let { fileUri -> try { @@ -79,16 +94,53 @@ class SaveFileToStorageWorker(val context: Context, workerParameters: WorkerPara // Notify the media scanner about the new file MediaScannerConnection.scanFile(context, arrayOf(cacheFile.absolutePath), null, null) + Handler(Looper.getMainLooper()).post { + Toast.makeText( + context, + context.resources.getString(R.string.nc_save_success), + Toast.LENGTH_SHORT + ).show() + } + return Result.success() } catch (e: IOException) { Log.e(TAG, "Something went wrong when trying to save file to internal storage", e) + Handler(Looper.getMainLooper()).post { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_SHORT + ).show() + } + return Result.failure() } catch (e: NullPointerException) { Log.e(TAG, "Something went wrong when trying to save file to internal storage", e) + Handler(Looper.getMainLooper()).post { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_SHORT + ).show() + } + return Result.failure() } } + private fun getUriByType(mimeType: String): Uri { + return when { + mimeType.startsWith(VIDEO_PREFIX) -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + mimeType.startsWith(AUDIO_PREFIX) -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + mimeType.startsWith(IMAGE_PREFIX) -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + else -> if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + Uri.fromFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)) + } else { + MediaStore.Downloads.EXTERNAL_CONTENT_URI + } + } + } + companion object { private val TAG = SaveFileToStorageWorker::class.java.simpleName const val KEY_FILE_NAME = "KEY_FILE_NAME" diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt index 95f09fdcb..45b3cae47 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt @@ -93,12 +93,7 @@ class ShareOperationWorker(context: Context, workerParams: WorkerParameters) : W companion object { private val TAG = ShareOperationWorker::class.simpleName - fun shareFile( - roomToken: String?, - currentUser: User, - remotePath: String, - metaData: String? - ) { + fun shareFile(roomToken: String?, currentUser: User, remotePath: String, metaData: String?) { val paths: MutableList = ArrayList() paths.add(remotePath) diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt index e6bc5370d..6ace53128 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt @@ -186,9 +186,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa return remotePath } - override fun onTransferProgress( - percentage: Int - ) { + override fun onTransferProgress(percentage: Int) { notification = mBuilder!! .setProgress(HUNDRED_PERCENT, percentage, false) .setContentText(getNotificationContentText(percentage)) @@ -322,12 +320,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa } } - fun upload( - fileUri: String, - roomToken: String, - conversationName: String, - metaData: String? - ) { + fun upload(fileUri: String, roomToken: String, conversationName: String, metaData: String?) { val data: Data = Data.Builder() .putString(DEVICE_SOURCE_FILE, fileUri) .putString(ROOM_TOKEN, roomToken) diff --git a/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt index 123b8ee10..1ecc4769f 100644 --- a/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt @@ -121,7 +121,7 @@ class GeocodingActivity : if (viewModel.getQuery().isNotEmpty() && adapter.itemCount == 0) { viewModel.searchLocation() } else { - Log.e(TAG, "search string that was passed to GeocodingController was null or empty") + Log.e(TAG, "search string that was passed to GeocodingActivity was null or empty") } adapter.setOnItemClickListener(object : GeocodingAdapter.OnItemClickListener { override fun onItemClick(position: Int) { diff --git a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt index be2bba066..c93de415a 100644 --- a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt @@ -335,41 +335,42 @@ class LocationPickerActivity : ) } - private fun delayedMapListener() = DelayedMapListener( - object : MapListener { - @Suppress("Detekt.TooGenericExceptionCaught") - override fun onScroll(paramScrollEvent: ScrollEvent): Boolean { - try { - when { - moveToCurrentLocation -> { - setLocationDescription(isGpsLocation = true, isGeocodedResult = false) - moveToCurrentLocation = false - } + private fun delayedMapListener() = + DelayedMapListener( + object : MapListener { + @Suppress("Detekt.TooGenericExceptionCaught") + override fun onScroll(paramScrollEvent: ScrollEvent): Boolean { + try { + when { + moveToCurrentLocation -> { + setLocationDescription(isGpsLocation = true, isGeocodedResult = false) + moveToCurrentLocation = false + } - geocodingResult != null -> { - binding.shareLocation.isClickable = true - setLocationDescription(isGpsLocation = false, isGeocodedResult = true) - geocodingResult = null - } + geocodingResult != null -> { + binding.shareLocation.isClickable = true + setLocationDescription(isGpsLocation = false, isGeocodedResult = true) + geocodingResult = null + } - else -> { - binding.shareLocation.isClickable = true - setLocationDescription(isGpsLocation = false, isGeocodedResult = false) + else -> { + binding.shareLocation.isClickable = true + setLocationDescription(isGpsLocation = false, isGeocodedResult = false) + } } + } catch (e: NullPointerException) { + Log.d(TAG, "UI already closed") } - } catch (e: NullPointerException) { - Log.d(TAG, "UI already closed") + + readyToShareLocation = true + return true } - readyToShareLocation = true - return true + override fun onZoom(event: ZoomEvent): Boolean { + return false + } } - - override fun onZoom(event: ZoomEvent): Boolean { - return false - } - } - ) + ) @Suppress("Detekt.TooGenericExceptionCaught") private fun requestLocationUpdates() { @@ -524,11 +525,7 @@ class LocationPickerActivity : ) } - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) fun areAllGranted(grantResults: IntArray): Boolean { diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt index 9859d3b8d..e9e2a164a 100644 --- a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt @@ -46,9 +46,7 @@ class ConversationModel( ) { companion object { - fun mapToConversationModel( - conversation: Conversation - ): ConversationModel { + fun mapToConversationModel(conversation: Conversation): ConversationModel { return ConversationModel( roomId = conversation.roomId, token = conversation.token, @@ -113,7 +111,13 @@ enum class ConversationType { } enum class ParticipantType { - DUMMY, OWNER, MODERATOR, USER, GUEST, USER_FOLLOWING_LINK, GUEST_MODERATOR + DUMMY, + OWNER, + MODERATOR, + USER, + GUEST, + USER_FOLLOWING_LINK, + GUEST_MODERATOR } enum class ObjectType { @@ -124,13 +128,18 @@ enum class ObjectType { } enum class NotificationLevel { - DEFAULT, ALWAYS, MENTION, NEVER + DEFAULT, + ALWAYS, + MENTION, + NEVER } enum class ConversationReadOnlyState { - CONVERSATION_READ_WRITE, CONVERSATION_READ_ONLY + CONVERSATION_READ_WRITE, + CONVERSATION_READ_ONLY } enum class LobbyState { - LOBBY_STATE_ALL_PARTICIPANTS, LOBBY_STATE_MODERATORS_ONLY + LOBBY_STATE_ALL_PARTICIPANTS, + LOBBY_STATE_MODERATORS_ONLY } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt index e05fea28b..c9d70038a 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt @@ -285,6 +285,7 @@ data class ChatMessage( "" } } + val lastMessageDisplayText: String get() { if (getCalculateMessageType() == MessageType.REGULAR_TEXT_MESSAGE || @@ -373,11 +374,12 @@ data class ChatMessage( return "" } - private fun getNullsafeActorDisplayName() = if (!TextUtils.isEmpty(actorDisplayName)) { - actorDisplayName - } else { - sharedApplication!!.getString(R.string.nc_guest) - } + private fun getNullsafeActorDisplayName() = + if (!TextUtils.isEmpty(actorDisplayName)) { + actorDisplayName + } else { + sharedApplication!!.getString(R.string.nc_guest) + } override fun getUser(): IUser { return object : IUser { @@ -470,7 +472,8 @@ data class ChatMessage( * see https://nextcloud-talk.readthedocs.io/en/latest/chat/#system-messages */ enum class SystemMessageType { - DUMMY, CONVERSATION_CREATED, + DUMMY, + CONVERSATION_CREATED, CONVERSATION_RENAMED, DESCRIPTION_REMOVED, DESCRIPTION_SET, @@ -504,7 +507,8 @@ data class ChatMessage( GUEST_MODERATOR_PROMOTED, GUEST_MODERATOR_DEMOTED, MESSAGE_DELETED, - FILE_SHARED, OBJECT_SHARED, + FILE_SHARED, + OBJECT_SHARED, MATTERBRIDGE_CONFIG_ADDED, MATTERBRIDGE_CONFIG_EDITED, MATTERBRIDGE_CONFIG_REMOVED, diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt index d6d1d6869..d09ccd680 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt @@ -26,8 +26,10 @@ package com.nextcloud.talk.models.json.chat class ChatUtils { companion object { - fun getParsedMessage(message: String?, messageParameters: HashMap>?): - String? { + fun getParsedMessage( + message: String?, + messageParameters: HashMap>? + ): String? { if (messageParameters != null && messageParameters.size > 0) { return parse(messageParameters, message) } @@ -35,10 +37,7 @@ class ChatUtils { } @Suppress("Detekt.ComplexMethod") - private fun parse( - messageParameters: HashMap>, - message: String? - ): String? { + private fun parse(messageParameters: HashMap>, message: String?): String? { var resultMessage = message for (key in messageParameters.keys) { val individualHashMap = messageParameters[key] diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt index d91368995..07259466d 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt @@ -20,5 +20,7 @@ package com.nextcloud.talk.models.json.chat enum class ReadStatus { - NONE, SENT, READ + NONE, + SENT, + READ } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt index e3dd2ea92..57dc17779 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt @@ -229,15 +229,20 @@ data class Conversation( } enum class NotificationLevel { - DEFAULT, ALWAYS, MENTION, NEVER + DEFAULT, + ALWAYS, + MENTION, + NEVER } enum class LobbyState { - LOBBY_STATE_ALL_PARTICIPANTS, LOBBY_STATE_MODERATORS_ONLY + LOBBY_STATE_ALL_PARTICIPANTS, + LOBBY_STATE_MODERATORS_ONLY } enum class ConversationReadOnlyState { - CONVERSATION_READ_WRITE, CONVERSATION_READ_ONLY + CONVERSATION_READ_WRITE, + CONVERSATION_READ_ONLY } @Parcelize diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.kt index 3b085025b..68f0c7bf9 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.kt @@ -124,11 +124,22 @@ data class Participant( } enum class ActorType { - DUMMY, EMAILS, GROUPS, GUESTS, USERS, CIRCLES + DUMMY, + EMAILS, + GROUPS, + GUESTS, + USERS, + CIRCLES } enum class ParticipantType { - DUMMY, OWNER, MODERATOR, USER, GUEST, USER_FOLLOWING_LINK, GUEST_MODERATOR + DUMMY, + OWNER, + MODERATOR, + USER, + GUEST, + USER_FOLLOWING_LINK, + GUEST_MODERATOR } object InCallFlags { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionVoter.kt b/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionVoter.kt index c50015587..74eeec9a3 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionVoter.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionVoter.kt @@ -41,6 +41,8 @@ data class ReactionVoter( constructor() : this(null, null, null, 0) enum class ReactionActorType { - DUMMY, GUESTS, USERS + DUMMY, + GUESTS, + USERS } } diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt index 1924b85c1..cde534757 100644 --- a/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt @@ -42,9 +42,7 @@ class OpenConversationsRepositoryImpl(private val ncApi: NcApi, currentUserProvi ).map { mapToOpenConversationsModel(it.ocs?.data!!) } } - private fun mapToOpenConversationsModel( - conversations: List - ): OpenConversationsModel { + private fun mapToOpenConversationsModel(conversations: List): OpenConversationsModel { return OpenConversationsModel( conversations.map { conversation -> OpenConversation( diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt index 26a97801e..6280b6a1e 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt @@ -75,20 +75,19 @@ class PollCreateOptionViewHolder( private fun getTextWatcher( pollCreateOptionItem: PollCreateOptionItem, itemsListener: PollCreateOptionsItemListener - ) = - object : TextWatcher { - override fun afterTextChanged(s: Editable) { - // unused atm - } - - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - // unused atm - } - - override fun onTextChanged(option: CharSequence, start: Int, before: Int, count: Int) { - pollCreateOptionItem.pollOption = option.toString() - - itemsListener.onOptionsItemTextChanged(pollCreateOptionItem) - } + ) = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + // unused atm } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + // unused atm + } + + override fun onTextChanged(option: CharSequence, start: Int, before: Int, count: Int) { + pollCreateOptionItem.pollOption = option.toString() + + itemsListener.onOptionsItemTextChanged(pollCreateOptionItem) + } + } } diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt index 80b8a62c6..803e1136c 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt @@ -49,11 +49,7 @@ class PollLoadingFragment : Fragment() { fragmentHeight = arguments?.getInt(KEY_FRAGMENT_HEIGHT)!! } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = DialogPollLoadingBinding.inflate(inflater, container, false) binding.root.layoutParams.height = fragmentHeight viewThemeUtils.platform.colorCircularProgressBar(binding.pollLoadingProgressbar, ColorRole.PRIMARY) @@ -65,9 +61,7 @@ class PollLoadingFragment : Fragment() { private const val KEY_FRAGMENT_HEIGHT = "keyFragmentHeight" @JvmStatic - fun newInstance( - fragmentHeight: Int - ): PollLoadingFragment { + fun newInstance(fragmentHeight: Int): PollLoadingFragment { val args = bundleOf( KEY_FRAGMENT_HEIGHT to fragmentHeight ) diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt index 27c28dd80..4ee667ad7 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt @@ -121,7 +121,7 @@ class PollMainDialogFragment : DialogFragment() { private fun showLoadingScreen() { binding.root.post { - run() { + run { val fragmentHeight = binding.messagePollContentFragment.measuredHeight val contentFragment = PollLoadingFragment.newInstance(fragmentHeight) diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt index 17c040886..2d6b338bd 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt @@ -65,11 +65,7 @@ class PollResultsFragment : Fragment(), PollResultItemClickListener { parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory)[PollMainViewModel::class.java] } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = DialogPollResultsBinding.inflate(inflater, container, false) return binding.root } diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt index 2443abaca..9ce154019 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt @@ -69,11 +69,7 @@ class PollVoteFragment : Fragment() { parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory)[PollMainViewModel::class.java] } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = DialogPollVoteBinding.inflate(inflater, container, false) return binding.root } diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt index 430b0d37c..8ce1737c5 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt @@ -41,8 +41,8 @@ class PollResultsViewModel @Inject constructor() : ViewModel() { val poll: Poll? get() = _poll - private var _itemsOverviewList: ArrayList = ArrayList() - private var _itemsDetailsList: ArrayList = ArrayList() + private var itemsOverviewList: ArrayList = ArrayList() + private var itemsDetailsList: ArrayList = ArrayList() private var _items: MutableLiveData?> = MutableLiveData?>() val items: MutableLiveData?> @@ -77,23 +77,23 @@ class PollResultsViewModel @Inject constructor() : ViewModel() { optionsPercent, isOptionSelfVoted(poll, index) ) - _itemsOverviewList.add(pollResultHeaderItem) - _itemsDetailsList.add(pollResultHeaderItem) + itemsOverviewList.add(pollResultHeaderItem) + itemsDetailsList.add(pollResultHeaderItem) val voters = poll.details?.filter { it.optionId == index } if (!voters.isNullOrEmpty()) { - _itemsOverviewList.add(PollResultVotersOverviewItem(voters)) + itemsOverviewList.add(PollResultVotersOverviewItem(voters)) } if (!voters.isNullOrEmpty()) { voters.forEach { - _itemsDetailsList.add(PollResultVoterItem(it)) + itemsDetailsList.add(PollResultVoterItem(it)) } } } - _items.value = _itemsOverviewList + _items.value = itemsOverviewList } private fun getVotersAmountForOption(poll: Poll, index: Int): Int { @@ -114,10 +114,10 @@ class PollResultsViewModel @Inject constructor() : ViewModel() { } fun toggleDetails() { - if (_items.value?.containsAll(_itemsDetailsList) == true) { - _items.value = _itemsOverviewList + if (_items.value?.containsAll(itemsDetailsList) == true) { + _items.value = itemsOverviewList } else { - _items.value = _itemsDetailsList + _items.value = itemsDetailsList } } diff --git a/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt b/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt index c44c50cff..48020e8c9 100644 --- a/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt @@ -608,7 +608,7 @@ class ProfileActivity : BaseActivity() { class UserInfoAdapter( displayList: List?, private val viewThemeUtils: ViewThemeUtils, - private val controller: ProfileActivity + private val profileActivity: ProfileActivity ) : RecyclerView.Adapter() { var displayList: List? var filteredDisplayList: MutableList = LinkedList() @@ -643,7 +643,7 @@ class ProfileActivity : BaseActivity() { } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val item: UserInfoDetailsItem = if (controller.edit) { + val item: UserInfoDetailsItem = if (profileActivity.edit) { displayList!![position] } else { filteredDisplayList[position] @@ -656,11 +656,11 @@ class ProfileActivity : BaseActivity() { holder.binding.icon.contentDescription = item.hint viewThemeUtils.platform.colorImageView(holder.binding.icon, ColorRole.PRIMARY) - if (!TextUtils.isEmpty(item.text) || controller.edit) { + if (!TextUtils.isEmpty(item.text) || profileActivity.edit) { holder.binding.userInfoDetailContainer.visibility = View.VISIBLE - controller.viewThemeUtils.material.colorTextInputLayout(holder.binding.userInfoInputLayout) - if (controller.edit && - controller.editableFields.contains(item.field.toString().lowercase()) + profileActivity.viewThemeUtils.material.colorTextInputLayout(holder.binding.userInfoInputLayout) + if (profileActivity.edit && + profileActivity.editableFields.contains(item.field.toString().lowercase()) ) { holder.binding.userInfoEditTextEdit.isEnabled = true holder.binding.userInfoEditTextEdit.isFocusableInTouchMode = true @@ -688,10 +688,7 @@ class ProfileActivity : BaseActivity() { } } - private fun initUserInfoEditText( - holder: ViewHolder, - item: UserInfoDetailsItem - ) { + private fun initUserInfoEditText(holder: ViewHolder, item: UserInfoDetailsItem) { holder.binding.userInfoEditTextEdit.setText(item.text) holder.binding.userInfoInputLayout.hint = item.hint holder.binding.userInfoEditTextEdit.addTextChangedListener(object : TextWatcher { @@ -700,7 +697,7 @@ class ProfileActivity : BaseActivity() { } override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - if (controller.edit) { + if (profileActivity.edit) { displayList!![holder.adapterPosition].text = holder.binding.userInfoEditTextEdit.text.toString() } else { filteredDisplayList[holder.adapterPosition].text = @@ -714,10 +711,7 @@ class ProfileActivity : BaseActivity() { }) } - private fun initScopeElements( - item: UserInfoDetailsItem, - holder: ViewHolder - ) { + private fun initScopeElements(item: UserInfoDetailsItem, holder: ViewHolder) { if (item.scope == null) { holder.binding.scope.visibility = View.GONE } else { @@ -739,7 +733,7 @@ class ProfileActivity : BaseActivity() { } override fun getItemCount(): Int { - return if (controller.edit) { + return if (profileActivity.edit) { displayList!!.size } else { filteredDisplayList.size @@ -762,7 +756,7 @@ class ProfileActivity : BaseActivity() { } companion object { - private const val TAG: String = "ProfileController" + private val TAG = ProfileActivity::class.java.simpleName private const val DEFAULT_CACHE_SIZE: Int = 20 private const val DEFAULT_RETRIES: Long = 3 private const val HIGH_EMPHASIS_ALPHA: Float = 0.87f diff --git a/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepository.kt b/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepository.kt index 826dc72e1..2c5d2b958 100644 --- a/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepository.kt @@ -24,11 +24,7 @@ import io.reactivex.Observable interface RequestAssistanceRepository { - fun requestAssistance( - roomToken: String - ): Observable + fun requestAssistance(roomToken: String): Observable - fun withdrawRequestAssistance( - roomToken: String - ): Observable + fun withdrawRequestAssistance(roomToken: String): Observable } diff --git a/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepositoryImpl.kt index aa42daa7d..757602762 100644 --- a/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepositoryImpl.kt @@ -57,18 +57,14 @@ class RequestAssistanceRepositoryImpl(private val ncApi: NcApi, currentUserProvi ).map { mapToWithdrawRequestAssistanceModel(it.ocs?.meta!!) } } - private fun mapToRequestAssistanceModel( - response: GenericMeta - ): RequestAssistanceModel { + private fun mapToRequestAssistanceModel(response: GenericMeta): RequestAssistanceModel { val success = response.statusCode == HTTP_OK return RequestAssistanceModel( success ) } - private fun mapToWithdrawRequestAssistanceModel( - response: GenericMeta - ): WithdrawRequestAssistanceModel { + private fun mapToWithdrawRequestAssistanceModel(response: GenericMeta): WithdrawRequestAssistanceModel { val success = response.statusCode == HTTP_OK return WithdrawRequestAssistanceModel( success diff --git a/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt index 11f6c5bf3..aebd9230a 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt +++ b/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt @@ -91,8 +91,6 @@ class ShareRecordingToChatReceiver : BroadcastReceiver() { // However, as we are in a broadcast receiver, this needs a TaskStackBuilder // combined with addNextIntentWithParentStack. For further reading, see // https://developer.android.com/develop/ui/views/notifications/navigation#DirectEntry - // As we are using the conductor framework it might be hard the combine this or to keep an overview. - // For this reason there is only a Snackbar for now until we got rid of conductor. Snackbar.make( View(context), diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt index ec0ee64fd..12c21bb61 100644 --- a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt @@ -138,6 +138,8 @@ class RemoteFileBrowserActivity : AppCompatActivity(), SelectionInterface, Swipe is RemoteFileBrowserItemsViewModel.FinishState -> { finishWithResult(state.selectedPaths) } + + else -> {} } } @@ -158,10 +160,7 @@ class RemoteFileBrowserActivity : AppCompatActivity(), SelectionInterface, Swipe } } - private fun loadList( - state: RemoteFileBrowserItemsViewModel.LoadedState, - mimeTypeSelectionFilter: String? - ) { + private fun loadList(state: RemoteFileBrowserItemsViewModel.LoadedState, mimeTypeSelectionFilter: String?) { val remoteFileBrowserItems = state.items Log.d(TAG, "Items received: $remoteFileBrowserItems") diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt index 45f104d3f..b238744d5 100644 --- a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt @@ -36,8 +36,7 @@ class RemoteFileBrowserItemsRepositoryImpl @Inject constructor( private val user: User get() = userProvider.currentUser.blockingGet() - override fun listFolder(path: String): - Observable> { + override fun listFolder(path: String): Observable> { return Observable.fromCallable { val operation = ReadFolderListingOperation( diff --git a/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepository.kt index a206442eb..8422a8315 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepository.kt @@ -26,11 +26,7 @@ import io.reactivex.Observable interface CallRecordingRepository { - fun startRecording( - roomToken: String - ): Observable + fun startRecording(roomToken: String): Observable - fun stopRecording( - roomToken: String - ): Observable + fun stopRecording(roomToken: String): Observable } diff --git a/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt index 601b431b8..71c6d64d2 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt @@ -37,9 +37,7 @@ class CallRecordingRepositoryImpl(private val ncApi: NcApi, currentUserProvider: var apiVersion = 1 - override fun startRecording( - roomToken: String - ): Observable { + override fun startRecording(roomToken: String): Observable { return ncApi.startRecording( credentials, ApiUtils.getUrlForRecording( @@ -51,9 +49,7 @@ class CallRecordingRepositoryImpl(private val ncApi: NcApi, currentUserProvider: ).map { mapToStartCallRecordingModel(it.ocs?.meta!!) } } - override fun stopRecording( - roomToken: String - ): Observable { + override fun stopRecording(roomToken: String): Observable { return ncApi.stopRecording( credentials, ApiUtils.getUrlForRecording( @@ -64,18 +60,14 @@ class CallRecordingRepositoryImpl(private val ncApi: NcApi, currentUserProvider: ).map { mapToStopCallRecordingModel(it.ocs?.meta!!) } } - private fun mapToStartCallRecordingModel( - response: GenericMeta - ): StartCallRecordingModel { + private fun mapToStartCallRecordingModel(response: GenericMeta): StartCallRecordingModel { val success = response.statusCode == HTTP_OK return StartCallRecordingModel( success ) } - private fun mapToStopCallRecordingModel( - response: GenericMeta - ): StopCallRecordingModel { + private fun mapToStopCallRecordingModel(response: GenericMeta): StopCallRecordingModel { val success = response.statusCode == HTTP_OK return StopCallRecordingModel( success diff --git a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt index 71a999bd5..e99340c39 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt @@ -27,15 +27,7 @@ import io.reactivex.Observable interface ReactionsRepository { - fun addReaction( - roomToken: String, - message: ChatMessage, - emoji: String - ): Observable + fun addReaction(roomToken: String, message: ChatMessage, emoji: String): Observable - fun deleteReaction( - roomToken: String, - message: ChatMessage, - emoji: String - ): Observable + fun deleteReaction(roomToken: String, message: ChatMessage, emoji: String): Observable } diff --git a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt index 2a99c3c42..55de85b81 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt @@ -36,11 +36,7 @@ class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: Cur val currentUser: User = currentUserProvider.currentUser.blockingGet() val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token) - override fun addReaction( - roomToken: String, - message: ChatMessage, - emoji: String - ): Observable { + override fun addReaction(roomToken: String, message: ChatMessage, emoji: String): Observable { return ncApi.sendReaction( credentials, ApiUtils.getUrlForMessageReaction( diff --git a/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt index 2083418ec..df475b455 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt @@ -81,8 +81,11 @@ class UnifiedSearchRepositoryImpl(private val api: NcApi, private val userProvid private const val ATTRIBUTE_CONVERSATION = "conversation" private const val ATTRIBUTE_MESSAGE_ID = "messageId" - private fun mapToMessageResults(data: UnifiedSearchResponseData, searchTerm: String, limit: Int): - UnifiedSearchRepository.UnifiedSearchResults { + private fun mapToMessageResults( + data: UnifiedSearchResponseData, + searchTerm: String, + limit: Int + ): UnifiedSearchRepository.UnifiedSearchResults { val entries = data.entries?.map { it -> mapToMessage(it, searchTerm) } val cursor = data.cursor ?: 0 val hasMore = entries?.size == limit diff --git a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt index 134173198..2bf2c3d47 100644 --- a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt @@ -27,6 +27,7 @@ package com.nextcloud.talk.settings import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint import android.app.KeyguardManager import android.content.Context import android.content.DialogInterface @@ -51,6 +52,7 @@ import android.view.View import android.view.WindowManager import android.widget.EditText import android.widget.LinearLayout +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.view.ContextThemeWrapper import androidx.core.content.ContextCompat @@ -66,6 +68,7 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.activities.MainActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.setAppTheme @@ -465,17 +468,47 @@ class SettingsActivity : BaseActivity() { } } + @SuppressLint("CheckResult") private fun removeCurrentAccount() { - val otherUserExists = userManager.scheduleUserForDeletionWithId(currentUser!!.id!!).blockingGet() + userManager.scheduleUserForDeletionWithId(currentUser!!.id!!).blockingGet() val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) - if (otherUserExists) { - // TODO: find better solution once Conductor is removed - finish() - startActivity(intent) - } else if (!otherUserExists) { - Log.d(TAG, "No other users found. AccountRemovalWorker will restart the app.") - } + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + .observeForever { workInfo: WorkInfo -> + + when (workInfo.state) { + WorkInfo.State.SUCCEEDED -> { + val text = String.format( + context.resources.getString(R.string.nc_deleted_user), + currentUser!!.displayName + ) + Toast.makeText( + context, + text, + Toast.LENGTH_LONG + ).show() + restartApp() + } + WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "something went wrong when deleting user with id " + currentUser!!.userId) + restartApp() + } + + else -> {} + } + } + } + + private fun restartApp() { + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) } private fun getRingtoneName(context: Context, ringtoneUri: Uri?): String { @@ -554,7 +587,9 @@ class SettingsActivity : BaseActivity() { } } - if (("No proxy" == appPreferences.proxyType) || appPreferences.proxyType == null) { + if (((context.resources.getString(R.string.nc_no_proxy)) == appPreferences.proxyType) || + appPreferences.proxyType == null + ) { hideProxySettings() } else { showProxySettings() @@ -954,7 +989,7 @@ class SettingsActivity : BaseActivity() { proxyTypeFlow.collect { newString -> if (newString != state) { state = newString - if (("No proxy" == newString) || newString.isEmpty()) { + if (((context.resources.getString(R.string.nc_no_proxy)) == newString) || newString.isEmpty()) { hideProxySettings() } else { when (newString) { @@ -1205,7 +1240,7 @@ class SettingsActivity : BaseActivity() { } companion object { - private const val TAG = "SettingsController" + private val TAG = SettingsActivity::class.java.simpleName private const val DURATION: Long = 2500 private const val START_DELAY: Long = 5000 private const val DISABLED_ALPHA: Float = 0.38f diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt index 89353f611..0860d159b 100644 --- a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt @@ -28,16 +28,9 @@ import io.reactivex.Observable interface SharedItemsRepository { - fun media( - parameters: Parameters, - type: SharedItemType - ): Observable? + fun media(parameters: Parameters, type: SharedItemType): Observable? - fun media( - parameters: Parameters, - type: SharedItemType, - lastKnownMessageId: Int? - ): Observable? + fun media(parameters: Parameters, type: SharedItemType, lastKnownMessageId: Int?): Observable? fun availableTypes(parameters: Parameters): Observable> diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt index ba422c1ab..21d7419ca 100644 --- a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt @@ -48,10 +48,7 @@ import javax.inject.Inject class SharedItemsRepositoryImpl @Inject constructor(private val ncApi: NcApi, private val dateUtils: DateUtils) : SharedItemsRepository { - override fun media( - parameters: SharedItemsRepository.Parameters, - type: SharedItemType - ): Observable? { + override fun media(parameters: SharedItemsRepository.Parameters, type: SharedItemType): Observable? { return media(parameters, type, null) } diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt b/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt index d5d7b81f0..96dbf5b00 100644 --- a/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt @@ -98,10 +98,11 @@ class SharedItemsViewModel @Inject constructor( }) } - private fun chooseInitialType(newTypes: Set): SharedItemType = when { - newTypes.contains(SharedItemType.MEDIA) -> SharedItemType.MEDIA - else -> newTypes.toList().first() - } + private fun chooseInitialType(newTypes: Set): SharedItemType = + when { + newTypes.contains(SharedItemType.MEDIA) -> SharedItemType.MEDIA + else -> newTypes.toList().first() + } fun initialLoadItems(type: SharedItemType) { val state = _viewState.value diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepository.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepository.kt index 0e925aee8..d464e8984 100644 --- a/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepository.kt @@ -1,5 +1,6 @@ package com.nextcloud.talk.translate.repositories +import com.nextcloud.talk.translate.repositories.model.Language import io.reactivex.Observable interface TranslateRepository { @@ -11,4 +12,6 @@ interface TranslateRepository { toLanguage: String, fromLanguage: String? ): Observable + + fun getLanguages(authorization: String, url: String): Observable> } diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepositoryImpl.kt index 9a81d9248..8828f33ab 100644 --- a/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepositoryImpl.kt @@ -1,6 +1,7 @@ package com.nextcloud.talk.translate.repositories import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.translate.repositories.model.Language import io.reactivex.Observable import javax.inject.Inject @@ -15,4 +16,8 @@ class TranslateRepositoryImpl @Inject constructor(private val ncApi: NcApi) : Tr ): Observable { return ncApi.translateMessage(authorization, url, text, toLanguage, fromLanguage).map { it.ocs?.data!!.text } } + + override fun getLanguages(authorization: String, url: String): Observable> { + return ncApi.getLanguages(authorization, url).map { it.ocs?.data?.languages } + } } diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/Language.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/Language.kt new file mode 100644 index 000000000..15287dab2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/Language.kt @@ -0,0 +1,41 @@ +/* + * Nextcloud Talk application + * + * @author Julius Linus + * Copyright (C) 2023 Julius Linus + * + * 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 . + */ +package com.nextcloud.talk.translate.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Language( + @JsonField(name = ["from"]) + var from: String?, + @JsonField(name = ["fromLabel"]) + var fromLabel: String?, + @JsonField(name = ["to"]) + var to: String?, + @JsonField(name = ["toLabel"]) + var toLabel: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesData.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesData.kt new file mode 100644 index 000000000..3cd2f2206 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesData.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud Talk application + * + * @author Julius Linus + * Copyright (C) 2023 Julius Linus + * + * 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 . + */ +package com.nextcloud.talk.translate.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class LanguagesData( + @JsonField(name = ["languageDetection"]) + var languageDetection: Boolean?, + @JsonField(name = ["languages"]) + var languages: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOCS.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOCS.kt new file mode 100644 index 000000000..f8c88edd8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOCS.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk application + * + * @author Julius Linus + * Copyright (C) 2023 Julius Linus + * + * 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 . + */ +package com.nextcloud.talk.translate.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class LanguagesOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: LanguagesData? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOverall.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOverall.kt new file mode 100644 index 000000000..1e8a35b90 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOverall.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk application + * + * @author Julius Linus + * Copyright (C) 2023 Julius Linus + * + * 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 . + */ +package com.nextcloud.talk.translate.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +class LanguagesOverall( + @JsonField(name = ["ocs"]) + var ocs: LanguagesOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateOCS.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateOCS.kt index 66cde16d3..617eb20a1 100644 --- a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateOCS.kt +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateOCS.kt @@ -27,7 +27,7 @@ import kotlinx.parcelize.Parcelize @Parcelize @JsonObject -data class TranslateOCS( // TODO finish this model +data class TranslateOCS( @JsonField(name = ["meta"]) var meta: GenericMeta?, @JsonField(name = ["data"]) diff --git a/app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt b/app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt index a65dd45db..9ba98ee99 100644 --- a/app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt @@ -28,6 +28,7 @@ import android.content.Context import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.text.method.ScrollingMovementMethod +import android.util.Log import android.view.View import android.widget.AdapterView import android.widget.ArrayAdapter @@ -38,14 +39,14 @@ import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.databinding.ActivityTranslateBinding +import com.nextcloud.talk.translate.repositories.model.Language import com.nextcloud.talk.translate.viewmodels.TranslateViewModel import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.bundle.BundleKeys -import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew -import org.json.JSONArray import java.util.Locale import javax.inject.Inject +@Suppress("TooManyFunctions") @AutoInjector(NextcloudTalkApplication::class) class TranslateActivity : BaseActivity() { @@ -60,6 +61,7 @@ class TranslateActivity : BaseActivity() { private var toLanguages: Array? = null private var fromLanguages: Array? = null + private var languages: List? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -79,8 +81,20 @@ class TranslateActivity : BaseActivity() { onTranslatedState(state.msg) } - is TranslateViewModel.ErrorState -> { - onErrorState() + is TranslateViewModel.TranslationErrorState -> { + onTranslationErrorState() + } + + is TranslateViewModel.LanguagesErrorState -> { + onLanguagesErrorState() + } + + is TranslateViewModel.LanguagesRetrievedState -> { + Log.d(TAG, "Languages are: ${state.list}") + languages = state.list + getLanguageOptions() + setupSpinners() + setItems() } } } @@ -88,8 +102,7 @@ class TranslateActivity : BaseActivity() { setContentView(binding.root) setupSystemColors() setupTextViews() - getLanguageOptions() - setupSpinners() + viewModel.getLanguages() setupCopyButton() if (savedInstanceState == null) { @@ -102,7 +115,7 @@ class TranslateActivity : BaseActivity() { override fun onResume() { super.onResume() - setItems() + languages?.let { setItems() } } override fun onSaveInstanceState(outState: Bundle) { outState.run { @@ -155,19 +168,16 @@ class TranslateActivity : BaseActivity() { } private fun getLanguageOptions() { - val currentUser = userManager.currentUser.blockingGet() - val json = JSONArray((CapabilitiesUtilNew.getLanguages(currentUser) as ArrayList<*>).toArray()) - val fromLanguagesSet = mutableSetOf(resources.getString(R.string.translation_detect_language)) val toLanguagesSet = mutableSetOf(resources.getString(R.string.translation_device_settings)) - for (i in 0 until json.length()) { - val current = json.getJSONObject(i) - if (current.getString(FROM_ID) != Locale.getDefault().language) { - toLanguagesSet.add(current.getString(FROM_LABEL)) + for (language in languages!!) { + val locale = Locale.getDefault().language + if (language.from != locale) { + toLanguagesSet.add(language.fromLabel!!) } - fromLanguagesSet.add(current.getString(TO_LABEL)) + fromLanguagesSet.add(language.toLabel!!) } toLanguages = toLanguagesSet.toTypedArray() @@ -179,7 +189,7 @@ class TranslateActivity : BaseActivity() { binding.toLanguageInputLayout.isEnabled = value } - private fun showDialog() { + private fun showDialog(titleInt: Int, messageInt: Int) { val dialogBuilder = MaterialAlertDialogBuilder(this@TranslateActivity) .setIcon( viewThemeUtils.dialog.colorMaterialAlertDialogIcon( @@ -187,8 +197,8 @@ class TranslateActivity : BaseActivity() { R.drawable.ic_warning_white ) ) - .setTitle(R.string.translation_error_title) - .setMessage(R.string.translation_error_message) + .setTitle(titleInt) + .setMessage(messageInt) .setPositiveButton(R.string.nc_ok) { dialog, _ -> dialog.dismiss() } @@ -210,18 +220,15 @@ class TranslateActivity : BaseActivity() { return getISOFromServer(language) } - private fun getISOFromServer(language: String): String { - val currentUser = userManager.currentUser.blockingGet() - val json = JSONArray((CapabilitiesUtilNew.getLanguages(currentUser) as ArrayList<*>).toArray()) - - for (i in 0 until json.length()) { - val current = json.getJSONObject(i) - if (current.getString(FROM_LABEL) == language) { - return current.getString(FROM_ID) + private fun getISOFromServer(label: String): String { + var result = "" + for (language in languages!!) { + if (language.fromLabel == label) { + result = language.from!! } } - return "" + return result } private fun setupSpinners() { @@ -279,15 +286,19 @@ class TranslateActivity : BaseActivity() { enableSpinners(true) } - private fun onErrorState() { + private fun onTranslationErrorState() { binding.progressBar.visibility = View.GONE enableSpinners(true) - showDialog() + showDialog(R.string.translation_error_title, R.string.translation_error_message) + } + + private fun onLanguagesErrorState() { + binding.progressBar.visibility = View.GONE + enableSpinners(true) + showDialog(R.string.languages_error_title, R.string.languages_error_message) } companion object { - private const val FROM_ID = "from" - private const val FROM_LABEL = "fromLabel" - private const val TO_LABEL = "toLabel" + val TAG = TranslateActivity::class.simpleName } } diff --git a/app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt b/app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt index a8952fde9..99a40e3bc 100644 --- a/app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.translate.repositories.TranslateRepository +import com.nextcloud.talk.translate.repositories.model.Language import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils import io.reactivex.Observer @@ -21,9 +22,14 @@ class TranslateViewModel @Inject constructor( sealed interface ViewState - object StartState : ViewState + data object StartState : ViewState class TranslatedState(val msg: String) : ViewState - object ErrorState : ViewState + + class LanguagesRetrievedState(val list: List) : ViewState + + data object LanguagesErrorState : ViewState + + data object TranslationErrorState : ViewState private val _viewState: MutableLiveData = MutableLiveData(StartState) val viewState: LiveData @@ -33,7 +39,12 @@ class TranslateViewModel @Inject constructor( val currentUser: User = userManager.currentUser.blockingGet() val authorization: String = ApiUtils.getCredentials(currentUser.username, currentUser.token) val url: String = ApiUtils.getUrlForTranslation(currentUser.baseUrl) - val calculatedFromLanguage = if (fromLanguage == null || fromLanguage == "") { null } else { fromLanguage } + val calculatedFromLanguage = + if (fromLanguage == null || fromLanguage == "") { + null + } else { + fromLanguage + } Log.i(TAG, "translateMessage Called") repository.translateMessage( authorization, @@ -47,6 +58,35 @@ class TranslateViewModel @Inject constructor( ?.subscribe(TranslateObserver()) } + fun getLanguages() { + val currentUser: User = userManager.currentUser.blockingGet() + val authorization: String = ApiUtils.getCredentials(currentUser.username, currentUser.token) + val url: String = ApiUtils.getUrlForLanguages(currentUser.baseUrl) + Log.d(TAG, "URL is: $url") + repository.getLanguages(authorization, url) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer> { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onError(e: Throwable) { + _viewState.value = LanguagesErrorState + Log.e(TAG, "Error while retrieving languages: $e") + } + + override fun onComplete() { + // unused atm + } + + override fun onNext(list: List) { + _viewState.value = LanguagesRetrievedState(list) + Log.d(TAG, "Languages retrieved: $list") + } + }) + } + inner class TranslateObserver : Observer { override fun onSubscribe(d: Disposable) { _viewState.value = StartState @@ -57,7 +97,7 @@ class TranslateViewModel @Inject constructor( } override fun onError(e: Throwable) { - _viewState.value = ErrorState + _viewState.value = TranslationErrorState Log.e(TAG, "Error while translating message", e) } diff --git a/app/src/main/java/com/nextcloud/talk/ui/MicInputCloud.kt b/app/src/main/java/com/nextcloud/talk/ui/MicInputCloud.kt index 4311583ef..cc1ce661c 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/MicInputCloud.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/MicInputCloud.kt @@ -156,10 +156,6 @@ class MicInputCloud(context: Context, attrs: AttributeSet) : View(context, attrs invalidate() // needed to animate the other listeners as well } } - - ovalOneAnimator?.start() - ovalTwoAnimator?.start() - ovalThreeAnimator?.start() } private fun destroyAnimators() { @@ -360,6 +356,15 @@ class MicInputCloud(context: Context, attrs: AttributeSet) : View(context, attrs invalidate() } + /** + * Starts the growing and shrinking animation + */ + fun startAnimators() { + ovalOneAnimator?.start() + ovalTwoAnimator?.start() + ovalThreeAnimator?.start() + } + companion object { val TAG: String? = MicInputCloud::class.simpleName const val DEFAULT_RADIUS: Float = 70f diff --git a/app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt b/app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt index 4a208324b..b69600fa7 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt @@ -38,7 +38,9 @@ class WaveformSeekBar : AppCompatSeekBar { @ColorInt private var secondary: Int = Color.parseColor("#a6c6f7") + private var rawData: FloatArray = floatArrayOf() private var waveData: FloatArray = floatArrayOf() + private var savedMeasure: Int = 0 private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) constructor(context: Context) : super(context) { @@ -67,12 +69,7 @@ class WaveformSeekBar : AppCompatSeekBar { * therefore, the gap is determined by the width of the seekBar by extension. */ fun setWaveData(data: FloatArray) { - val usableWidth = width - paddingLeft - paddingRight - if (usableWidth > 0) { - val numBars = if (usableWidth > VALUE_100) (usableWidth / WIDTH_DIVISOR) else usableWidth / 2f - waveData = AudioUtils.shrinkFloatArray(data, numBars.roundToInt()) - invalidate() - } + rawData = data } private fun init() { @@ -83,6 +80,17 @@ class WaveformSeekBar : AppCompatSeekBar { } } + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val usableWidth = measuredWidth - paddingLeft - paddingRight + if (usableWidth > MINIMUM_WIDTH && rawData.isNotEmpty() && usableWidth != savedMeasure) { + savedMeasure = usableWidth + val numBars = if (usableWidth > VALUE_100) (usableWidth / WIDTH_DIVISOR) else usableWidth / 2f + waveData = AudioUtils.shrinkFloatArray(rawData, numBars.roundToInt()) + invalidate() + } + } + override fun onDraw(canvas: Canvas) { if (waveData.isEmpty() || waveData[0].toString() == "NaN") { super.onDraw(canvas) @@ -123,6 +131,7 @@ class WaveformSeekBar : AppCompatSeekBar { private const val MAX_HEIGHT_DIVISOR: Float = 4.0f private const val WIDTH_DIVISOR = 20f private const val VALUE_100 = 100 + private const val MINIMUM_WIDTH = 50 private val Int.dp: Int get() = (this * Resources.getSystem().displayMetrics.density).roundToInt() } diff --git a/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt b/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt index 82be2318c..605e65b11 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt @@ -31,8 +31,8 @@ import com.afollestad.materialdialogs.bottomsheets.BottomSheet import com.nextcloud.talk.R import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.chat.ChatActivity -import com.nextcloud.talk.controllers.bottomsheet.items.BasicListItemWithImage -import com.nextcloud.talk.controllers.bottomsheet.items.listItemsWithImage +import com.nextcloud.talk.bottomsheet.items.BasicListItemWithImage +import com.nextcloud.talk.bottomsheet.items.listItemsWithImage import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.hovercard.HoverCardAction @@ -40,6 +40,7 @@ import com.nextcloud.talk.models.json.hovercard.HoverCardOverall import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet.AllowedAppIds.EMAIL import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet.AllowedAppIds.PROFILE import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet.AllowedAppIds.SPREED +import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.bundle.BundleKeys import io.reactivex.Observer @@ -49,7 +50,7 @@ import io.reactivex.schedulers.Schedulers private const val TAG = "ProfileBottomSheet" -class ProfileBottomSheet(val ncApi: NcApi, val userModel: User) { +class ProfileBottomSheet(val ncApi: NcApi, val userModel: User, val viewThemeUtils: ViewThemeUtils) { private val allowedAppIds = listOf(SPREED.stringValue, PROFILE.stringValue, EMAIL.stringValue) @@ -89,6 +90,7 @@ class ProfileBottomSheet(val ncApi: NcApi, val userModel: User) { MaterialDialog(context, BottomSheet(LayoutMode.WRAP_CONTENT)).show { cornerRadius(res = R.dimen.corner_radius) + viewThemeUtils.platform.themeDialog(this.view) title(text = displayName) listItemsWithImage(items = items) { _, index, _ -> diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java index 1e2c37cf6..4dfb482bb 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java @@ -35,7 +35,7 @@ import android.view.View; import android.view.ViewGroup; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.nextcloud.talk.activities.MainActivity; +import com.nextcloud.talk.account.ServerSelectionActivity; import com.nextcloud.talk.adapters.items.AdvancedUserItem; import com.nextcloud.talk.api.NcApi; import com.nextcloud.talk.application.NextcloudTalkApplication; @@ -71,7 +71,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import static com.nextcloud.talk.utils.bundle.BundleKeys.ADD_ACCOUNT; +import static com.nextcloud.talk.utils.bundle.BundleKeys.ADD_ADDITIONAL_ACCOUNT; @AutoInjector(NextcloudTalkApplication.class) public class ChooseAccountDialogFragment extends DialogFragment { @@ -185,11 +185,9 @@ public class ChooseAccountDialogFragment extends DialogFragment { // Creating listeners for quick-actions binding.currentAccount.getRoot().setOnClickListener(v -> dismiss()); - binding.addAccount.setOnClickListener(v -> { - // TODO: change this when conductor is removed - Intent intent = new Intent(getContext(), MainActivity.class); - intent.putExtra(ADD_ACCOUNT, true); + Intent intent = new Intent(getContext(), ServerSelectionActivity.class); + intent.putExtra(ADD_ADDITIONAL_ACCOUNT, true); startActivity(intent); dismiss(); }); diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimePickerFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimePickerFragment.kt index 886ef57f9..93a8d42db 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimePickerFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimePickerFragment.kt @@ -299,14 +299,11 @@ class DateTimePickerFragment( private const val HOUR_SIX_PM = 18 @JvmStatic - fun newInstance( - token: String, - id: String, - chatViewModel: ChatViewModel - ) = DateTimePickerFragment( - token, - id, - chatViewModel - ) + fun newInstance(token: String, id: String, chatViewModel: ChatViewModel) = + DateTimePickerFragment( + token, + id, + chatViewModel + ) } } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/FileAttachmentPreviewFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/FileAttachmentPreviewFragment.kt new file mode 100644 index 000000000..01ce6dd79 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/FileAttachmentPreviewFragment.kt @@ -0,0 +1,99 @@ +/* + * Nextcloud Talk application + * + * @author Julius Linus + * Copyright (C) 2023 Julius Linus + * + * 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 . + */ +package com.nextcloud.talk.ui.dialog + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogFileAttachmentPreviewBinding +import com.nextcloud.talk.jobs.UploadAndShareFilesWorker +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class FileAttachmentPreviewFragment( + filenames: String, + filesToUpload: MutableList, + functionToCall: (files: MutableList, caption: String) -> Unit +) : DialogFragment() { + private val files = filenames + private val filesList = filesToUpload + private val uploadFiles = functionToCall + lateinit var binding: DialogFileAttachmentPreviewBinding + + @Inject + lateinit var permissionUtil: PlatformPermissionUtil + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogFileAttachmentPreviewBinding.inflate(LayoutInflater.from(context)) + return MaterialAlertDialogBuilder(requireContext()).setView(binding.root).create() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + setUpViews() + setUpListeners() + return inflater.inflate(R.layout.dialog_file_attachment_preview, container, false) + } + + private fun setUpViews() { + binding.dialogFileAttachmentPreviewFilenames.text = files + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.buttonClose) + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.buttonSend) + viewThemeUtils.platform.colorViewBackground(binding.root) + viewThemeUtils.material.colorTextInputLayout(binding.dialogFileAttachmentPreviewLayout) + } + + private fun setUpListeners() { + binding.buttonClose.setOnClickListener { + dismiss() + } + + binding.buttonSend.setOnClickListener { + if (permissionUtil.isFilesPermissionGranted()) { + val caption: String = binding.dialogFileAttachmentPreviewCaption.text.toString() + uploadFiles(filesList, caption) + } else { + UploadAndShareFilesWorker.requestStoragePermission(requireActivity()) + } + dismiss() + } + } + + companion object { + @JvmStatic + fun newInstance( + filenames: String, + filesToUpload: MutableList, + functionToCall: (files: MutableList, caption: String) -> Unit + ) = FileAttachmentPreviewFragment(filenames, filesToUpload, functionToCall) + val TAG: String = FilterConversationFragment::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt index aa8061e42..a990a0c81 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt @@ -62,11 +62,7 @@ class FilterConversationFragment( return MaterialAlertDialogBuilder(requireContext()).setView(dialogView).create() } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) setUpColors() setUpListeners() diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt index 128028364..4860ff00b 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt @@ -52,7 +52,6 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import org.json.JSONArray import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -87,8 +86,7 @@ class MessageActionsDialog( initMenuItemTranslate( !message.isDeleted && ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() && - CapabilitiesUtilNew.isTranslationsSupported(user) && - JSONArray((CapabilitiesUtilNew.getLanguages(user) as ArrayList<*>).toArray()).length() > 0 + CapabilitiesUtilNew.isTranslationsSupported(user) ) initMenuReplyToMessage(message.replyable && hasChatPermission) initMenuReplyPrivately( diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt new file mode 100644 index 000000000..e5d8def95 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt @@ -0,0 +1,129 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * @author Fariba Khandani + * Copyright (C) 2023 Marcel Hibbe (dev@mhibbe.de) + * Copyright (C) 2023 Fariba Khandani + * + * 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 . + */ +package com.nextcloud.talk.ui.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.jobs.SaveFileToStorageWorker +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import java.util.concurrent.ExecutionException +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class SaveToStorageDialogFragment : DialogFragment() { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + lateinit var fileName: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + fileName = arguments?.getString(KEY_FILE_NAME)!! + } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialogText = StringBuilder() + dialogText.append(resources.getString(R.string.nc_dialog_save_to_storage_content)) + dialogText.append("\n") + dialogText.append("\n") + dialogText.append(resources.getString(R.string.nc_dialog_save_to_storage_continue)) + + val dialogBuilder = MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.nc_dialog_save_to_storage_title) + .setMessage(dialogText) + .setPositiveButton(R.string.nc_dialog_save_to_storage_yes) { _: DialogInterface?, _: Int -> + saveImageToStorage(fileName) + } + .setNegativeButton(R.string.nc_dialog_save_to_storage_no) { _: DialogInterface?, _: Int -> + } + viewThemeUtils.dialog.colorMaterialAlertDialogBackground( + requireContext(), + dialogBuilder + ) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + + return dialog + } + + @SuppressLint("LongLogTag") + private fun saveImageToStorage(fileName: String) { + val sourceFilePath = requireContext().cacheDir.path + val workerTag = SAVE_TO_STORAGE_WORKER_PREFIX + fileName + + val workers = WorkManager.getInstance(requireContext()).getWorkInfosByTag(workerTag) + try { + for (workInfo in workers.get()) { + if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { + return + } + } + } catch (e: ExecutionException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } catch (e: InterruptedException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } + + val data: Data = Data.Builder() + .putString(SaveFileToStorageWorker.KEY_FILE_NAME, fileName) + .putString(SaveFileToStorageWorker.KEY_SOURCE_FILE_PATH, "$sourceFilePath/$fileName") + .build() + + val saveWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SaveFileToStorageWorker::class.java) + .setInputData(data) + .addTag(workerTag) + .build() + + WorkManager.getInstance().enqueue(saveWorker) + } + + companion object { + val TAG = SaveToStorageDialogFragment::class.java.simpleName + private const val KEY_FILE_NAME = "keyFileName" + private const val SAVE_TO_STORAGE_WORKER_PREFIX = "saveToStorage_" + + fun newInstance(fileName: String): SaveToStorageDialogFragment { + val args = Bundle() + args.putString(KEY_FILE_NAME, fileName) + val fragment = SaveToStorageDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt index 1900a4925..a7ae172a2 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt @@ -337,9 +337,7 @@ class SetStatusDialogFragment : DialogFragment(), PredefinedStatusClickListener return returnValue } - private fun clearAtToUnixTimeTypeEndOf( - clearAt: ClearAt - ): Long { + private fun clearAtToUnixTimeTypeEndOf(clearAt: ClearAt): Long { var returnValue = -1L if (clearAt.time == "day") { val date = Calendar.getInstance().apply { diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt index 99faa5efc..d42b1a242 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt @@ -295,11 +295,7 @@ class TalkSpecificViewThemeUtils @Inject constructor( } } - fun themeAndHighlightText( - textView: TextView, - originalText: String?, - c: String? - ) { + fun themeAndHighlightText(textView: TextView, originalText: String?, c: String?) { withScheme(textView) { scheme -> var constraint = c constraint = FlexibleUtils.toLowerCase(constraint) @@ -374,11 +370,7 @@ class TalkSpecificViewThemeUtils @Inject constructor( } } - fun getTextColor( - isOutgoingMessage: Boolean, - isSelfReaction: Boolean, - binding: ReactionsInsideMessageBinding - ): Int { + fun getTextColor(isOutgoingMessage: Boolean, isSelfReaction: Boolean, binding: ReactionsInsideMessageBinding): Int { return withScheme(binding.root) { scheme -> return@withScheme if (!isOutgoingMessage || isSelfReaction) { ContextCompat.getColor(binding.root.context, R.color.high_emphasis_text) diff --git a/app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt b/app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt index 018305951..6c7021371 100644 --- a/app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt +++ b/app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt @@ -83,11 +83,7 @@ class ChunkedFileUploader( } @Suppress("Detekt.TooGenericExceptionCaught") - fun upload( - localFile: File, - mimeType: MediaType?, - targetPath: String - ): Boolean { + fun upload(localFile: File, mimeType: MediaType?, targetPath: String): Boolean { try { val uploadFolderUri: String = remoteChunkUrl + "/" + FileUtils.md5Sum(localFile) val davResource = DavResource( @@ -137,10 +133,7 @@ class ChunkedFileUploader( } @Suppress("Detekt.ComplexMethod") - private fun getUploadedChunks( - davResource: DavResource, - uploadFolderUri: String - ): MutableList { + private fun getUploadedChunks(davResource: DavResource, uploadFolderUri: String): MutableList { val davResponse = DavResponse() val memberElements: MutableList = ArrayList() val rootElement = arrayOfNulls(1) diff --git a/app/src/main/java/com/nextcloud/talk/upload/chunked/OnDataTransferProgressListener.kt b/app/src/main/java/com/nextcloud/talk/upload/chunked/OnDataTransferProgressListener.kt index 049fee4fb..6ba463342 100644 --- a/app/src/main/java/com/nextcloud/talk/upload/chunked/OnDataTransferProgressListener.kt +++ b/app/src/main/java/com/nextcloud/talk/upload/chunked/OnDataTransferProgressListener.kt @@ -25,7 +25,5 @@ package com.nextcloud.talk.upload.chunked interface OnDataTransferProgressListener { - fun onTransferProgress( - percentage: Int - ) + fun onTransferProgress(percentage: Int) } diff --git a/app/src/main/java/com/nextcloud/talk/upload/normal/FileUploader.kt b/app/src/main/java/com/nextcloud/talk/upload/normal/FileUploader.kt index 514aae0da..d21f90605 100644 --- a/app/src/main/java/com/nextcloud/talk/upload/normal/FileUploader.kt +++ b/app/src/main/java/com/nextcloud/talk/upload/normal/FileUploader.kt @@ -21,12 +21,7 @@ class FileUploader( val roomToken: String, val ncApi: NcApi ) { - fun upload( - sourceFileUri: Uri, - fileName: String, - remotePath: String, - metaData: String? - ): Observable { + fun upload(sourceFileUri: Uri, fileName: String, remotePath: String, metaData: String?): Observable { return ncApi.uploadFile( ApiUtils.getCredentials(currentUser.username, currentUser.token), ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, remotePath), diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 9cac57436..298ced754 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -247,7 +247,7 @@ public class ApiUtils { } public static String getUrlForRoomWebinaryLobby(int version, String baseUrl, String token) { - return getUrlForRoom(version, baseUrl, token) + "/webinary/lobby"; + return getUrlForRoom(version, baseUrl, token) + "/webinar/lobby"; } public static String getUrlForRoomNotificationCalls(int version, String baseUrl, String token) { @@ -532,6 +532,10 @@ public class ApiUtils { return baseUrl + ocsApiVersion + "/translation/translate"; } + public static String getUrlForLanguages(String baseUrl) { + return baseUrl + ocsApiVersion + "/translation/languages"; + } + public static String getUrlForReminder(User user, String roomToken, String messageId, int version) { String url = ApiUtils.getUrlForChatMessage(version, user.getBaseUrl(), roomToken, messageId); return url + "/reminder"; diff --git a/app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt b/app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt index 9f9635f8d..008b2355d 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt @@ -35,21 +35,13 @@ object BitmapShrinker { private const val DEGREES_270 = 270f @JvmStatic - fun shrinkBitmap( - path: String, - reqWidth: Int, - reqHeight: Int - ): Bitmap { + fun shrinkBitmap(path: String, reqWidth: Int, reqHeight: Int): Bitmap { val bitmap = decodeBitmap(path, reqWidth, reqHeight) return rotateBitmap(path, bitmap) } // solution inspired by https://developer.android.com/topic/performance/graphics/load-bitmap - private fun decodeBitmap( - path: String, - requestedWidth: Int, - requestedHeight: Int - ): Bitmap { + private fun decodeBitmap(path: String, requestedWidth: Int, requestedHeight: Int): Bitmap { return BitmapFactory.Options().run { inJustDecodeBounds = true BitmapFactory.decodeFile(path, this) @@ -60,11 +52,7 @@ object BitmapShrinker { } // solution inspired by https://developer.android.com/topic/performance/graphics/load-bitmap - private fun getInSampleSize( - options: BitmapFactory.Options, - requestedWidth: Int, - requestedHeight: Int - ): Int { + private fun getInSampleSize(options: BitmapFactory.Options, requestedWidth: Int, requestedHeight: Int): Int { val (height: Int, width: Int) = options.run { outHeight to outWidth } var inSampleSize = 1 if (height > requestedHeight || width > requestedWidth) { diff --git a/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt index 6f94209e2..af5c3f600 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt @@ -37,14 +37,17 @@ class DateUtils(val context: Context) { /* date formatter in local timezone and locale */ private var format: DateFormat = DateFormat.getDateTimeInstance( - DateFormat.DEFAULT, // dateStyle - DateFormat.SHORT, // timeStyle + // dateStyle + DateFormat.DEFAULT, + // timeStyle + DateFormat.SHORT, context.resources.configuration.locales[0] ) /* date formatter in local timezone and locale */ private var formatTime: DateFormat = DateFormat.getTimeInstance( - DateFormat.SHORT, // timeStyle + // timeStyle + DateFormat.SHORT, context.resources.configuration.locales[0] ) diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index 4682f50fc..c964645e8 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java @@ -89,12 +89,12 @@ import coil.target.Target; import coil.transform.CircleCropTransformation; import third.parties.fresco.BetterImageSpan; -import static com.nextcloud.talk.utils.FileSortOrder.sort_a_to_z_id; -import static com.nextcloud.talk.utils.FileSortOrder.sort_big_to_small_id; -import static com.nextcloud.talk.utils.FileSortOrder.sort_new_to_old_id; -import static com.nextcloud.talk.utils.FileSortOrder.sort_old_to_new_id; -import static com.nextcloud.talk.utils.FileSortOrder.sort_small_to_big_id; -import static com.nextcloud.talk.utils.FileSortOrder.sort_z_to_a_id; +import static com.nextcloud.talk.utils.FileSortOrder.SORT_A_TO_Z_ID; +import static com.nextcloud.talk.utils.FileSortOrder.SORT_BIG_TO_SMALL_ID; +import static com.nextcloud.talk.utils.FileSortOrder.SORT_NEW_TO_OLD_ID; +import static com.nextcloud.talk.utils.FileSortOrder.SORT_OLD_TO_NEW_ID; +import static com.nextcloud.talk.utils.FileSortOrder.SORT_SMALL_TO_BIG_ID; +import static com.nextcloud.talk.utils.FileSortOrder.SORT_Z_TO_A_ID; public class DisplayUtils { private static final String TAG = DisplayUtils.class.getSimpleName(); @@ -476,17 +476,17 @@ public class DisplayUtils { public static @StringRes int getSortOrderStringId(FileSortOrder sortOrder) { switch (sortOrder.getName()) { - case sort_z_to_a_id: + case SORT_Z_TO_A_ID: return R.string.menu_item_sort_by_name_z_a; - case sort_new_to_old_id: + case SORT_NEW_TO_OLD_ID: return R.string.menu_item_sort_by_date_newest_first; - case sort_old_to_new_id: + case SORT_OLD_TO_NEW_ID: return R.string.menu_item_sort_by_date_oldest_first; - case sort_big_to_small_id: + case SORT_BIG_TO_SMALL_ID: return R.string.menu_item_sort_by_size_biggest_first; - case sort_small_to_big_id: + case SORT_SMALL_TO_BIG_ID: return R.string.menu_item_sort_by_size_smallest_first; - case sort_a_to_z_id: + case SORT_A_TO_Z_ID: default: return R.string.menu_item_sort_by_name_a_z; } diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.kt b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.kt index f3bfffb15..405e76635 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.kt @@ -27,19 +27,19 @@ import java.util.Collections open class FileSortOrder(var name: String, var isAscending: Boolean) { companion object { - const val sort_a_to_z_id = "sort_a_to_z" - const val sort_z_to_a_id = "sort_z_to_a" - const val sort_old_to_new_id = "sort_old_to_new" - const val sort_new_to_old_id = "sort_new_to_old" - const val sort_small_to_big_id = "sort_small_to_big" - const val sort_big_to_small_id = "sort_big_to_small" + const val SORT_A_TO_Z_ID = "sort_a_to_z" + const val SORT_Z_TO_A_ID = "sort_z_to_a" + const val SORT_OLD_TO_NEW_ID = "sort_old_to_new" + const val SORT_NEW_TO_OLD_ID = "sort_new_to_old" + const val SORT_SMALL_TO_BIG_ID = "sort_small_to_big" + const val SORT_BIG_TO_SMALL_ID = "sort_big_to_small" - val sort_a_to_z: FileSortOrder = FileSortOrderByName(sort_a_to_z_id, true) - val sort_z_to_a: FileSortOrder = FileSortOrderByName(sort_z_to_a_id, false) - val sort_old_to_new: FileSortOrder = FileSortOrderByDate(sort_old_to_new_id, true) - val sort_new_to_old: FileSortOrder = FileSortOrderByDate(sort_new_to_old_id, false) - val sort_small_to_big: FileSortOrder = FileSortOrderBySize(sort_small_to_big_id, true) - val sort_big_to_small: FileSortOrder = FileSortOrderBySize(sort_big_to_small_id, false) + val sort_a_to_z: FileSortOrder = FileSortOrderByName(SORT_A_TO_Z_ID, true) + val sort_z_to_a: FileSortOrder = FileSortOrderByName(SORT_Z_TO_A_ID, false) + val sort_old_to_new: FileSortOrder = FileSortOrderByDate(SORT_OLD_TO_NEW_ID, true) + val sort_new_to_old: FileSortOrder = FileSortOrderByDate(SORT_NEW_TO_OLD_ID, false) + val sort_small_to_big: FileSortOrder = FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, true) + val sort_big_to_small: FileSortOrder = FileSortOrderBySize(SORT_BIG_TO_SMALL_ID, false) val sortOrders: Map = mapOf( sort_a_to_z.name to sort_a_to_z, diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt index af5f636ac..92cc4183f 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt @@ -37,9 +37,9 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import com.google.android.material.snackbar.Snackbar import com.nextcloud.talk.R -import com.nextcloud.talk.activities.FullScreenImageActivity -import com.nextcloud.talk.activities.FullScreenMediaActivity -import com.nextcloud.talk.activities.FullScreenTextViewerActivity +import com.nextcloud.talk.fullscreenfile.FullScreenImageActivity +import com.nextcloud.talk.fullscreenfile.FullScreenMediaActivity +import com.nextcloud.talk.fullscreenfile.FullScreenTextViewerActivity import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.jobs.DownloadFileToCacheWorker @@ -73,10 +73,7 @@ import java.util.concurrent.ExecutionException */ class FileViewerUtils(private val context: Context, private val user: User) { - fun openFile( - message: ChatMessage, - progressUi: ProgressUi - ) { + fun openFile(message: ChatMessage, progressUi: ProgressUi) { val fileName = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_NAME]!! val mimetype = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_MIMETYPE]!! val link = message.selectedIndividualHashMap!!["link"]!! diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt index a5682d959..68ff86ca4 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt @@ -102,10 +102,7 @@ object NotificationUtils { } } - private fun createCallsNotificationChannel( - context: Context, - appPreferences: AppPreferences - ) { + private fun createCallsNotificationChannel(context: Context, appPreferences: AppPreferences) { val audioAttributes = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) @@ -126,10 +123,7 @@ object NotificationUtils { ) } - private fun createMessagesNotificationChannel( - context: Context, - appPreferences: AppPreferences - ) { + private fun createMessagesNotificationChannel(context: Context, appPreferences: AppPreferences) { val audioAttributes = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) @@ -150,9 +144,7 @@ object NotificationUtils { ) } - private fun createUploadsNotificationChannel( - context: Context - ) { + private fun createUploadsNotificationChannel(context: Context) { createNotificationChannel( context, Channel( @@ -166,10 +158,7 @@ object NotificationUtils { ) } - fun registerNotificationChannels( - context: Context, - appPreferences: AppPreferences - ) { + fun registerNotificationChannels(context: Context, appPreferences: AppPreferences) { createCallsNotificationChannel(context, appPreferences) createMessagesNotificationChannel(context, appPreferences) createUploadsNotificationChannel(context) @@ -197,10 +186,7 @@ object NotificationUtils { } @TargetApi(Build.VERSION_CODES.O) - private fun getNotificationChannel( - context: Context, - channelId: String - ): NotificationChannel? { + private fun getNotificationChannel(context: Context, channelId: String): NotificationChannel? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager return notificationManager.getNotificationChannel(channelId) @@ -275,10 +261,7 @@ object NotificationUtils { } } - fun isNotificationVisible( - context: Context?, - notificationId: Int - ): Boolean { + fun isNotificationVisible(context: Context?, notificationId: Int): Boolean { var isVisible = false val notificationManager = context!!.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -319,10 +302,7 @@ object NotificationUtils { } } - fun getCallRingtoneUri( - context: Context, - appPreferences: AppPreferences - ): Uri? { + fun getCallRingtoneUri(context: Context, appPreferences: AppPreferences): Uri? { return getRingtoneUri( context, appPreferences.callRingtoneUri, @@ -331,10 +311,7 @@ object NotificationUtils { ) } - fun getMessageRingtoneUri( - context: Context, - appPreferences: AppPreferences - ): Uri? { + fun getMessageRingtoneUri(context: Context, appPreferences: AppPreferences): Uri? { return getRingtoneUri( context, appPreferences.messageRingtoneUri, diff --git a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java deleted file mode 100644 index 2890acfea..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Andy Scherzinger - * @author Marcel Hibbe - * @author Mario Danic - * Copyright (C) 2022 Andy Scherzinger - * Copyright (C) 2022 Marcel Hibbe - * Copyright (C) 2017 Mario Danic - * - * 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 . - */ - -package com.nextcloud.talk.utils; - -import android.content.Context; -import android.text.TextUtils; -import android.util.Base64; -import android.util.Log; - -import com.nextcloud.talk.R; -import com.nextcloud.talk.api.NcApi; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.data.user.model.User; -import com.nextcloud.talk.events.EventStatus; -import com.nextcloud.talk.models.SignatureVerification; -import com.nextcloud.talk.models.json.push.PushConfigurationState; -import com.nextcloud.talk.models.json.push.PushRegistrationOverall; -import com.nextcloud.talk.users.UserManager; -import com.nextcloud.talk.utils.preferences.AppPreferences; - -import org.greenrobot.eventbus.EventBus; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.inject.Inject; - -import autodagger.AutoInjector; -import io.reactivex.Observer; -import io.reactivex.SingleObserver; -import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -@AutoInjector(NextcloudTalkApplication.class) -public class PushUtils { - private static final String TAG = "PushUtils"; - - @Inject - UserManager userManager; - - @Inject - AppPreferences appPreferences; - - @Inject - EventBus eventBus; - - private final File publicKeyFile; - private final File privateKeyFile; - - private final String proxyServer; - - public PushUtils() { - NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); - - String keyPath = NextcloudTalkApplication - .Companion - .getSharedApplication() - .getDir("PushKeystore", Context.MODE_PRIVATE) - .getAbsolutePath(); - publicKeyFile = new File(keyPath, "push_key.pub"); - privateKeyFile = new File(keyPath, "push_key.priv"); - proxyServer = NextcloudTalkApplication - .Companion - .getSharedApplication() - .getResources(). - getString(R.string.nc_push_server_url); - } - - public SignatureVerification verifySignature(byte[] signatureBytes, byte[] subjectBytes) { - SignatureVerification signatureVerification = new SignatureVerification(); - signatureVerification.setSignatureValid(false); - - List users = userManager.getUsers().blockingGet(); - try { - Signature signature = Signature.getInstance("SHA512withRSA"); - if (users != null && users.size() > 0) { - PublicKey publicKey; - for (User user : users) { - if (user.getPushConfigurationState() != null) { - publicKey = (PublicKey) readKeyFromString(true, - user.getPushConfigurationState().getUserPublicKey()); - signature.initVerify(publicKey); - signature.update(subjectBytes); - if (signature.verify(signatureBytes)) { - signatureVerification.setSignatureValid(true); - signatureVerification.setUser(user); - return signatureVerification; - } - } - } - } - } catch (NoSuchAlgorithmException e) { - Log.d(TAG, "No such algorithm"); - } catch (InvalidKeyException e) { - Log.d(TAG, "Invalid key while trying to verify"); - } catch (SignatureException e) { - Log.d(TAG, "Signature exception while trying to verify"); - } - - return signatureVerification; - } - - private int saveKeyToFile(Key key, String path) { - byte[] encoded = key.getEncoded(); - - try { - if (!new File(path).exists()) { - if (!new File(path).createNewFile()) { - return -1; - } - } - - try (FileOutputStream keyFileOutputStream = new FileOutputStream(path)) { - keyFileOutputStream.write(encoded); - return 0; - } - } catch (FileNotFoundException e) { - Log.d(TAG, "Failed to save key to file"); - } catch (IOException e) { - Log.d(TAG, "Failed to save key to file via IOException"); - } - - return -1; - } - - private String generateSHA512Hash(String pushToken) { - MessageDigest messageDigest = null; - try { - messageDigest = MessageDigest.getInstance("SHA-512"); - messageDigest.update(pushToken.getBytes()); - return bytesToHex(messageDigest.digest()); - } catch (NoSuchAlgorithmException e) { - Log.d(TAG, "SHA-512 algorithm not supported"); - } - return ""; - } - - private String bytesToHex(byte[] bytes) { - StringBuilder result = new StringBuilder(); - for (byte individualByte : bytes) { - result.append(Integer.toString((individualByte & 0xff) + 0x100, 16) - .substring(1)); - } - return result.toString(); - } - - public int generateRsa2048KeyPair() { - if (!publicKeyFile.exists() && !privateKeyFile.exists()) { - - KeyPairGenerator keyGen = null; - try { - keyGen = KeyPairGenerator.getInstance("RSA"); - keyGen.initialize(2048); - - KeyPair pair = keyGen.generateKeyPair(); - int statusPrivate = saveKeyToFile(pair.getPrivate(), privateKeyFile.getAbsolutePath()); - int statusPublic = saveKeyToFile(pair.getPublic(), publicKeyFile.getAbsolutePath()); - - if (statusPrivate == 0 && statusPublic == 0) { - // all went well - return 0; - } else { - return -2; - } - - } catch (NoSuchAlgorithmException e) { - Log.d(TAG, "RSA algorithm not supported"); - } - } else { - // We already have the key - return -1; - } - - // we failed to generate the key - return -2; - } - - public void pushRegistrationToServer(NcApi ncApi) { - String token = appPreferences.getPushToken(); - - if (!TextUtils.isEmpty(token)) { - String pushTokenHash = generateSHA512Hash(token).toLowerCase(); - PublicKey devicePublicKey = (PublicKey) readKeyFromFile(true); - if (devicePublicKey != null) { - byte[] devicePublicKeyBytes = Base64.encode(devicePublicKey.getEncoded(), Base64.NO_WRAP); - String devicePublicKeyBase64 = new String(devicePublicKeyBytes); - devicePublicKeyBase64 = devicePublicKeyBase64.replaceAll("(.{64})", "$1\n"); - - devicePublicKeyBase64 = - "-----BEGIN PUBLIC KEY-----\n" - + devicePublicKeyBase64 - + "\n-----END PUBLIC KEY-----\n"; - - List users = userManager.getUsers().blockingGet(); - - for (User user : users) { - if (!user.getScheduledForDeletion()) { - Map nextcloudRegisterPushMap = new HashMap<>(); - nextcloudRegisterPushMap.put("format", "json"); - nextcloudRegisterPushMap.put("pushTokenHash", pushTokenHash); - nextcloudRegisterPushMap.put("devicePublicKey", devicePublicKeyBase64); - nextcloudRegisterPushMap.put("proxyServer", proxyServer); - - registerDeviceWithNextcloud(ncApi, nextcloudRegisterPushMap, token, user); - } - } - - } - } else { - Log.e(TAG, "push token was empty when trying to register at nextcloud server"); - } - } - - private void registerDeviceWithNextcloud(NcApi ncApi, - Map nextcloudRegisterPushMap, - String token, - User user) { - String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken()); - - ncApi.registerDeviceForNotificationsWithNextcloud( - credentials, - ApiUtils.getUrlNextcloudPush(user.getBaseUrl()), - nextcloudRegisterPushMap) - .subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) { - // unused atm - } - - @Override - public void onNext(@NonNull PushRegistrationOverall pushRegistrationOverall) { - Log.d(TAG, "pushTokenHash successfully registered at nextcloud server."); - - Map proxyMap = new HashMap<>(); - proxyMap.put("pushToken", token); - proxyMap.put("deviceIdentifier", - pushRegistrationOverall.getOcs().getData().getDeviceIdentifier()); - proxyMap.put("deviceIdentifierSignature", - pushRegistrationOverall.getOcs().getData().getSignature()); - proxyMap.put("userPublicKey", - pushRegistrationOverall.getOcs().getData().getPublicKey()); - - registerDeviceWithPushProxy(ncApi, proxyMap, user); - } - - @Override - public void onError(@NonNull Throwable e) { - eventBus.post(new EventStatus(user.getId(), - EventStatus.EventType.PUSH_REGISTRATION, false)); - } - - @Override - public void onComplete() { - // unused atm - } - }); - } - - private void registerDeviceWithPushProxy(NcApi ncApi, Map proxyMap, User user) { - ncApi.registerDeviceForNotificationsWithPushProxy(ApiUtils.getUrlPushProxy(), proxyMap) - .subscribeOn(Schedulers.io()) - .subscribe(new Observer() { - @Override - public void onSubscribe(@NonNull Disposable d) { - // unused atm - } - - @Override - public void onNext(@NonNull Void aVoid) { - try { - Log.d(TAG, "pushToken successfully registered at pushproxy."); - updatePushStateForUser(proxyMap, user); - } catch (IOException e) { - Log.e(TAG, "IOException while updating user", e); - } - } - - @Override - public void onError(@NonNull Throwable e) { - eventBus.post(new EventStatus(user.getId(), - EventStatus.EventType.PUSH_REGISTRATION, false)); - } - - @Override - public void onComplete() { - // unused atm - } - }); - } - - private void updatePushStateForUser(Map proxyMap, User user) throws IOException { - PushConfigurationState pushConfigurationState = new PushConfigurationState(); - pushConfigurationState.setPushToken(proxyMap.get("pushToken")); - pushConfigurationState.setDeviceIdentifier(proxyMap.get("deviceIdentifier")); - pushConfigurationState.setDeviceIdentifierSignature(proxyMap.get("deviceIdentifierSignature")); - pushConfigurationState.setUserPublicKey(proxyMap.get("userPublicKey")); - pushConfigurationState.setUsesRegularPass(Boolean.FALSE); - - if (user.getId() != null) { - userManager.updatePushState(user.getId(), pushConfigurationState).subscribe(new SingleObserver() { - @Override - public void onSubscribe(Disposable d) { - // unused atm - } - - @Override - public void onSuccess(Integer integer) { - eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user), - EventStatus.EventType.PUSH_REGISTRATION, - true)); - } - - @Override - public void onError(Throwable e) { - eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user), - EventStatus.EventType.PUSH_REGISTRATION, - false)); - } - }); - } else { - Log.e(TAG, "failed to update updatePushStateForUser. user.getId() was null"); - } - - } - - private Key readKeyFromString(boolean readPublicKey, String keyString) { - if (readPublicKey) { - keyString = keyString.replaceAll("\\n", "").replace("-----BEGIN PUBLIC KEY-----", - "").replace("-----END PUBLIC KEY-----", ""); - } else { - keyString = keyString.replaceAll("\\n", "").replace("-----BEGIN PRIVATE KEY-----", - "").replace("-----END PRIVATE KEY-----", ""); - } - - KeyFactory keyFactory = null; - try { - keyFactory = KeyFactory.getInstance("RSA"); - if (readPublicKey) { - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT)); - return keyFactory.generatePublic(keySpec); - } else { - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT)); - return keyFactory.generatePrivate(keySpec); - } - - } catch (NoSuchAlgorithmException e) { - Log.d(TAG, "No such algorithm while reading key from string"); - } catch (InvalidKeySpecException e) { - Log.d(TAG, "Invalid key spec while reading key from string"); - } - - return null; - } - - public Key readKeyFromFile(boolean readPublicKey) { - String path; - - if (readPublicKey) { - path = publicKeyFile.getAbsolutePath(); - } else { - path = privateKeyFile.getAbsolutePath(); - } - - try (FileInputStream fileInputStream = new FileInputStream(path)) { - byte[] bytes = new byte[fileInputStream.available()]; - fileInputStream.read(bytes); - - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - - if (readPublicKey) { - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes); - return keyFactory.generatePublic(keySpec); - } else { - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); - return keyFactory.generatePrivate(keySpec); - } - - } catch (FileNotFoundException e) { - Log.d(TAG, "Failed to find path while reading the Key"); - } catch (IOException e) { - Log.d(TAG, "IOException while reading the key"); - } catch (InvalidKeySpecException e) { - Log.d(TAG, "InvalidKeySpecException while reading the key"); - } catch (NoSuchAlgorithmException e) { - Log.d(TAG, "RSA algorithm not supported"); - } - - return null; - } -} diff --git a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt new file mode 100644 index 000000000..5e7dbe4e1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt @@ -0,0 +1,400 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Marcel Hibbe + * @author Mario Danic + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2022 Marcel Hibbe + * Copyright (C) 2017 Mario Danic + * + * 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 . + */ +package com.nextcloud.talk.utils + +import android.content.Context +import android.util.Base64 +import android.util.Log +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.events.EventStatus +import com.nextcloud.talk.models.SignatureVerification +import com.nextcloud.talk.models.json.push.PushConfigurationState +import com.nextcloud.talk.models.json.push.PushRegistrationOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.UserIdUtils.getIdForUser +import com.nextcloud.talk.utils.preferences.AppPreferences +import io.reactivex.Observer +import io.reactivex.SingleObserver +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import org.greenrobot.eventbus.EventBus +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.security.InvalidKeyException +import java.security.Key +import java.security.KeyFactory +import java.security.KeyPairGenerator +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.PublicKey +import java.security.Signature +import java.security.SignatureException +import java.security.spec.InvalidKeySpecException +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec +import java.util.Locale +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PushUtils { + @JvmField + @Inject + var userManager: UserManager? = null + + @Inject + lateinit var appPreferences: AppPreferences + + @JvmField + @Inject + var eventBus: EventBus? = null + private val publicKeyFile: File + private val privateKeyFile: File + private val proxyServer: String + + init { + sharedApplication!!.componentApplication.inject(this) + val keyPath = sharedApplication!! + .getDir("PushKeystore", Context.MODE_PRIVATE) + .absolutePath + publicKeyFile = File(keyPath, "push_key.pub") + privateKeyFile = File(keyPath, "push_key.priv") + proxyServer = sharedApplication!! + .resources.getString(R.string.nc_push_server_url) + } + + fun verifySignature(signatureBytes: ByteArray?, subjectBytes: ByteArray?): SignatureVerification { + val signatureVerification = SignatureVerification() + signatureVerification.signatureValid = false + val users = userManager!!.users.blockingGet() + try { + val signature = Signature.getInstance("SHA512withRSA") + if (users != null && users.size > 0) { + var publicKey: PublicKey? + for (user in users) { + if (user.pushConfigurationState != null) { + publicKey = readKeyFromString( + true, + user.pushConfigurationState!!.userPublicKey + ) as PublicKey? + signature.initVerify(publicKey) + signature.update(subjectBytes) + if (signature.verify(signatureBytes)) { + signatureVerification.signatureValid = true + signatureVerification.user = user + return signatureVerification + } + } + } + } + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "No such algorithm") + } catch (e: InvalidKeyException) { + Log.d(TAG, "Invalid key while trying to verify") + } catch (e: SignatureException) { + Log.d(TAG, "Signature exception while trying to verify") + } + return signatureVerification + } + + private fun saveKeyToFile(key: Key, path: String): Int { + val encoded = key.encoded + try { + if (!File(path).exists()) { + if (!File(path).createNewFile()) { + return -1 + } + } + FileOutputStream(path).use { keyFileOutputStream -> + keyFileOutputStream.write(encoded) + return 0 + } + } catch (e: FileNotFoundException) { + Log.d(TAG, "Failed to save key to file") + } catch (e: IOException) { + Log.d(TAG, "Failed to save key to file via IOException") + } + return -1 + } + + private fun generateSHA512Hash(pushToken: String): String { + var messageDigest: MessageDigest? = null + try { + messageDigest = MessageDigest.getInstance("SHA-512") + messageDigest.update(pushToken.toByteArray()) + return bytesToHex(messageDigest.digest()) + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "SHA-512 algorithm not supported") + } + return "" + } + + private fun bytesToHex(bytes: ByteArray): String { + val result = StringBuilder() + for (individualByte in bytes) { + result.append( + Integer.toString((individualByte.toInt() and 0xff) + 0x100, 16) + .substring(1) + ) + } + return result.toString() + } + + fun generateRsa2048KeyPair(): Int { + if (!publicKeyFile.exists() && !privateKeyFile.exists()) { + var keyGen: KeyPairGenerator? = null + try { + keyGen = KeyPairGenerator.getInstance("RSA") + keyGen.initialize(2048) + val pair = keyGen.generateKeyPair() + val statusPrivate = saveKeyToFile(pair.private, privateKeyFile.absolutePath) + val statusPublic = saveKeyToFile(pair.public, publicKeyFile.absolutePath) + return if (statusPrivate == 0 && statusPublic == 0) { + // all went well + 0 + } else { + -2 + } + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "RSA algorithm not supported") + } + } else { + // We already have the key + return -1 + } + + // we failed to generate the key + return -2 + } + + fun pushRegistrationToServer(ncApi: NcApi) { + val pushToken = appPreferences.pushToken + + if (pushToken.isNotEmpty()) { + Log.d(TAG, "pushRegistrationToServer will be done with pushToken: $pushToken") + val pushTokenHash = generateSHA512Hash(pushToken).lowercase(Locale.getDefault()) + val devicePublicKey = readKeyFromFile(true) as PublicKey? + if (devicePublicKey != null) { + val devicePublicKeyBytes = Base64.encode(devicePublicKey.encoded, Base64.NO_WRAP) + var devicePublicKeyBase64 = String(devicePublicKeyBytes) + devicePublicKeyBase64 = devicePublicKeyBase64.replace("(.{64})".toRegex(), "$1\n") + devicePublicKeyBase64 = "-----BEGIN PUBLIC KEY-----\n$devicePublicKeyBase64\n-----END PUBLIC KEY-----" + + val users = userManager!!.users.blockingGet() + for (user in users) { + if (!user.scheduledForDeletion) { + val nextcloudRegisterPushMap: MutableMap = HashMap() + nextcloudRegisterPushMap["format"] = "json" + nextcloudRegisterPushMap["pushTokenHash"] = pushTokenHash + nextcloudRegisterPushMap["devicePublicKey"] = devicePublicKeyBase64 + nextcloudRegisterPushMap["proxyServer"] = proxyServer + registerDeviceWithNextcloud(ncApi, nextcloudRegisterPushMap, pushToken, user) + } + } + } + } else { + Log.e(TAG, "push token was empty when trying to register at server") + } + } + + private fun registerDeviceWithNextcloud( + ncApi: NcApi, + nextcloudRegisterPushMap: Map, + token: String, + user: User + ) { + val credentials = ApiUtils.getCredentials(user.username, user.token) + ncApi.registerDeviceForNotificationsWithNextcloud( + credentials, + ApiUtils.getUrlNextcloudPush(user.baseUrl), + nextcloudRegisterPushMap + ) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(pushRegistrationOverall: PushRegistrationOverall) { + Log.d(TAG, "pushTokenHash successfully registered at nextcloud server.") + val proxyMap: MutableMap = HashMap() + proxyMap["pushToken"] = token + proxyMap["deviceIdentifier"] = pushRegistrationOverall.ocs!!.data!!.deviceIdentifier + proxyMap["deviceIdentifierSignature"] = pushRegistrationOverall.ocs!!.data!!.signature + proxyMap["userPublicKey"] = pushRegistrationOverall.ocs!!.data!!.publicKey + registerDeviceWithPushProxy(ncApi, proxyMap, user) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to register device with nextcloud", e) + eventBus!!.post(EventStatus(user.id!!, EventStatus.EventType.PUSH_REGISTRATION, false)) + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun registerDeviceWithPushProxy(ncApi: NcApi, proxyMap: Map, user: User) { + ncApi.registerDeviceForNotificationsWithPushProxy(ApiUtils.getUrlPushProxy(), proxyMap) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(t: Unit) { + try { + Log.d(TAG, "pushToken successfully registered at pushproxy.") + updatePushStateForUser(proxyMap, user) + } catch (e: IOException) { + Log.e(TAG, "IOException while updating user", e) + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to register device with pushproxy", e) + eventBus!!.post(EventStatus(user.id!!, EventStatus.EventType.PUSH_REGISTRATION, false)) + } + + override fun onComplete() { + // unused atm + } + }) + } + + @Throws(IOException::class) + private fun updatePushStateForUser(proxyMap: Map, user: User) { + val pushConfigurationState = PushConfigurationState() + pushConfigurationState.pushToken = proxyMap["pushToken"] + pushConfigurationState.deviceIdentifier = proxyMap["deviceIdentifier"] + pushConfigurationState.deviceIdentifierSignature = proxyMap["deviceIdentifierSignature"] + pushConfigurationState.userPublicKey = proxyMap["userPublicKey"] + pushConfigurationState.usesRegularPass = java.lang.Boolean.FALSE + if (user.id != null) { + userManager!!.updatePushState(user.id!!, pushConfigurationState).subscribe(object : SingleObserver { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onSuccess(integer: Int) { + eventBus!!.post( + EventStatus( + getIdForUser(user), + EventStatus.EventType.PUSH_REGISTRATION, + true + ) + ) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "update push state for user failed", e) + eventBus!!.post( + EventStatus( + getIdForUser(user), + EventStatus.EventType.PUSH_REGISTRATION, + false + ) + ) + } + }) + } else { + Log.e(TAG, "failed to update updatePushStateForUser. user.getId() was null") + } + } + + private fun readKeyFromString(readPublicKey: Boolean, keyString: String?): Key? { + var keyString = keyString + keyString = if (readPublicKey) { + keyString!!.replace("\\n".toRegex(), "").replace( + "-----BEGIN PUBLIC KEY-----", + "" + ).replace("-----END PUBLIC KEY-----", "") + } else { + keyString!!.replace("\\n".toRegex(), "").replace( + "-----BEGIN PRIVATE KEY-----", + "" + ).replace("-----END PRIVATE KEY-----", "") + } + var keyFactory: KeyFactory? = null + try { + keyFactory = KeyFactory.getInstance("RSA") + return if (readPublicKey) { + val keySpec = X509EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT)) + keyFactory.generatePublic(keySpec) + } else { + val keySpec = PKCS8EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT)) + keyFactory.generatePrivate(keySpec) + } + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "No such algorithm while reading key from string") + } catch (e: InvalidKeySpecException) { + Log.d(TAG, "Invalid key spec while reading key from string") + } + return null + } + + fun readKeyFromFile(readPublicKey: Boolean): Key? { + val path: String + path = if (readPublicKey) { + publicKeyFile.absolutePath + } else { + privateKeyFile.absolutePath + } + try { + FileInputStream(path).use { fileInputStream -> + val bytes = ByteArray(fileInputStream.available()) + fileInputStream.read(bytes) + val keyFactory = KeyFactory.getInstance("RSA") + return if (readPublicKey) { + val keySpec = X509EncodedKeySpec(bytes) + keyFactory.generatePublic(keySpec) + } else { + val keySpec = PKCS8EncodedKeySpec(bytes) + keyFactory.generatePrivate(keySpec) + } + } + } catch (e: FileNotFoundException) { + Log.d(TAG, "Failed to find path while reading the Key") + } catch (e: IOException) { + Log.d(TAG, "IOException while reading the key") + } catch (e: InvalidKeySpecException) { + Log.d(TAG, "InvalidKeySpecException while reading the key") + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "RSA algorithm not supported") + } + return null + } + + companion object { + private const val TAG = "PushUtils" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/RemoteFileUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/RemoteFileUtils.kt index 4786997e4..2e3681f88 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/RemoteFileUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/RemoteFileUtils.kt @@ -31,11 +31,7 @@ import io.reactivex.schedulers.Schedulers object RemoteFileUtils { private val TAG = RemoteFileUtils::class.java.simpleName - fun getNewPathIfFileExists( - ncApi: NcApi, - currentUser: User, - remotePath: String - ): String { + fun getNewPathIfFileExists(ncApi: NcApi, currentUser: User, remotePath: String): String { var finalPath = remotePath val fileExists = doesFileExist( ncApi, @@ -53,11 +49,7 @@ object RemoteFileUtils { return finalPath } - private fun doesFileExist( - ncApi: NcApi, - currentUser: User, - remotePath: String - ): Observable { + private fun doesFileExist(ncApi: NcApi, currentUser: User, remotePath: String): Observable { return ncApi.checkIfFileExists( ApiUtils.getCredentials(currentUser.username, currentUser.token), ApiUtils.getUrlForFileUpload( @@ -72,11 +64,7 @@ object RemoteFileUtils { } } - private fun getFileNameWithoutCollision( - ncApi: NcApi, - currentUser: User, - remotePath: String - ): String { + private fun getFileNameWithoutCollision(ncApi: NcApi, currentUser: User, remotePath: String): String { val extPos = remotePath.lastIndexOf('.') var suffix: String var extension = "" diff --git a/app/src/main/java/com/nextcloud/talk/utils/ShareUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ShareUtils.kt index 3aea93772..41c644878 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ShareUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ShareUtils.kt @@ -25,11 +25,7 @@ import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.conversations.Conversation object ShareUtils { - fun getStringForIntent( - context: Context, - user: User, - conversation: Conversation? - ): String { + fun getStringForIntent(context: Context, user: User, conversation: Conversation?): String { return String.format( context.resources.getString(R.string.nc_share_text), user.baseUrl, diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index d5b9cae6e..4e2331e31 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -84,6 +84,8 @@ object BundleKeys { const val KEY_DISMISS_RECORDING_URL = "KEY_DISMISS_RECORDING_URL" const val KEY_SHARE_RECORDING_TO_CHAT_URL = "KEY_SHARE_RECORDING_TO_CHAT_URL" const val KEY_GEOCODING_RESULT = "KEY_GEOCODING_RESULT" - const val ADD_ACCOUNT = "ADD_ACCOUNT" // temp workaround until conductor is removed + const val ADD_ADDITIONAL_ACCOUNT = "ADD_ADDITIONAL_ACCOUNT" const val SAVED_TRANSLATED_MESSAGE = "SAVED_TRANSLATED_MESSAGE" + const val KEY_REAUTHORIZE_ACCOUNT = "KEY_REAUTHORIZE_ACCOUNT" + const val KEY_PASSWORD = "KEY_PASSWORD" } diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt index babf6bed6..1969c6912 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt @@ -214,12 +214,8 @@ object CapabilitiesUtilNew { @JvmStatic fun isLinkPreviewAvailable(user: User): Boolean { - if (user.capabilities?.coreCapability?.referenceApi != null && + return user.capabilities?.coreCapability?.referenceApi != null && user.capabilities?.coreCapability?.referenceApi == "true" - ) { - return true - } - return false } fun isTranslationsSupported(user: User?): Boolean { @@ -227,20 +223,13 @@ object CapabilitiesUtilNew { val capabilities = user.capabilities return capabilities?.spreedCapability?.config?.containsKey("chat") == true && capabilities.spreedCapability!!.config!!["chat"] != null && - capabilities.spreedCapability!!.config!!["chat"]!!.containsKey("translations") + capabilities.spreedCapability!!.config!!["chat"]!!.containsKey("has-translation-providers") && + capabilities.spreedCapability!!.config!!["chat"]!!["has-translation-providers"] == true } return false } - fun getLanguages(user: User?): Any? { - return if (isTranslationsSupported(user)) { - user!!.capabilities!!.spreedCapability!!.config!!["chat"]!!["translations"] - } else { - null - } - } - fun isRemindSupported(user: User?): Boolean { if (user?.capabilities != null) { val capabilities = user.capabilities diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java index 42e286bae..3097b3037 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java @@ -168,5 +168,9 @@ public interface AppPreferences { String getSorting(); + void saveWaveFormForFile(String filename, Float[] array); + + Float[] getWaveFormFromFile(String filename); + void clear(); } diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt index 12f341293..c096044eb 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt @@ -41,16 +41,19 @@ import kotlinx.coroutines.runBlocking class AppPreferencesImpl(val context: Context) : AppPreferences { override fun getProxyType(): String { - return runBlocking { async { readString(PROXY_TYPE, "No proxy").first() } }.getCompleted() + return runBlocking { + async { readString(PROXY_TYPE, context.resources.getString(R.string.nc_no_proxy)).first() } + }.getCompleted() } - override fun setProxyType(proxyType: String?) = runBlocking { - async { - if (proxyType != null) { - writeString(PROXY_TYPE, proxyType) + override fun setProxyType(proxyType: String?) = + runBlocking { + async { + if (proxyType != null) { + writeString(PROXY_TYPE, proxyType) + } } } - } override fun removeProxyType() { proxyType = "" @@ -60,13 +63,14 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readString(PROXY_HOST).first() } }.getCompleted() } - override fun setProxyHost(proxyHost: String?) = runBlocking { - async { - if (proxyHost != null) { - writeString(PROXY_HOST, proxyHost) + override fun setProxyHost(proxyHost: String?) = + runBlocking { + async { + if (proxyHost != null) { + writeString(PROXY_HOST, proxyHost) + } } } - } override fun removeProxyHost() { proxyHost = "" @@ -76,13 +80,14 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readString(PROXY_PORT).first() } }.getCompleted() } - override fun setProxyPort(proxyPort: String?) = runBlocking { - async { - if (proxyPort != null) { - writeString(PROXY_PORT, proxyPort) + override fun setProxyPort(proxyPort: String?) = + runBlocking { + async { + if (proxyPort != null) { + writeString(PROXY_PORT, proxyPort) + } } } - } override fun removeProxyPort() { proxyPort = "" @@ -92,11 +97,12 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readBoolean(PROXY_CRED).first() } }.getCompleted() } - override fun setProxyNeedsCredentials(proxyNeedsCredentials: Boolean) = runBlocking { - async { - writeBoolean(PROXY_CRED, proxyNeedsCredentials) + override fun setProxyNeedsCredentials(proxyNeedsCredentials: Boolean) = + runBlocking { + async { + writeBoolean(PROXY_CRED, proxyNeedsCredentials) + } } - } override fun removeProxyCredentials() { setProxyNeedsCredentials(false) @@ -106,13 +112,14 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readString(PROXY_USERNAME).first() } }.getCompleted() } - override fun setProxyUsername(proxyUsername: String?) = runBlocking { - async { - if (proxyUsername != null) { - writeString(PROXY_USERNAME, proxyUsername) + override fun setProxyUsername(proxyUsername: String?) = + runBlocking { + async { + if (proxyUsername != null) { + writeString(PROXY_USERNAME, proxyUsername) + } } } - } override fun removeProxyUsername() { proxyUsername = "" @@ -122,13 +129,14 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readString(PROXY_PASSWORD).first() } }.getCompleted() } - override fun setProxyPassword(proxyPassword: String?) = runBlocking { - async { - if (proxyPassword != null) { - writeString(PROXY_PASSWORD, proxyPassword) + override fun setProxyPassword(proxyPassword: String?) = + runBlocking { + async { + if (proxyPassword != null) { + writeString(PROXY_PASSWORD, proxyPassword) + } } } - } override fun removeProxyPassword() { proxyPassword = "" @@ -138,13 +146,14 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readString(PUSH_TOKEN).first() } }.getCompleted() } - override fun setPushToken(pushToken: String?) = runBlocking { - async { - if (pushToken != null) { - writeString(PUSH_TOKEN, pushToken) + override fun setPushToken(pushToken: String?) = + runBlocking { + async { + if (pushToken != null) { + writeString(PUSH_TOKEN, pushToken) + } } } - } override fun removePushToken() { pushToken = "" @@ -154,13 +163,14 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readString(TEMP_CLIENT_CERT_ALIAS).first() } }.getCompleted() } - override fun setTemporaryClientCertAlias(alias: String?) = runBlocking { - async { - if (alias != null) { - writeString(TEMP_CLIENT_CERT_ALIAS, alias) + override fun setTemporaryClientCertAlias(alias: String?) = + runBlocking { + async { + if (alias != null) { + writeString(TEMP_CLIENT_CERT_ALIAS, alias) + } } } - } override fun removeTemporaryClientCertAlias() { temporaryClientCertAlias = "" @@ -170,11 +180,12 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readBoolean(PUSH_TO_TALK_INTRO_SHOWN).first() } }.getCompleted() } - override fun setPushToTalkIntroShown(shown: Boolean) = runBlocking { - async { - writeBoolean(PUSH_TO_TALK_INTRO_SHOWN, shown) + override fun setPushToTalkIntroShown(shown: Boolean) = + runBlocking { + async { + writeBoolean(PUSH_TO_TALK_INTRO_SHOWN, shown) + } } - } override fun removePushToTalkIntroShown() { pushToTalkIntroShown = false @@ -184,13 +195,14 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readString(CALL_RINGTONE).first() } }.getCompleted() } - override fun setCallRingtoneUri(value: String?) = runBlocking { - async { - if (value != null) { - writeString(CALL_RINGTONE, value) + override fun setCallRingtoneUri(value: String?) = + runBlocking { + async { + if (value != null) { + writeString(CALL_RINGTONE, value) + } } } - } override fun removeCallRingtoneUri() { callRingtoneUri = "" @@ -200,13 +212,14 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readString(MESSAGE_RINGTONE).first() } }.getCompleted() } - override fun setMessageRingtoneUri(value: String?) = runBlocking { - async { - if (value != null) { - writeString(MESSAGE_RINGTONE, value) + override fun setMessageRingtoneUri(value: String?) = + runBlocking { + async { + if (value != null) { + writeString(MESSAGE_RINGTONE, value) + } } } - } override fun removeMessageRingtoneUri() { messageRingtoneUri = "" @@ -216,11 +229,12 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readBoolean(NOTIFY_UPGRADE_V2).first() } }.getCompleted() } - override fun setNotificationChannelIsUpgradedToV2(value: Boolean) = runBlocking { - async { - writeBoolean(NOTIFY_UPGRADE_V2, value) + override fun setNotificationChannelIsUpgradedToV2(value: Boolean) = + runBlocking { + async { + writeBoolean(NOTIFY_UPGRADE_V2, value) + } } - } override fun removeNotificationChannelUpgradeToV2() { setNotificationChannelIsUpgradedToV2(false) @@ -230,11 +244,12 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readBoolean(NOTIFY_UPGRADE_V3).first() } }.getCompleted() } - override fun setNotificationChannelIsUpgradedToV3(value: Boolean) = runBlocking { - async { - writeBoolean(NOTIFY_UPGRADE_V3, value) + override fun setNotificationChannelIsUpgradedToV3(value: Boolean) = + runBlocking { + async { + writeBoolean(NOTIFY_UPGRADE_V3, value) + } } - } override fun removeNotificationChannelUpgradeToV3() { setNotificationChannelIsUpgradedToV3(false) @@ -244,11 +259,12 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readBoolean(SCREEN_SECURITY).first() } }.getCompleted() } - override fun setScreenSecurity(value: Boolean) = runBlocking { - async { - writeBoolean(SCREEN_SECURITY, value) + override fun setScreenSecurity(value: Boolean) = + runBlocking { + async { + writeBoolean(SCREEN_SECURITY, value) + } } - } override fun removeScreenSecurity() { setScreenSecurity(false) @@ -258,11 +274,12 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readBoolean(SCREEN_LOCK).first() } }.getCompleted() } - override fun setScreenLock(value: Boolean) = runBlocking { - async { - writeBoolean(SCREEN_LOCK, value) + override fun setScreenLock(value: Boolean) = + runBlocking { + async { + writeBoolean(SCREEN_LOCK, value) + } } - } override fun removeScreenLock() { setScreenLock(false) @@ -273,11 +290,12 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return read } - override fun setIncognitoKeyboard(value: Boolean) = runBlocking { - async { - writeBoolean(INCOGNITO_KEYBOARD, value) + override fun setIncognitoKeyboard(value: Boolean) = + runBlocking { + async { + writeBoolean(INCOGNITO_KEYBOARD, value) + } } - } override fun removeIncognitoKeyboard() { setIncognitoKeyboard(false) @@ -287,17 +305,19 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return runBlocking { async { readBoolean(PHONE_BOOK_INTEGRATION).first() } }.getCompleted() } - override fun setPhoneBookIntegration(value: Boolean) = runBlocking { - async { - writeBoolean(PHONE_BOOK_INTEGRATION, value) + override fun setPhoneBookIntegration(value: Boolean) = + runBlocking { + async { + writeBoolean(PHONE_BOOK_INTEGRATION, value) + } } - } - override fun removeLinkPreviews() = runBlocking { - async { - writeBoolean(LINK_PREVIEWS, false) + override fun removeLinkPreviews() = + runBlocking { + async { + writeBoolean(LINK_PREVIEWS, false) + } } - } override fun getScreenLockTimeout(): String { val default = context.resources.getString(R.string.nc_screen_lock_timeout_sixty) @@ -305,13 +325,14 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return read.ifEmpty { default } } - override fun setScreenLockTimeout(value: String?) = runBlocking { - async { - if (value != null) { - writeString(SCREEN_LOCK_TIMEOUT, value) + override fun setScreenLockTimeout(value: String?) = + runBlocking { + async { + if (value != null) { + writeString(SCREEN_LOCK_TIMEOUT, value) + } } } - } override fun removeScreenLockTimeout() { screenLockTimeout = "" @@ -324,14 +345,15 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return read.ifEmpty { default } } - override fun setTheme(value: String?) = runBlocking { - async { - if (value != null) { - val key = context.resources.getString(R.string.nc_settings_theme_key) - writeString(key, value) + override fun setTheme(value: String?) = + runBlocking { + async { + if (value != null) { + val key = context.resources.getString(R.string.nc_settings_theme_key) + writeString(key, value) + } } } - } override fun removeTheme() { theme = "" @@ -342,27 +364,30 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return read } - override fun setDbCypherToUpgrade(value: Boolean) = runBlocking { - async { - writeBoolean(DB_CYPHER_V4_UPGRADE, value) + override fun setDbCypherToUpgrade(value: Boolean) = + runBlocking { + async { + writeBoolean(DB_CYPHER_V4_UPGRADE, value) + } } - } override fun getIsDbRoomMigrated(): Boolean { return runBlocking { async { readBoolean(DB_ROOM_MIGRATED).first() } }.getCompleted() } - override fun setIsDbRoomMigrated(value: Boolean) = runBlocking { - async { - writeBoolean(DB_ROOM_MIGRATED, value) + override fun setIsDbRoomMigrated(value: Boolean) = + runBlocking { + async { + writeBoolean(DB_ROOM_MIGRATED, value) + } } - } - override fun setPhoneBookIntegrationLastRun(currentTimeMillis: Long) = runBlocking { - async { - writeLong(PHONE_BOOK_INTEGRATION_LAST_RUN, currentTimeMillis) + override fun setPhoneBookIntegrationLastRun(currentTimeMillis: Long) = + runBlocking { + async { + writeLong(PHONE_BOOK_INTEGRATION_LAST_RUN, currentTimeMillis) + } } - } override fun getPhoneBookIntegrationLastRun(defaultValue: Long?): Long { val result = if (defaultValue != null) { @@ -374,36 +399,39 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return result } - override fun setReadPrivacy(value: Boolean) = runBlocking { - val key = context.resources.getString(R.string.nc_settings_read_privacy_key) - async { - writeBoolean(key, value) + override fun setReadPrivacy(value: Boolean) = + runBlocking { + val key = context.resources.getString(R.string.nc_settings_read_privacy_key) + async { + writeBoolean(key, value) + } } - } override fun getReadPrivacy(): Boolean { val key = context.resources.getString(R.string.nc_settings_read_privacy_key) return runBlocking { async { readBoolean(key).first() } }.getCompleted() } - override fun setTypingStatus(value: Boolean) = runBlocking { - async { - writeBoolean(TYPING_STATUS, value) + override fun setTypingStatus(value: Boolean) = + runBlocking { + async { + writeBoolean(TYPING_STATUS, value) + } } - } override fun getTypingStatus(): Boolean { return runBlocking { async { readBoolean(TYPING_STATUS).first() } }.getCompleted() } - override fun setSorting(value: String?) = runBlocking { - val key = context.resources.getString(R.string.nc_file_browser_sort_by_key) - async { - if (value != null) { - writeString(key, value) + override fun setSorting(value: String?) = + runBlocking { + val key = context.resources.getString(R.string.nc_file_browser_sort_by_key) + async { + if (value != null) { + writeString(key, value) + } } } - } override fun getSorting(): String { val key = context.resources.getString(R.string.nc_file_browser_sort_by_key) @@ -412,31 +440,46 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { return read.ifEmpty { default } } + override fun saveWaveFormForFile(filename: String, array: Array) = + runBlocking { + async { + writeString(filename, array.contentToString()) + } + } + + override fun getWaveFormFromFile(filename: String): Array { + val string = runBlocking { async { readString(filename).first() } }.getCompleted() + return if (string.isNotEmpty()) string.convertStringToArray() else floatArrayOf().toTypedArray() + } + override fun clear() {} - private suspend fun writeString(key: String, value: String) = context.dataStore.edit { settings -> - settings[ - stringPreferencesKey( - key - ) - ] = value - } + private suspend fun writeString(key: String, value: String) = + context.dataStore.edit { settings -> + settings[ + stringPreferencesKey( + key + ) + ] = value + } /** * Returns a Flow of type String * @param key the key of the persisted data to be observed */ - fun readString(key: String, defaultValue: String = ""): Flow = context.dataStore.data.map { preferences -> - preferences[stringPreferencesKey(key)] ?: defaultValue - } + fun readString(key: String, defaultValue: String = ""): Flow = + context.dataStore.data.map { preferences -> + preferences[stringPreferencesKey(key)] ?: defaultValue + } - private suspend fun writeBoolean(key: String, value: Boolean) = context.dataStore.edit { settings -> - settings[ - booleanPreferencesKey( - key - ) - ] = value - } + private suspend fun writeBoolean(key: String, value: Boolean) = + context.dataStore.edit { settings -> + settings[ + booleanPreferencesKey( + key + ) + ] = value + } /** * Returns a Flow of type Boolean @@ -447,17 +490,15 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { preferences[booleanPreferencesKey(key)] ?: defaultValue } - private suspend fun writeLong(key: String, value: Long) = context.dataStore.edit { settings -> - settings[ - longPreferencesKey( - key - ) - ] = value - } + private suspend fun writeLong(key: String, value: Long) = + context.dataStore.edit { settings -> + settings[longPreferencesKey(key)] = value + } - private fun readLong(key: String, defaultValue: Long = 0): Flow = context.dataStore.data.map { preferences -> - preferences[longPreferencesKey(key)] ?: defaultValue - } + private fun readLong(key: String, defaultValue: Long = 0): Flow = + context.dataStore.data.map { preferences -> + preferences[longPreferencesKey(key)] ?: defaultValue + } companion object { @Suppress("UnusedPrivateProperty") @@ -487,5 +528,13 @@ class AppPreferencesImpl(val context: Context) : AppPreferences { const val DB_ROOM_MIGRATED = "db_room_migrated" const val PHONE_BOOK_INTEGRATION_LAST_RUN = "phone_book_integration_last_run" const val TYPING_STATUS = "typing_status" + private fun String.convertStringToArray(): Array { + var varString = this + val floatList = mutableListOf() + varString = varString.replace("\\[".toRegex(), "") + varString = varString.replace("]".toRegex(), "") + varString.split(",").forEach { floatList.add(it.toFloat()) } + return floatList.toTypedArray() + } } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideMessageHolder.java b/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideMessageHolder.java index 60a5899d5..44ed423fe 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideMessageHolder.java +++ b/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideMessageHolder.java @@ -39,7 +39,7 @@ public class ApplicationWideMessageHolder { } public enum MessageType { - WRONG_ACCOUNT, ACCOUNT_UPDATED_NOT_ADDED, ACCOUNT_SCHEDULED_FOR_DELETION, SERVER_WITHOUT_TALK, + WRONG_ACCOUNT, ACCOUNT_UPDATED_NOT_ADDED, SERVER_WITHOUT_TALK, FAILED_TO_IMPORT_ACCOUNT, ACCOUNT_WAS_IMPORTED, CALL_PASSWORD_WRONG } diff --git a/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt b/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt index 56e6ec811..48f37449f 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt @@ -55,11 +55,9 @@ class SSLSocketFactoryCompat( } } - override fun getDefaultCipherSuites(): Array? = cipherSuites - ?: delegate.defaultCipherSuites + override fun getDefaultCipherSuites(): Array? = cipherSuites ?: delegate.defaultCipherSuites - override fun getSupportedCipherSuites(): Array? = cipherSuites - ?: delegate.supportedCipherSuites + override fun getSupportedCipherSuites(): Array? = cipherSuites ?: delegate.supportedCipherSuites override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket { val ssl = delegate.createSocket(s, host, port, autoClose) diff --git a/app/src/main/java/third/parties/fresco/BetterImageSpan.kt b/app/src/main/java/third/parties/fresco/BetterImageSpan.kt index fbd003ed0..c4b7241d6 100644 --- a/app/src/main/java/third/parties/fresco/BetterImageSpan.kt +++ b/app/src/main/java/third/parties/fresco/BetterImageSpan.kt @@ -65,13 +65,7 @@ open class BetterImageSpan @JvmOverloads constructor( /** * Returns the width of the image span and increases the height if font metrics are available. */ - override fun getSize( - paint: Paint, - text: CharSequence, - start: Int, - end: Int, - fontMetrics: FontMetricsInt? - ): Int { + override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fontMetrics: FontMetricsInt?): Int { updateBounds() if (fontMetrics == null) { return mWidth diff --git a/app/src/main/res/drawable/baseline_download_24.xml b/app/src/main/res/drawable/baseline_download_24.xml new file mode 100644 index 000000000..e5253209d --- /dev/null +++ b/app/src/main/res/drawable/baseline_download_24.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/app/src/main/res/layout/controller_account_verification.xml b/app/src/main/res/layout/activity_account_verification.xml similarity index 100% rename from app/src/main/res/layout/controller_account_verification.xml rename to app/src/main/res/layout/activity_account_verification.xml diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 0d885922b..f2b9361ca 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -234,7 +234,7 @@ android:id="@+id/separator_1" android:layout_width="match_parent" android:layout_height="1dp" - android:background="@color/controller_chat_separator" /> + android:background="@color/chat_separator" /> - + + + + diff --git a/app/src/main/res/layout/controller_conversations_rv.xml b/app/src/main/res/layout/activity_conversations.xml similarity index 100% rename from app/src/main/res/layout/controller_conversations_rv.xml rename to app/src/main/res/layout/activity_conversations.xml diff --git a/app/src/main/res/layout/activity_full_screen_image.xml b/app/src/main/res/layout/activity_full_screen_image.xml index d7c954011..5e304e925 100644 --- a/app/src/main/res/layout/activity_full_screen_image.xml +++ b/app/src/main/res/layout/activity_full_screen_image.xml @@ -28,7 +28,7 @@ android:id="@+id/image_wrapper_view" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".activities.FullScreenImageActivity"> + tools:context=".fullscreenfile.FullScreenImageActivity"> + tools:context=".fullscreenfile.FullScreenMediaActivity"> + tools:context=".fullscreenfile.FullScreenTextViewerActivity"> - - - - diff --git a/app/src/main/res/layout/controller_server_selection.xml b/app/src/main/res/layout/activity_server_selection.xml similarity index 100% rename from app/src/main/res/layout/controller_server_selection.xml rename to app/src/main/res/layout/activity_server_selection.xml diff --git a/app/src/main/res/layout/controller_generic_rv.xml b/app/src/main/res/layout/activity_switch_account.xml similarity index 57% rename from app/src/main/res/layout/controller_generic_rv.xml rename to app/src/main/res/layout/activity_switch_account.xml index 3178906d1..c917b9971 100644 --- a/app/src/main/res/layout/controller_generic_rv.xml +++ b/app/src/main/res/layout/activity_switch_account.xml @@ -18,25 +18,34 @@ ~ along with this program. If not, see . --> - + android:layout_height="match_parent" + android:orientation="vertical" + android:animateLayoutChanges="true"> - + android:layout_height="wrap_content"> - + android:layout_height="?attr/actionBarSize" + android:background="@color/appbar" + android:theme="?attr/actionBarPopupTheme" + app:layout_scrollFlags="enterAlwaysCollapsed|noScroll" + app:navigationIconTint="@color/fontAppbar" + app:popupTheme="@style/appActionBarPopupMenu" + app:titleTextColor="@color/fontAppbar" + tools:title="@string/nc_select_an_account"> - + - + - + diff --git a/app/src/main/res/layout/controller_web_view_login.xml b/app/src/main/res/layout/activity_web_view_login.xml similarity index 100% rename from app/src/main/res/layout/controller_web_view_login.xml rename to app/src/main/res/layout/activity_web_view_login.xml diff --git a/app/src/main/res/layout/call_activity.xml b/app/src/main/res/layout/call_activity.xml index ec162f705..2e39c606f 100644 --- a/app/src/main/res/layout/call_activity.xml +++ b/app/src/main/res/layout/call_activity.xml @@ -28,7 +28,7 @@ @@ -83,7 +83,7 @@ android:layout_marginTop="16dp" android:text="@string/nc_call_unknown" android:textAlignment="center" - android:textColor="@color/controller_call_incomingCallTextView" + android:textColor="@color/call_incomingCallTextView" android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -109,7 +109,7 @@ android:layout_below="@+id/conversationNameTextView" android:text="@string/nc_call_incoming" android:textAlignment="center" - android:textColor="@color/controller_call_incomingCallTextView" + android:textColor="@color/call_incomingCallTextView" android:textSize="16sp" /> diff --git a/app/src/main/res/layout/dialog_conversation_operations.xml b/app/src/main/res/layout/dialog_conversation_operations.xml index 65df06d17..62658acb2 100644 --- a/app/src/main/res/layout/dialog_conversation_operations.xml +++ b/app/src/main/res/layout/dialog_conversation_operations.xml @@ -20,242 +20,256 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + android:layout_gravity="bottom" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> + android:paddingBottom="@dimen/standard_half_padding"> - + - + android:layout_height="@dimen/bottom_sheet_item_height" + android:gravity="start|center_vertical" + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_padding" + android:textColor="@color/medium_emphasis_text" + android:textSize="@dimen/bottom_sheet_text_size" + tools:text="conversation name" /> - - + android:layout_height="@dimen/bottom_sheet_item_height" + android:background="?android:attr/selectableItemBackground" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_padding" + tools:ignore="UseCompoundDrawables"> - + - + + - - + android:layout_height="@dimen/bottom_sheet_item_height" + android:background="?android:attr/selectableItemBackground" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_padding" + tools:ignore="UseCompoundDrawables"> - + - + + - - + android:layout_height="@dimen/bottom_sheet_item_height" + android:background="?android:attr/selectableItemBackground" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_padding" + tools:ignore="UseCompoundDrawables"> - + - + + - - + android:layout_height="@dimen/bottom_sheet_item_height" + android:background="?android:attr/selectableItemBackground" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_padding" + tools:ignore="UseCompoundDrawables"> - + - + + - - + android:layout_height="@dimen/bottom_sheet_item_height" + android:background="?android:attr/selectableItemBackground" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_padding" + tools:ignore="UseCompoundDrawables"> - + - + + - - + android:layout_height="@dimen/bottom_sheet_item_height" + android:background="?android:attr/selectableItemBackground" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_padding" + tools:ignore="UseCompoundDrawables"> - + - + + - + android:layout_height="@dimen/bottom_sheet_item_height" + android:background="?android:attr/selectableItemBackground" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_padding" + tools:ignore="UseCompoundDrawables"> + + + + + + - + diff --git a/app/src/main/res/layout/dialog_file_attachment_preview.xml b/app/src/main/res/layout/dialog_file_attachment_preview.xml new file mode 100644 index 000000000..91bc4148c --- /dev/null +++ b/app/src/main/res/layout/dialog_file_attachment_preview.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_message_actions.xml b/app/src/main/res/layout/dialog_message_actions.xml index c7a79e13b..17ce0ed4b 100644 --- a/app/src/main/res/layout/dialog_message_actions.xml +++ b/app/src/main/res/layout/dialog_message_actions.xml @@ -26,7 +26,6 @@ android:orientation="vertical" android:paddingBottom="@dimen/standard_half_padding"> - + + + diff --git a/app/src/main/res/layout/dialog_more_call_actions.xml b/app/src/main/res/layout/dialog_more_call_actions.xml index 9a3ffe617..67a278b75 100644 --- a/app/src/main/res/layout/dialog_more_call_actions.xml +++ b/app/src/main/res/layout/dialog_more_call_actions.xml @@ -27,6 +27,10 @@ android:orientation="vertical" android:paddingBottom="@dimen/standard_half_padding"> + + + + + + - - + + diff --git a/app/src/main/res/layout/item_custom_outcoming_preview_message.xml b/app/src/main/res/layout/item_custom_outcoming_preview_message.xml index 6d2dd6b7c..1380a2dfb 100644 --- a/app/src/main/res/layout/item_custom_outcoming_preview_message.xml +++ b/app/src/main/res/layout/item_custom_outcoming_preview_message.xml @@ -33,6 +33,7 @@ android:layout_marginBottom="2dp"> + app:strokeWidth="0dp" + tools:visibility="gone"> + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 046550081..f57069989 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -40,6 +40,8 @@ %1$s (%2$d) 4 ساعات غير مرئي + تعذّرت استعادة اللغات + فشلت الاستعادة في وقت لاحقٍ اليوم أنت غادرت المحادثة %1$s تحميل المزيد من النتائج @@ -102,6 +104,7 @@ لتمكين اتصال الفيديو ، يرجى منح إذن \"الكاميرا\". إلغاء فشل في جلب الإمكانيات، جارِ إلالغاء + الشرح هل تثق في شهادة SSL غير المعروفة حتى الآن ، الصادرة من%1$sإلى %2$s ، الصالحة %3$s إلى%4$s؟ تحقق من الشهادة إعدادات SSL الخاص بك تمنع الاتصال @@ -141,6 +144,7 @@ إذا قمت بحذف المحادثة، سوف يتم حذفها أيضًا لدى المشاركين الآخرين. حذف تم حذف الرسالة بنجاح، لكن من الممكن أن تم تسريبها لخدمات أخرى + تمّ حذف المستخدِم %1$s تخفيض رتبة مشرف تسجيل رسالة صوتية أرسل رسالة @@ -151,6 +155,11 @@ التطبيق قديم جدًا ولم يعد مدعوماً من قبل هذا الخادوم. يُرجى التحديث. تحديث هل تريد إعادة تفويض هذا الحساب أو مسحه؟ + حفظ هذه الوسائط على وحدة التخزين سيسمح لأي تطبيق آخر على الوحدة بالوصول إلى هذه الوسائط. + إستمرار؟ + لا + حفظ على وحدة التخزين؟ + نعم لايمكن عرض الاسم، جارِ إلالغاء تعذر تخزين اسم العرض ، جارِ إلالغاء البريد الإلكتروني @@ -283,6 +292,8 @@ تغيير تسمية رد ردّ على الخاص + حفظ + تمّ الحفط بنجاحٍ 30 ثانية 5 دقائق 1 دقيقة diff --git a/app/src/main/res/values-b+en+001/strings.xml b/app/src/main/res/values-b+en+001/strings.xml index 6d8f6d4dd..0a379997e 100644 --- a/app/src/main/res/values-b+en+001/strings.xml +++ b/app/src/main/res/values-b+en+001/strings.xml @@ -40,6 +40,8 @@ %1$s (%2$d) 4 hours Invisible + Languages could not be retrieved + Retrieval failed Later today You left the conversation %1$s Load more results @@ -102,6 +104,7 @@ To enable video communication please grant \"Camera\" permission. Cancel Failed to fetch capabilities, aborting + Caption Do you trust the until now unknown SSL certificate, issued by %1$s for %2$s, valid from %3$s to %4$s? Check out the certificate Your SSL setup prevented connection @@ -141,6 +144,7 @@ If you delete the conversation, it will also be deleted for all other participants. Delete Message deleted successfully, but it might have been leaked to other services + User %1$s was removed Demote from moderator Record voice message Send message @@ -151,7 +155,10 @@ The app is too old and no longer supported by this server. Please update. Update Do you want to reauthorise or delete this account? + Saving this media to storage will allow any other apps on your device to access it. + Continue? No + Save to storage? Yes Display name couldn\'t be fetched, aborting Could not store display name, aborting @@ -285,6 +292,8 @@ Rename Reply Reply privately + Save + Saved successfully 30 seconds 5 minutes 1 minute @@ -453,6 +462,7 @@ Stop recording Stopping recording … Recording consent is required for all calls + The recording might include your voice, video from camera, and screen share. Your consent is required before joining the call. Do you consent? Require recording consent before joining call in this conversation Recording consent The call might be recorded. diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml index 2f91e21f3..aec80e6a7 100644 --- a/app/src/main/res/values-bg-rBG/strings.xml +++ b/app/src/main/res/values-bg-rBG/strings.xml @@ -132,6 +132,8 @@ Режим на поддръжка Обновяване Искате ли да разрешите повторно този профил или да го изтриете? + Не + Да Показваното име не може да бъде извлечено, прекъсване Името за визуализация не може да бъде записано, прекратяване Имейл @@ -255,6 +257,8 @@ Преименуване Отговори Отговаряне на лично + Запиши + Успешно запазено 30 секунди 5 минути 1 минута diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index bc9d19adb..d286e1d56 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -254,6 +254,8 @@ Anomena Respon Respon en privat + Desar + S\'ha desat correctament 30 segons 5 minuts 1 minut diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 6bc60149e..6dd0e788d 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -40,6 +40,8 @@ %1$s (%2$d) 4 hodiny Není vidět + Jazyky se nepodařilo získat + Získání se nezdařil Později dnes Opustili jste konverzaci %1$s Načíst další výsledky @@ -102,6 +104,7 @@ Pro zapnutí videokomunikace udělte oprávnění „Kamera“ . Storno Nepodařilo se získat schopnosti, přerušuje se + Titulek Věříte doposud neznámému SSL certifikátu, vydaném %1$s pro %2$s, platnému od %3$s do %4$s? Zkontrolovat certifikát Vaše nastavení SSL zabránilo připojení @@ -141,6 +144,7 @@ Pokud konverzaci smažete, bude smazána také pro všechny její ostatní účastníky. Smazat Zpráva úspěšně smazána, ale možná unikla do jiných služeb + Uživatel %1$s byl odebrán Odebrat oprávnění moderátora Nahrát hlasovou zprávu Poslat zprávu @@ -151,6 +155,11 @@ Tato aplikace je příliš stará a už nepodporovaná tímto serverem. Prosíme aktualizujte ji. Aktualizovat Chcete tento účet znovu ověřit nebo smazat? + Uložení tohoto média na úložiště umožní ostatním aplikacím na vašem zařízení k němu přistupovat. + Pokračovat? + Ne + Uložit na úložiště? + Ano Nepodařilo se získat zobrazované jméno, přerušuje se Nepodařilo se uložit zobrazované jméno, přerušuje se E-mail @@ -283,6 +292,8 @@ Přejmenovat Odpovědět Odpovědět soukromě + Uložit + Úspěšně uloženo 30 sekund 5 minut 1 minuta diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index c2bd98467..56dd06951 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -168,6 +168,8 @@ Omdøb Besvar Svar privat + Gem + Gemt 30 sekunder 5 minutter 1 minut diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e2405bb73..7b7471bc7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -40,6 +40,8 @@ %1$s (%2$d) 4 Stunden Unsichtbar + Sprachen konnten nicht abgerufen werden + Abrufen fehlgeschlagen Später heute Sie haben die Unterhaltung %1$s verlassen Weitere Ergebnisse laden @@ -102,6 +104,7 @@ Um Videokommunikation zu ermöglichen, bitte die Berechtigung „Kamera“ erteilen. Abbrechen Fehler beim Abfragen der Fähigkeiten. Abbruch + Untertitel Soll dem bisher unbekannten SSL-Zertifikat, ausgestellt von %1$s für %2$s, gültig von %3$s bis %4$s, vertraut werden? Überprüfen Sie das Zertifikat Ihre SSL-Konfiguration hat die Verbindung verhindert @@ -141,6 +144,7 @@ Wenn Sie diese Unterhaltung löschen, dann wird diese auch für alle anderen Teilnehmer gelöscht. Löschen Nachricht erfolgreich gelöscht, sie wurde aber möglicherweise an andere Dienste weitergegeben. + Benutzer %1$s wurde entfernt Moderator absetzen Sprachnachricht aufnehmen Nachricht senden @@ -151,7 +155,10 @@ Die App ist zu alt und wird von diesem Server nicht mehr unterstützt. Bitte aktualisieren. Aktualisieren Möchten Sie das Konto erneut autorisieren oder löschen? + Wenn Sie dieses Medium im Speicher sichern, können alle anderen Apps auf Ihrem Gerät darauf zugreifen. + Fortsetzen? Nein + Im Speicher speichern? Ja Anzeigename konnte nicht abgerufen werden. Breche ab. Der Anzeigename konnte nicht gespeichert werden. Breche ab. @@ -285,6 +292,8 @@ Umbenennen Antworten Privat antworten + Speichern + Erfolgreich gespeichert 30 Sekunden 5 Minuten 1 Minute diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index ee7cf519e..cacf1ecc7 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -213,6 +213,8 @@ Μετονομασία Απάντηση Απάντηση ιδιωτικά + Αποθήκευση + Επιτυχής αποθήκευση 30 δευτερόλεπτα 5 λεπτά 1 λεπτό diff --git a/app/src/main/res/values-es-rEC/strings.xml b/app/src/main/res/values-es-rEC/strings.xml index 6c05dddd7..f78721836 100644 --- a/app/src/main/res/values-es-rEC/strings.xml +++ b/app/src/main/res/values-es-rEC/strings.xml @@ -268,6 +268,7 @@ Renombrar Responder Responder de forma privada + Guardar 30 segundos 5 minutos 1 minuto diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8b8fcda7e..607c73a7b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -285,6 +285,8 @@ Renombrar Responder Responder en privado + Guardar + Guardado correctamente 30 segundos 5 minutos 1 minuto @@ -453,7 +455,10 @@ Detener grabación de la llamada Detener la grabación Deteniendo grabación ... + El consentimiento para grabar es obligatorio para todas las llamadas + Obtener consentimiento para grabar antes de unirse a llamadas en esta conversación Consentimiento de grabación + La llamada puede ser grabada. Grabando Se quitó la conversación %1$s de los favoritos La conversación %1$s fue renombrada diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 5302f5b96..f3183e2a0 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -40,6 +40,8 @@ %1$s(%2$d) 4 ordu Ikusezina + Ezin izan dira hizkuntzak berreskuratu + Ezin izan da berreskuratu Beranduago gaur %1$s elkarrizketa utzi duzu Kargatu emaitza gehiago @@ -54,6 +56,7 @@ Z - A Handienak lehenengo Txikienak lehenengo + Mezuen iraungitzea Sakatu galdeketa irekitzeko Ez dago bilaketaren emaitzarik Hasi idazten bilatzeko … @@ -101,6 +104,7 @@ Bideo komunikazioa gaitzeko, eman \"Kamera\" baimena. Utzi Akatsak gaitasunak berreskuratzean. Bertan behera uzten. + Epigrafea Fio zara %1$s (e)k %2$s(e)ntzat emandako SSL ziurtagiri ezezagunaz?, %3$s tik %4$sra baliagarria? Egiaztatu ziurtagiria Zure SSL konfigurazioak konexioa eragotzi du @@ -140,6 +144,7 @@ Elkarrizketa hau ezabatzen baduzu beste kideentzat ere ezabatuko da. Ezabatu Mezua ondo ezabatu da, baina baliteke beste zerbitzu batzuetara filtratu izana + %1$s erabiltzailea kendu da Moderatzailetik degradatua Grabatu ahots mezua Bidali mezua @@ -150,7 +155,10 @@ Aplikazio hau zaharregia da eta ez dago zerbitzari honengatik onartuta. Mesedez eguneratu. Eguneratu Kontu hau birbaimendu edo ezabatu nahi duzu? + Multimedia hau biltegian gordetzen baduzu, zure gailuko beste edozein aplikaziok atzitu ahal izango du. + Jarraitu? Ez + Biltegian gorde nahi duzu? Bai Ezin izan da erakusteko izena lortu, bertan behera uzten Ezin izan da erakusteko izena biltegiratu, bertan behera uzten @@ -284,6 +292,8 @@ Berrizendatu Erantzun Erantzun pribatuki + Gorde + Ondo gorde da 30 segundo 5 minutu Minutu 1 @@ -451,6 +461,11 @@ Gelditu grabaketa Utzi grabaketa Grabazioa gelditzen… + Dei guztietan grabatzeko baimena behar da + Baliteke grabazioan zure ahotsa, kamerako bideoa eta pantaila partekatzea. Zure baimena beharrezkoa da deian sartu aurretik. Onartzen al duzu? + Eskatu grabatzeko baimena elkarrizketa honetako deian sartu aurretik + Grabatzeko baimena + Baliteke deia grabatzea. Grabatzea %1$s elkarrizketa gogokoetatik kendu da %1$s elkarrizketa kendu da diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index e15b27925..4585308d7 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -277,6 +277,7 @@ تغییرنام پاسخ Reply privately + ذخیره ۳۰ ثانیه ۵ دقیقه ۱ دقیقه diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 4c102c094..05a04ad1f 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -211,6 +211,8 @@ Nimeä uudelleen Vastaa Vastaa yksityisesti + Tallenna + Tallennettu onnistuneesti 30 sekuntia 5 minuuttia 1 minuutti diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1fa0dc867..10f6c2997 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -40,6 +40,7 @@ %1$s (%2$d) 4 heures Invisible + Les langues n\'ont pas pu être récupérées Plus tard aujourd\'hui Vous avez quitté la conversation %1$s Charger plus de résultats @@ -102,6 +103,7 @@ Pour activer la communication vidéo, merci de donner l\'autorisation « Appareil photo ». Annuler Échec de la récupération des capacités, abandon + Légende Faites-vous confiance au certificat SSL jusque-là inconnu, émis par %1$s pour %2$s, et et valide du %3$s au %4$s ? Vérifier le certificat Votre configuration SSL a empêché la connexion @@ -141,6 +143,7 @@ Si vous supprimez la conversation, elle sera supprimée pour tous les participants. Supprimer Message supprimé avec succès, mais il pourrait avoir été divulgué à d’autres services + L\'utilisateur %1$s a été supprimé Destituer de modérateur Enregistrer un message vocal Envoyer un message @@ -151,6 +154,7 @@ L\'application est trop ancienne et n\'est plus prise en charge par ce serveur. Veuillez la mettre à jour. Mise à jour Voulez-vous autoriser à nouveau ou supprimer ce compte ? + Poursuivre ? Non Oui Le nom d’affichage n’a pas pu être récupéré, annulation @@ -285,6 +289,8 @@ Renommer Répondre Répondre en privé + Enregistrer + Enregistré avec succès 30 secondes 5 minutes 1 minute diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index d7a003de0..4212d8f49 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -1,7 +1,9 @@ Icona da conta + Engadiuse a conversa %1$s aos favoritos Buscar en %s + Chamada de voz Bluetooth Saída de son Teléfono @@ -24,6 +26,7 @@ Personalizado Zona de perigo Eliminar avatar + A conversa %1$s foi eliminada Non molestar Non limpar Editar @@ -37,10 +40,15 @@ %1$s (%2$d) 4 horas Invisíbel + Non foi posíbel recuperar os idiomas + Produciuse un fallo na recuperación Hoxe máis tarde + Vde. deixou a conversa %1$s Cargando máis resultados Símbolo de bloqueo Baixar a man + A conversa %1$s foi marcada como lida + A conversa %1$s foi marcada como sen ler Mencionado Primeiro o máis recente Primeiro o máis antigo @@ -96,6 +104,7 @@ Para activar a comunicación por vídeo, conceda o permiso de «Cámara». Cancelar Produciuse un fallo ao recuperar as funcionalidades. Interrompendo. + Lenda Quere confiar nun certificado SSL descoñecido, emitido por %1$s para %2$s, válido dende %3$s ata %4$s? Verificar o certificado A súa configuración SSL impediu a conexión @@ -135,6 +144,7 @@ Se elimina a conversa, tamén se eliminará para todos os demais participantes. Eliminar A mensaxe foi eliminada correctamente, pero é posíbel que se filtrase a outros servizos + O usuario %1$s foi retirado Relegar de moderador Gravar mensaxe de voz Enviar a mensaxe @@ -145,7 +155,10 @@ A aplicación é demasiado antiga e xa non é compatíbel con este servidor. Actualícea. Actualizar Quere volver autorizar ou eliminar esta conta? + Se garda este multimedia no almacenamento, calquera outra aplicación do seu dispositivo poderá acceder a el. + Continuar? Non + Quere gardalo no almacenamento? Si Non foi posíbel recuperar o nome para amosar, interrompendo Non foi posíbel almacenar o nome para amosar, interrompendo @@ -162,7 +175,7 @@ O servidor de destino non admite unirse a conversas públicas mediante teléfonos móbiles. Pode tentar unirse á chamada empregando o navegador web. Desculpe, algo foi mal! Atrás - Precisase permiso para acceder ao ficheiro + Precísase de permiso para acceder ao ficheiro Filtrar conversas Usuario seguindo unha ligazón pública Vde.: %1$s @@ -225,7 +238,7 @@ Mencións sen ler Mensaxes sen ler Novo contrasinal - %1$s non dispoñíbel (non instalado nin restrinxido pola administración da instancia.) + %1$s non está dispoñíbel (nin instalado nin restrinxido pola administración da instancia.) Convidado Non Non hai conversas abertas @@ -280,6 +293,7 @@ Responder Responder en privado Gardar + Gardadao satisfactoriamente 30 segundos 5 minutos 1 minuto @@ -334,6 +348,7 @@ Notificacións Mensaxes Emparellar os contactos en función do número de teléfono para integrar o atallo de Talk na lista de teléfonos + Erro 429, demasiadas solicitudes Pode estabelecer o seu número de teléfono para que outros usuarios poidan atopalo Número de teléfono non válido O número de teléfono foi estabelecido correctamente @@ -361,7 +376,7 @@ Servidor non admitido Estabelecido polo aforrador de batería Escuro - Usar o redeterminado do sistema + Usar o predeterminado do sistema tema Claro Tema @@ -372,7 +387,7 @@ Advertencia Só pode volver autorizarse a conta actual Compartir o contacto - Precisase permiso para ler a lista de contactos + Precísase de permiso para ler a lista de contactos Compartir a localización actual Compartir a localización %1$s convite @@ -405,7 +420,7 @@ Gravación de vídeo de %1$s Gravación da conversa de %1$s (%2$s) Manteña premido para gravar, solte para enviar. - Precisase permiso para gravar son + Precísase de permiso para gravar son « Esvare para cancelar Seminario web A ligazón da conversa non é válida @@ -446,7 +461,14 @@ Deter a gravación da chamada Deter a gravación Deter a gravación + O consentimento de gravación é necesario para todas as chamadas + A gravación pode incluír a súa voz, o vídeo da cámara e da pantalla compartida. É preciso o seu consentimento antes de unirse á chamada. Consinte? + Esixir o consentimento de gravación antes de unirse á chamada nesta conversa + Consentimento de gravación + É posíbel que a chamada sexa gravada. Gravando + Retirouse a conversa %1$s dos favoritos + Cambióuselle o nome a conversa %1$s Non é posible unirse a outras salas mentres está nunha chamada Gardar Sincronizar só con servidores de confianza @@ -481,6 +503,7 @@ Voz Favorito Non ten permiso para iniciar unha chamada + iniciar unha chamada Mensaxe de estado Cambiar á sala parcial Cambiar á sala principal @@ -525,6 +548,7 @@ Produciuse un erro ao recuperar a información persoal do usuario. Non foi estabelecida a información persoal Engada o seu nome, imaxe e detalles de contacto na súa páxina de perfil. + Vídeo chamada Cal é o seu estado? %d voto diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 7b7e3eff9..65614585c 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -218,6 +218,7 @@ Odgovori Odgovori privatno Spremi + Uspješno spremljeno 30 sekundi 5 minuta 1 minuta diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index ebaa35976..4bb674b2c 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -283,6 +283,7 @@ Válasz Válasz privátban Mentés + Sikeres mentés 30 másodperc 5 perc 1 perc diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 6fbb31503..2a9fc89cd 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -15,6 +15,7 @@ Ónáðið ekki Ekki hreinsa Breyta + Baklykill (backspace) Nýlegt Dulritað mappa @@ -29,6 +30,7 @@ Ö - A Stærsta fyrst Minnsta fyrst + Engar leitarniðurstöður Skilaboð Valinn aðgangur er núna fluttur inn og tiltækur Um hugbúnaðinn @@ -55,6 +57,7 @@ Hringir Skipta um skilríki biðlara Setja upp skilríki biðlara + Búa til Dismiss Því miður, eitthvað fór úrskeiðis! Setja @@ -86,6 +89,7 @@ Gat ekki náð í birtingarnafn, hætti Gat ekki geymt birtingarnafn, hætti Tölvupóstur + 8 klukkustundir Slökkt 1 day 1 klukkustund @@ -117,8 +121,10 @@ Notkunarleyfi Lobby Ýttu til að aflæsa + Ekki stillt Merkja sem lesið Merkja allt sem ólesið + Hætta við svar Skilaboðin hafa verið lesið Skilaboðin hafa verið send Umsjónarmaður @@ -161,6 +167,7 @@ Endurnefna Svara Vista + Tókst að vista 30 sekúndur 5 mínútur 1 mínúta @@ -226,10 +233,12 @@ \nLykilorð: %1$s Deila þessari staðsetningu Veldu aðgang + Deck-spjald Staðsetning Raða sem Upphafstími Skipta um notandaaðgang + Senda inn frá tæki Sendi inn Notandi Webinar @@ -246,7 +255,9 @@ Greiðsla atkvæða Allt Vista + Aðeins samstilla við treysta þjóna Deilt milli þjóna (skýjasambandssameign) + Einungis sýnilegt staðværum notendum og gestum Staðvært Einka Útgefið @@ -264,14 +275,20 @@ Raddskilaboð Eftirlæti Stöðuskilaboð + Taka ljósmynd Senda + Skipta á milli myndavéla + Víxla vasaljósi af/á 30 mínútur Í þessari viku Viðhengi Í dag Á morgun Þýða + Greina tungumál Stillingar tækis + Gat ekki greint tungumálið + Þýðing mistókst Frá Til Vistfang diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index fb0ec04d6..e8750a572 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -237,6 +237,7 @@ Rispondi Rispondi in privato Salva + Salvato correttamente 30 secondi 5 minuti 1 minuto @@ -343,6 +344,7 @@ Inviare questi file a %1$s? Inviare questo file a %1$s? Spiacenti, caricamento non riuscito + Fallimento Condividi da %1$s Caricamento Scatta foto @@ -366,6 +368,7 @@ Aggiungi opzione Opzioni Sondaggio privato + Domanda Risultati Impostazioni Votare @@ -397,6 +400,7 @@ File Media Altro + Sondaggio Voce Preferito Non ti è consentito avviare una chiamata diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 85afa1177..a4c90c5cb 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -164,6 +164,7 @@ שינוי שם תגובה שמירה + נשמר בהצלחה 30 שניות 5 דקות דקה diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index ebbbb00e9..792865acb 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -232,6 +232,7 @@ 返信 個人的に返信する 保存 + 保存しました 30 秒 5分 1 分 @@ -419,8 +420,10 @@ 今日 明日 翻訳 + 言語を検出する デバイスの設定 言語を検出できませんでした + 翻訳に失敗しました 差出人 宛先 未読 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index e9cdf6dcd..47a2fb8b1 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -406,6 +406,7 @@ 녹음/녹화가 실패했습니다. 관리자에게 연락해 주세요. 녹음/녹화 시작 녹음/녹화 중단 + 녹음/녹화 동의 녹음/녹화 중 저장 신뢰할 수 있는 서버만 동기화할 수 있습니다. diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml index 929a47d2a..775fd3b0b 100644 --- a/app/src/main/res/values-lt-rLT/strings.xml +++ b/app/src/main/res/values-lt-rLT/strings.xml @@ -167,6 +167,7 @@ Atsakyti Atsakyti privačiai Įrašyti + Sėkmingai įrašyta 30 sekundžių 5 minutės 1 minutė diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 7a1e8a6b8..3d4a0e342 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -53,7 +53,7 @@ #373737 #D8D8D8 - #484848 + #484848 #2C2C2C diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 454e0a882..cbea94255 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -241,6 +241,7 @@ Kies er eentje van een provider. Antwoord Antwoord privé Opslaan + Succesvol opgeslagen 30 seconden 5 minuten 1 minuut diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 1573b3954..c8ed58537 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,7 +1,9 @@ Ikona konta + Dodano rozmowę %1$s do ulubionych Szukaj w %s + Rozmowa audio Bluetooth Wyjście audio Telefon @@ -24,6 +26,7 @@ Dowolnie Strefa niebezpieczeństwa Usuń awatar + Usunięto rozmowę %1$s Nie przeszkadzać Nie czyść Edytuj @@ -37,10 +40,15 @@ %1$s (%2$d) 4 godziny Niewidoczny + Nie udało się pobrać języków + Pobieranie nie powiodło się Później dzisiaj + Opuściłeś rozmowę %1$s Wczytaj więcej wyników Symbol zamknięcia Opuścić rękę + Oznaczono rozmowę %1$s jako przeczytaną + Oznaczono rozmowę %1$s jako nieprzeczytaną Wspomniany Od najnowszych Od najstarszych @@ -48,6 +56,7 @@ Z - A Od największych Od najmniejszych + Wygaśnięcie wiadomości Dotknij, aby otworzyć sondę Brak wyników wyszukiwania Zacznij pisać, aby wyszukać… @@ -95,6 +104,7 @@ Aby umożliwić komunikację wideo, należy udzielić dostępu dla \"Kamera\". Anuluj Błąd pobierania, przerwano żądanie + Podpis Czy ufasz dotychczas nieznanemu certyfikatowi SSL, wydanemu przez %1$s dla %2$s, ważnemu od %3$s do %4$s? Sprawdź certyfikat Twoje ustawienia SSL uniemożliwiają połaczenie @@ -134,6 +144,7 @@ Jeśli usuniesz rozmowę, zostanie ona również usunięta dla wszystkich pozostałych uczestników. Usuń Wiadomość została pomyślnie usunięta, ale mogła przedostać się do innych usług + Użytkownik %1$s został usunięty Zdegraduj z moderatora Nagraj wiadomość głosową Wyślij wiadomość @@ -144,7 +155,10 @@ Aplikacja jest zbyt stara i nie jest już obsługiwana przez ten serwer. Proszę zaktualizować. Aktualizuj Czy chcesz ponownie autoryzować lub usunąć konto? + Zapisanie tych multimediów do pamięci umożliwi dostęp do nich innym aplikacjom na Twoim urządzeniu. + Kontynuować? Nie + Zapisać w pamięci? Tak Nie udało się pobrać nazwy, przerwano żądanie Nie można zapisać wyświetlanej nazwy, przerwano żądanie @@ -279,6 +293,7 @@ Odpowiedz Odpowiedz prywatnie Zapisz + Zapisano pomyślnie 30 sekund 5 minut 1 minuta @@ -407,7 +422,7 @@ Przytrzymaj, aby nagrać, zwolnij, aby wysłać. Wymagane jest uprawnienie na nagrywanie dźwięku « Przesuń, aby anulować - Webinar + Webinarium Link rozmowy jest nieprawidłowy Złe hasło Tak @@ -446,7 +461,14 @@ Zatrzymaj nagrywanie rozmowy Zatrzymaj nagrywanie Zatrzymywanie nagrywania… + W przypadku wszystkich rozmów wymagana jest zgoda na nagrywanie + Nagranie może obejmować Twój głos, wideo z kamery i udostępniony ekran. Przed dołączeniem do rozmowy wymagana jest Twoja zgoda. Czy wyrażasz zgodę? + Wymagaj zgody na nagrywanie przed dołączeniem do tej rozmowy + Zgoda na nagrywanie + Rozmowa może zostać nagrana. Nagranie + Usunięto rozmowę %1$s z ulubionych + Nazwa rozmowy %1$s została zmieniona Podczas rozmowy nie można dołączać do innych pokoi Zapisz Synchronizuj tylko z zaufanymi serwerami @@ -481,6 +503,7 @@ Połączenie głosowe Ulubione Nie możesz rozpocząć połączenia + rozpoczął połączenie Komunikat statusu Przełącz się do pokoju podgrupy Przełącz się do głównego pokoju @@ -507,8 +530,8 @@ Ustawienia urządzenia Nie można wykryć języka Tłumaczenie nie powiodło się - Od - Do + Z + Na i 1 inny pisze… piszą… pisze… @@ -525,6 +548,7 @@ Nie udało się pobrać osobistych informacji o użytkowniku. Brak informacji osobistych Dodaj nazwę, zdjęcie profilowe i dane kontaktowe do swojego profilu. + Wideo rozmowa Jaki jest Twój status? %d głos diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 50157e3f5..40873b1e4 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1,7 +1,9 @@ Ícone da conta + Conversa adicionada %1$s para favoritos Pesquisar em %s + Chamada de áudio Bluetooth Saída de áudio Celular @@ -24,6 +26,7 @@ Personalizar Zona de perigo Excluir avatar + Conversa excluída %1$s Não perturbe Não limpe Editar @@ -37,10 +40,15 @@ %1$s (%2$d) 4 horas Invisível + Não foi possível recuperar os idiomas + Falha na recuperação Hoje mais tarde + Você saiu da conversa %1$s Carregar mais resultados Símbolo de cadeado Baixar a mão + Conversa marcada %1$s como lida + Conversa marcada %1$s como não lida Mentioned Mais novo primeiro Mais antigo primeiro @@ -48,6 +56,7 @@ Z - A Maior primeiro Menor primeiro + Expiração da mensagem Toque para abrir a enquete Nenhum resultado encontrado Comece a digitar para pesquisar … @@ -95,6 +104,7 @@ Para ativar a comunicação por vídeo, conceda a permissão \"Câmera\". Cancelar Falhou ao buscar recursos, interrompendo + Rubrica Quer confiar em um certificado SSL desconhecido, emitido por %1$s para %2$s, válido de %3$s a %4$s? Confira o certificado Sua configuração SSL impediu a conexão @@ -134,6 +144,7 @@ Se você excluir a conversa, ela também será excluída para todos os outros participantes. Excluir Mensagem excluída com sucesso, mas pode ter sido vazada para outros serviços + Usuário %1$s foi removido Rebaixar de moderador Gravar mensagem de voz Enviar mensagem @@ -144,7 +155,10 @@ O aplicativo é muito antigo e não é mais suportado por este servidor. Por favor atualize. Atualizar Quer reautorizar ou excluir esta conta? + Salvar esta mídia no armazenamento permitirá que qualquer outro aplicativo em seu dispositivo a acesse. + Continuar? Não + Salvar no armazenamento? Sim Nome de exibição não obtido, interrompendo Não foi possível armazenar o nome de exibição, cancelando @@ -279,6 +293,7 @@ Responder Responder privadamente Salvar + Salvo com sucesso 30 segundos 5 minutos 1 minuto @@ -333,6 +348,7 @@ Notificações Mensagens Combine os contatos com base no número de telefone para integrar o atalho do Bate Papo no aplicativo de contatos do sistema + Erro 429 Muitas Solicitações Você pode definir seu número de telefone para que outros usuários possam encontrar você Número de telefone inválido Número de telefone definido com sucesso @@ -445,7 +461,14 @@ Parar gravação de chamada Pare de gravar Stopping recording … + O consentimento de gravação é necessário para todas as chamadas + A gravação pode incluir sua voz, vídeo da câmera e compartilhamento de tela. Seu consentimento é necessário antes de entrar na chamada. Você consente? + Exigir consentimento de gravação antes de participar da chamada nesta conversa + Consentimento de gravação + A chamada pode ser gravada. Gravação + Conversa removida %1$s dos favoritos + Conversação %1$s foi renomeada It\'s not possible to join other rooms while being in a call Salvar Sincronizar apenas com servidores confiáveis  @@ -480,6 +503,7 @@ Voz Favorito Você não tem permissão para iniciar uma ligação + iniciou uma chamada Mensagem de status Switch to breakout room Switch to main room @@ -524,6 +548,7 @@ Falha ao recuperar informações pessoais do usuário. Nenhuma informação pessoal definida Adicione nome, foto e detalhes de contato em sua página de perfil. + Video Chamada Qual é o seu status? %d votos diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 26d46f1e1..b0db178a0 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -156,7 +156,7 @@ Требуется разрешение на доступ к файлу Пользователь, вошедший по ссылке Вы: %1$s - Вперёд + Переслать Вперёд к … Нет своего сервера?\nНажмите здесь чтобы заказать у провайдера Исходный код @@ -201,7 +201,7 @@ Коснитесь для разблокирования Не задано Отметить прочитанным - Отметить не прочитанным + Отметить непрочитанным Не удалось отправить сообщение: Отменить ответ Сообщение прочитано @@ -262,6 +262,7 @@ Ответить Ответить личным сообщением Сохранить + Сохранено успешно 30 секунд 5 минут 1 минута diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index 7b969dd5b..967c79ce2 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -206,6 +206,7 @@ Risponde Risponde in privadu Sarva + Sarvadu 30 segundos 5 minutos 1 minutu diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index 64f82d4fc..c36c6858f 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -279,6 +279,7 @@ Odpoveď Odpovedať súkromne Uložiť + Úspešne uložené 30 sekúnd 5 minút 1 minúta diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index bad7e4dcd..b24de4b61 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -261,6 +261,7 @@ Odgovori Odgovori zasebno Shrani + Uspešno shranjeno 30 sekund 5 minut 1 minuta diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 231651c2d..4edb8281a 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -40,6 +40,8 @@ %1$s (%2$d) 4 сата Невидљива + Није успело преузимање језика + Није успело преузимање Касније данас Напустили сте разговор %1$s Учитај још резултата @@ -102,6 +104,7 @@ Ако желите да укључите видео комуникацију, молимо вас да дате дозволу „Камера” Откажи Грешка приликом дохватања могућности, прекидам + Наслов Да ли верујете, до сада непознатом, SSL сертификату, који је издао %1$s на %2$s, валидним од %3$s до %4$s? Провери сертификат Ваша ССЛ постава је онемогућила повезивање @@ -141,6 +144,7 @@ Ако обришете разговор, он ће бити обрисан и за све друге учеснике. Избриши Порука је успешно обрисана, али је можда процурела на друге сервисе + Корисник %1$s је уклоњен Скини улогу модератора Сними гласовну поруку Пошаљи поруку @@ -151,7 +155,10 @@ Ова апликација је превише стара и овај сервер је више не подржава. Молимо вас да је ажурирате. Ажурирај Да ли желите поново да ауторизујете или обришете овај налог? + Ако овај медијум сачувате на складиште, остале апликације на уређаје ће моћи да му приступе. + Желите ли да наставите? Не + Да сачувам на складиште?` Да Име за приказ се не може добавити. Прекидам Не могу да сачувам име за приказ. Прекидам @@ -286,6 +293,7 @@ Одговори Одговори приватно Сачувај + Успешно сачувано 30 секунди 5 минута 1 минут diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e3596d719..737c2cc1d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -40,6 +40,8 @@ %1$s (%2$d) 4 saat Görünmez + Diller alınamadı + Alınamadı Bugün daha sonra %1$s görüşmesinden ayrıldınız Diğer sonuçları yükle @@ -102,6 +104,7 @@ Görüntülü iletişim kurabilmek için \"Kamera\" için erişme izni verin. İptal Yeterlilikler alınamadı, vazgeçiliyor + Alt yazı Şimdiye kadar bilinmeyen, %1$s tarafından %2$s için yayınlanmış %3$s ile %4$s tarihleri arasında geçerli olan sertifikaya güvenilsin mi? Sertifikayı denetleyin SSL kurulumunuz bağlantı kurulmasını engelliyor @@ -141,6 +144,7 @@ Görüşmeyi silerseniz tüm diğer katılımcılar için de silinecek. Sil İleti silindi ancak başka hizmetlere aktarılmış olabilir + %1$s kullanıcısı kaldırıldı Sorumluluktan çıkar Ses iletisi kaydet İleti gönder @@ -151,7 +155,10 @@ Uygulama çok eski ve artık bu sunucu tarafından desteklenmiyor. Lütfen güncelleyin. Güncelle Bu hesabı yeniden etkinleştirmek ya da silmek ister misiniz? + Bu ortamı depolama alanına kaydetmek, aygıtınızdaki diğer uygulamaların da buna erişmesine izin verir. + İlerlemek istiyor musunuz? Hayır + Depolama alanına kaydedilsin mi? Evet Görüntülenecek ad alınamadı, vazgeçiliyor Görüntülenecek ad kaydedilemedi, vazgeçiliyor @@ -286,6 +293,7 @@ Yanıtla Kişisel yanıt gönder Kaydet + Kaydedildi 30 saniye 5 dakika 1 dakika diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index de5334bec..18a300464 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -2,6 +2,7 @@ Зображення облікового запису Пошук у %s + Голосовий виклик Bluetooth Вивід аудіо Телефон @@ -134,6 +135,8 @@ Режим технічного обслуговування Оновлення Чи хочете ви повторно авторизувати чи вилучити цей акаунт? + Ні + Так Ім\'я для показу не вдалося розпізнати. Скасування Неможливо отримати ім\'я для показу. Скасування Електронна пошта @@ -258,6 +261,7 @@ Відповісти Відповісти у приватній розмові Зберегти + Збережено успішно 30 секунд 5 хвилин Одна хвилина @@ -267,7 +271,7 @@ 30 300 Пошук - Обрати акаунт + Оберіть обліковий запис Виберіть учасників %1$s надсилає GIF. Ви надіслали GIF. diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml index f1c0083c1..e62af63b4 100644 --- a/app/src/main/res/values-v27/styles.xml +++ b/app/src/main/res/values-v27/styles.xml @@ -8,6 +8,9 @@ true true shortEdges + @color/bg_default + @style/Theme.AppCompat.DayNight.Dialog + ?alertDialogTheme - \ No newline at end of file + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e841574fe..1a69a12d6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,7 +1,9 @@ 账户图标 + 对话%1$s已添加至收藏 搜索位置 %s + 语音通话 蓝牙 音频输出 电话 @@ -11,17 +13,20 @@ 离开 日历 高级呼叫选项 + 通话已持续一小时 无通知呼叫 摄像头访问已授权。请重新选择摄像头。 从云端选择头像 清除状态消息 过多长时间清除状态消息 关闭 + 锁定录音以连续录制语音信息 对话 建立对话 自定义 危险区域 删除头像 + 对话%1$s已删除 不要打扰 不要清除 编辑 @@ -35,9 +40,13 @@ %1$s (%2$d) 4小时 不可见 + 今日稍晚 + 你已离开对话%1$s 加载更多结果 锁定头像 放下手 + 已将对话%1$s标记为已读 + 已将对话%1$s标记为未读 已提及 最新的在前 最旧的在前 @@ -66,6 +75,7 @@ 允许访客 固定:%1$s 解锁 %1$s + 使用蓝牙音箱需要“附近的设备”权限 高级呼叫选项 以视频通话接听 以语音通话接听 @@ -89,6 +99,7 @@ %s 通话 %s 语音通话 %s 语音通话 + 使用视频通话需要“相机”权限 取消 获取容量已失败,正在中止 您是否信任由 %1$s 颁发给 %2$s,生效时间从 %3$s 至 %4$s的未知证书? @@ -140,7 +151,10 @@ 应用版本过低,不再被服务器支持,请更新 更新 你想重新授权还是删除此帐户? + 将此媒体保存到存储空间中将允许您设备上的所有其他应用访问 + 是否继续? + 保存到存储空间中? 无法获取显示名,中止 无法储存显示名称,正在终止 @@ -212,6 +226,7 @@ 取消回复 消息已读 消息已发送 + 使用语音通话需要“麦克风”权限 你错过了来自 %s 的一个通话 主持人 从未加入 @@ -244,7 +259,11 @@ 参与者 添加参与者 密码 + 设置权限 + 部分权限被拒绝 + 请允许权限 打开设置 + 请在 “设置” > “权限” 中开启权限 未找到账户 通过 %s 聊天 将麦克风静音 @@ -270,6 +289,7 @@ 回复 私下回复 保存 + 已成功保存 30秒 5分钟 1分钟 @@ -316,14 +336,15 @@ 高级 外观 通话 - 通知键盘禁用个性化学习(不保证键盘会执行) - 匿名键盘 + 要求输入法禁用个性化学习(不保证键盘会执行) + 无痕键盘 无声音 您进行身份验证的服务器上未安装通话应用 通知声音 通知 消息 根据电话号码匹配联系人,以集成通话应用快捷方式到系统联系人应用 + 错误 429 请求太多 你可以设置你的电话号码,这样其他用户能够找到你 无效电话号码 成功设置电话号码 @@ -436,7 +457,14 @@ 停止通话录制 停止录制 正在停止录制…… + 所有通话都需要同意才可录制 + 录制内容可能包括您的声音、摄像头的画面,以及屏幕分享的内容。要加入通话,您需要同意录制。您是否同意? + 在此对话中加入通话前要求准许录制 + 录制许可 + 通话可能被录制 录音 + 对话%1$s已从收藏中移除 + 对话%1$s已重命名 在通话时无法加入其他房间 保存 只同步到受信任的服务器 @@ -451,6 +479,7 @@ 更改 %1$s 的隐私级别 滚动到底部 几秒前 + 查看%1$s条相似的消息 已选中 发送到 你不能在此聊天中分享内容 @@ -470,6 +499,7 @@ 语音 收藏 您没有开始通话的权限。 + 发起了通话 状态消息 切换到分组讨论 切换到主房间 @@ -485,6 +515,7 @@ 30分钟 本周 这是一个测试消息 + 本周末 附件 今天 明天 @@ -513,6 +544,7 @@ 检索个人用户信息失败 未设置个人信息 在你的个人资料页上添加姓名、图片和联系方式。 + 视频通话 你什么状态? %d 票 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 8faba1aa2..116a44b8e 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -40,6 +40,8 @@ %1$s(%2$d) 4 小時 隱藏 + 無法擷取語言 + 擷取失敗 今日稍後 你離開了對話 %1$s 正在載入更多結果 @@ -102,6 +104,7 @@ 要啟用影像通訊,請授予「相機」權限。 取消 無法獲取功能,正在中止 + 說明 您是否信任 %1$s 為 %2$s 發行的,有效期從 %3$s 到 %4$s,迄今不詳的SSL證書? 請檢查憑證 您的SSL設定阻擋這連線。 @@ -141,6 +144,7 @@ 如果您刪除了此對話,它也將會從所有其他參與者處刪除。 刪除 消息已成功刪除,但可能已分發到其他服務 + 用户 %1$s 已被移除 從主持人降級 錄製話音短訊 傳送訊息 @@ -151,7 +155,10 @@ 該應用程式太舊,不再受此伺服器支持。請更新。 更新 您要重新授權或刪除此賬戶嗎? + 將此媒體儲存到儲存空間中將允許您裝置上的任何其他應用程式存取它。 + 繼續? + 保存到儲存裝置中? 無法取得顯示名稱,將停止操作。 無法儲存顯示名稱,操作中斷。 @@ -286,6 +293,7 @@ 回覆 私下回覆 保存 + 保存成功 30秒 5分鐘 一分鐘 @@ -454,6 +462,7 @@ 停止錄音 停止錄音 ... 所有通話都需要獲得錄製同意 + 錄製可能包括您的聲音、視像和螢幕共享。在您加入通話之前,需要獲得您的同意。您同意嗎? 在此次對話中,需要在加入通話前給予錄製同意 錄製同意 通話可能會被錄製。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 41b602faa..898a41dc1 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -40,6 +40,8 @@ %1$s (%2$d) 4小時 隱藏 + 無法擷取語言 + 擷取失敗 今天稍後 您離開了對話 %1$s 載入更多結果 @@ -102,6 +104,7 @@ 要啟用影像通訊,請授予「相機」權限。 取消 無法獲取功能,正在中止 + 字幕 您是否信任 %1$s 為 %2$s 發行的,有效期從 %3$s 到 %4$s,迄今不詳的 SSL 憑證? 請檢查憑證 您的SSL設定阻擋這連線。 @@ -141,6 +144,7 @@ 如果您刪除了此對話,它也將會從所有其他參與者處刪除。 刪除 訊息已成功刪除,但可能已分發到其他服務 + 使用 %1$s 已移除 取消主持人資格 錄製語音訊息 傳送訊息 @@ -151,7 +155,10 @@ 應用程式太舊了,不再被此伺服器支援。請更新。 更新 您要重新授權或刪除此帳號嗎? + 將此媒體儲存到儲存空間中將允許您裝置上的任何其他應用程式存取它。 + 繼續? + 儲存到儲存裝置中? 無法取得顯示名稱,將停止動作。 無法儲存顯示名稱,操作中斷。 @@ -286,6 +293,7 @@ 回覆 私下回覆 儲存 + 已成功儲存 30秒 5分鐘 一分鐘 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5d7ec0cf3..982641554 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -61,10 +61,10 @@ #D32F2F #FF9800 #006400 - #E8E8E8 + #E8E8E8 #757575 #D5D5D5 - #E9FFFFFF + #E9FFFFFF #111111 #767676 #DBDBDB diff --git a/app/src/main/res/values/setup.xml b/app/src/main/res/values/setup.xml index 8bf763543..7d3e5bd5a 100644 --- a/app/src/main/res/values/setup.xml +++ b/app/src/main/res/values/setup.xml @@ -35,7 +35,7 @@ false - false + true true diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e673f0da9..f60b4e5bd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,7 +51,6 @@ How to translate with transifex: Sorry, something went wrong! Create - Settings @@ -519,14 +518,14 @@ How to translate with transifex: Chat via %s Account not found - //save feature + Save Save to storage? - Saving this media to storage will allow any other apps on your device to access it.\nContinue? + Saving this media to storage will allow any other apps on your device to access it. + Continue? Yes No - - + Saved successfully Favorite Status @@ -575,6 +574,7 @@ How to translate with transifex: %1$s (%2$d) Invalid password Do you want to reauthorize or delete this account? + User %1$s was removed App is outdated The app is too old and no longer supported by this server. Please update. @@ -722,4 +722,7 @@ How to translate with transifex: Audio Call started a call Error 429 Too Many Requests + Caption + Retrieval failed + Languages could not be retrieved diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 0450185fe..bd1570d13 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -221,6 +221,9 @@ @color/transparent true true + @color/bg_default + @style/Theme.AppCompat.DayNight.Dialog + ?alertDialogTheme