Merge branch 'master' into emoji2

Signed-off-by: Smarshall <99678760+Smarshal21@users.noreply.github.com>
This commit is contained in:
Smarshall 2023-12-16 20:35:45 +05:30 committed by GitHub
commit 561fd037c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
210 changed files with 3633 additions and 3271 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -289,7 +289,6 @@ We are using [Dagger 2](https://dagger.dev/) to inject dependencies into major A
* `Activity`
* `Fragment`
* `Controller`
* `Service`
* `BroadcastReceiver`
* `ContentProvider`

View File

@ -7,7 +7,7 @@
* @author Tim Krüger
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2021-2023 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
*
* This program is free software: you can redistribute it and/or modify
@ -24,6 +24,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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/")
}

View File

@ -32,6 +32,5 @@
<issue id="ObsoleteLintCustomCheck" severity="warning">
<ignore path="**/jetified-annotation-experimental-1.**/**/lint.jar" />
<ignore path="**/jetified-conductor-2.**/**/lint.jar" />
</issue>
</lint>

View File

@ -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"
)
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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()

View File

@ -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
}
}

View File

@ -4,7 +4,7 @@
~ @author Mario Danic
~ @author Marcel Hibbe
~ Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
~ Copyright (C) 2021-2022 Marcel Hibbe <dev@mhibbe.de>
~ Copyright (C) 2021-2023 Marcel Hibbe <dev@mhibbe.de>
~
~ 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 @@
</intent-filter>
</activity>
<activity
android:name=".account.ServerSelectionActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".account.WebViewLoginActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".account.AccountVerificationActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".account.SwitchAccountActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".conversationlist.ConversationsListActivity"
android:theme="@style/AppTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<activity
android:name=".chat.ChatActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".activities.CallActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
@ -152,17 +190,17 @@
android:theme="@style/AppTheme.CallLauncher" />
<activity
android:name=".activities.FullScreenImageActivity"
android:name=".fullscreenfile.FullScreenImageActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/FullScreenImageTheme"/>
<activity
android:name=".activities.FullScreenMediaActivity"
android:name=".fullscreenfile.FullScreenMediaActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/FullScreenMediaTheme"/>
<activity
android:name=".activities.FullScreenTextViewerActivity"
android:name=".fullscreenfile.FullScreenTextViewerActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/FullScreenTextTheme"/>
@ -215,32 +253,10 @@
android:name=".contacts.ContactsActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".chat.ChatActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".openconversations.ListOpenConversationsActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".conversationlist.ConversationsListActivity"
android:theme="@style/AppTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<activity
android:name=".lock.LockedActivity"
android:theme="@style/AppTheme" />

View File

@ -3,6 +3,8 @@
*
* @author Mario Danic
* @author Andy Scherzinger
* @author Marcel Hibbe
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* 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 <http://www.gnu.org/licenses/>.
*/
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<Disposable> = 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<CapabilitiesOverall> {
@ -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)
}
}
}
}

View File

@ -3,6 +3,8 @@
*
* @author Andy Scherzinger
* @author Mario Danic
* @author Marcel Hibbe
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* 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 <http://www.gnu.org/licenses/>.
*/
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
}
}

View File

@ -3,6 +3,8 @@
*
* @author Mario Danic
* @author Andy Scherzinger
* @author Marcel Hibbe
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
*
@ -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)
}
}

View File

@ -3,6 +3,8 @@
*
* @author Mario Danic
* @author Andy Scherzinger
* @author Marcel Hibbe
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* 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 <http://www.gnu.org/licenses/>.
*/
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<String, String> = 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

View File

@ -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;

View File

@ -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<View>(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<String> = 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
}
}

View File

@ -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<AudioDevice>
) {
private fun onAudioManagerDevicesChanged(currentDevice: AudioDevice, availableDevices: Set<AudioDevice>) {
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<RemoteAction>()
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) {

View File

@ -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
}

View File

@ -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<User>) {
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
}
}

View File

@ -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(

View File

@ -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

View File

@ -124,6 +124,6 @@ class CallStartedViewHolder(incomingView: View, payload: Any) :
}
companion object {
var TAG: String? = CallStartedViewHolder::class.simpleName
val TAG: String? = CallStartedViewHolder::class.simpleName
}
}

View File

@ -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
}
}

View File

@ -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<String, HashMap<String, String>> 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; }
}

View File

@ -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)

View File

@ -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 = ""

View File

@ -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
}
}

View File

@ -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<String, HashMap<String, String>> 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; }
}

View File

@ -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)

View File

@ -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

View File

@ -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<Void> registerDeviceForNotificationsWithPushProxy(@Url String url,
Observable<Unit> registerDeviceForNotificationsWithPushProxy(@Url String url,
@FieldMap Map<String, String> fields);
@ -675,6 +677,10 @@ public interface NcApi {
@Query("toLanguage") String toLanguage,
@Nullable @Query("fromLanguage") String fromLanguage);
@GET
Observable<LanguagesOverall> getLanguages(@Header("Authorization") String authorization,
@Url String url);
@GET
Observable<ReminderOverall> getReminder(@Header("Authorization") String authorization,
@Url String url);

View File

@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers.bottomsheet.items
package com.nextcloud.talk.bottomsheet.items
import android.widget.ImageView
import androidx.annotation.DrawableRes

View File

@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers.bottomsheet.items
package com.nextcloud.talk.bottomsheet.items
import androidx.annotation.CheckResult
import androidx.recyclerview.widget.LinearLayoutManager

View File

@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<IT : ListItemWithImage>(
}
}
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<IT : ListItemWithImage>(
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<IT : ListItemWithImage>(
}
}
override fun replaceItems(
items: List<IT>,
listener: ListItemListener<IT>
) {
override fun replaceItems(items: List<IT>, listener: ListItemListener<IT>) {
this.items = items
if (listener != null) {
this.selection = listener

View File

@ -46,10 +46,7 @@ class ReactionAnimator(
) {
private val reactionsList: MutableList<CallReaction> = 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,

View File

@ -317,7 +317,7 @@ class CallNotificationActivity : CallBaseActivity() {
}
override fun suppressFitsSystemWindows() {
binding!!.controllerCallNotificationLayout.fitsSystemWindows = false
binding!!.callNotificationLayout.fitsSystemWindows = false
}
companion object {

View File

@ -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<String> = 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<String>) {
for (file in files) {
uploadFile(file, false)
private fun uploadFiles(files: MutableList<String>, 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<ChatMessage>) {
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<ChatMessage>
) {
private fun addMessagesToAdapter(shouldAddNewMessagesNotice: Boolean, chatMessageList: List<ChatMessage>) {
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"
}
}

View File

@ -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<ConversationModel> {
override fun getRoom(user: User, roomToken: String): Observable<ConversationModel> {
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<ConversationModel> {
override fun joinRoom(user: User, roomToken: String, roomPassword: String): Observable<ConversationModel> {
val credentials: String = ApiUtils.getCredentials(user.username, user.token)
val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.APIv4, 1))

View File

@ -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())

View File

@ -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<Nothing>?)
}
binding.controllerGenericRv?.swipeRefreshLayout?.isEnabled = !adapter!!.hasFilter()
return true
}

View File

@ -1,256 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Andy Scherzinger
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package 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<AbstractFlexibleItem<*>> = 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<RingtoneSettings>(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<Nothing>?, 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)
}

View File

@ -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<String> = 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
}
}

View File

@ -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 <T : ViewBinding> Controller.viewBinding(bindingFactory: (View) -> T) =
ControllerViewBindingDelegate(this, bindingFactory)
class ControllerViewBindingDelegate<T : ViewBinding>(
controller: Controller,
private val viewBinder: (View) -> T
) : ReadOnlyProperty<Controller, T?>, 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
}
}

View File

@ -27,13 +27,7 @@ import io.reactivex.Observable
interface ConversationRepository {
fun renameConversation(
roomToken: String,
roomNameNew: String
): Observable<GenericOverall>
fun renameConversation(roomToken: String, roomNameNew: String): Observable<GenericOverall>
fun createConversation(
roomName: String,
conversationType: Conversation.ConversationType?
): Observable<RoomOverall>
fun createConversation(roomName: String, conversationType: Conversation.ConversationType?): Observable<RoomOverall>
}

View File

@ -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<GenericOverall> {
override fun renameConversation(roomToken: String, roomNameNew: String): Observable<GenericOverall> {
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
return ncApi.renameRoom(

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -24,10 +24,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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

View File

@ -24,7 +24,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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)
}

View File

@ -22,7 +22,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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)
}
}
}

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -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);

View File

@ -1,10 +1,10 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* @author Fariba Khandani
* @author Marcel Hibbe
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2023 Fariba Khandani <khandani@winworker.de>
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
*
* 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"

View File

@ -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<String> = ArrayList()
paths.add(remotePath)

View File

@ -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)

View File

@ -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) {

View File

@ -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<out String>,
grantResults: IntArray
) {
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
fun areAllGranted(grantResults: IntArray): Boolean {

View File

@ -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
}

View File

@ -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,

View File

@ -26,8 +26,10 @@ package com.nextcloud.talk.models.json.chat
class ChatUtils {
companion object {
fun getParsedMessage(message: String?, messageParameters: HashMap<String?, HashMap<String?, String?>>?):
String? {
fun getParsedMessage(
message: String?,
messageParameters: HashMap<String?, HashMap<String?, String?>>?
): 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<String?, HashMap<String?, String?>>,
message: String?
): String? {
private fun parse(messageParameters: HashMap<String?, HashMap<String?, String?>>, message: String?): String? {
var resultMessage = message
for (key in messageParameters.keys) {
val individualHashMap = messageParameters[key]

View File

@ -20,5 +20,7 @@
package com.nextcloud.talk.models.json.chat
enum class ReadStatus {
NONE, SENT, READ
NONE,
SENT,
READ
}

View File

@ -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

View File

@ -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 {

View File

@ -41,6 +41,8 @@ data class ReactionVoter(
constructor() : this(null, null, null, 0)
enum class ReactionActorType {
DUMMY, GUESTS, USERS
DUMMY,
GUESTS,
USERS
}
}

View File

@ -42,9 +42,7 @@ class OpenConversationsRepositoryImpl(private val ncApi: NcApi, currentUserProvi
).map { mapToOpenConversationsModel(it.ocs?.data!!) }
}
private fun mapToOpenConversationsModel(
conversations: List<Conversation>
): OpenConversationsModel {
private fun mapToOpenConversationsModel(conversations: List<Conversation>): OpenConversationsModel {
return OpenConversationsModel(
conversations.map { conversation ->
OpenConversation(

View File

@ -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)
}
}
}

View File

@ -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
)

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -41,8 +41,8 @@ class PollResultsViewModel @Inject constructor() : ViewModel() {
val poll: Poll?
get() = _poll
private var _itemsOverviewList: ArrayList<PollResultItem> = ArrayList()
private var _itemsDetailsList: ArrayList<PollResultItem> = ArrayList()
private var itemsOverviewList: ArrayList<PollResultItem> = ArrayList()
private var itemsDetailsList: ArrayList<PollResultItem> = ArrayList()
private var _items: MutableLiveData<ArrayList<PollResultItem>?> = MutableLiveData<ArrayList<PollResultItem>?>()
val items: MutableLiveData<ArrayList<PollResultItem>?>
@ -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
}
}

View File

@ -608,7 +608,7 @@ class ProfileActivity : BaseActivity() {
class UserInfoAdapter(
displayList: List<UserInfoDetailsItem>?,
private val viewThemeUtils: ViewThemeUtils,
private val controller: ProfileActivity
private val profileActivity: ProfileActivity
) : RecyclerView.Adapter<UserInfoAdapter.ViewHolder>() {
var displayList: List<UserInfoDetailsItem>?
var filteredDisplayList: MutableList<UserInfoDetailsItem> = 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

View File

@ -24,11 +24,7 @@ import io.reactivex.Observable
interface RequestAssistanceRepository {
fun requestAssistance(
roomToken: String
): Observable<RequestAssistanceModel>
fun requestAssistance(roomToken: String): Observable<RequestAssistanceModel>
fun withdrawRequestAssistance(
roomToken: String
): Observable<WithdrawRequestAssistanceModel>
fun withdrawRequestAssistance(roomToken: String): Observable<WithdrawRequestAssistanceModel>
}

View File

@ -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

View File

@ -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),

View File

@ -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")

View File

@ -36,8 +36,7 @@ class RemoteFileBrowserItemsRepositoryImpl @Inject constructor(
private val user: User
get() = userProvider.currentUser.blockingGet()
override fun listFolder(path: String):
Observable<List<RemoteFileBrowserItem>> {
override fun listFolder(path: String): Observable<List<RemoteFileBrowserItem>> {
return Observable.fromCallable {
val operation =
ReadFolderListingOperation(

View File

@ -26,11 +26,7 @@ import io.reactivex.Observable
interface CallRecordingRepository {
fun startRecording(
roomToken: String
): Observable<StartCallRecordingModel>
fun startRecording(roomToken: String): Observable<StartCallRecordingModel>
fun stopRecording(
roomToken: String
): Observable<StopCallRecordingModel>
fun stopRecording(roomToken: String): Observable<StopCallRecordingModel>
}

View File

@ -37,9 +37,7 @@ class CallRecordingRepositoryImpl(private val ncApi: NcApi, currentUserProvider:
var apiVersion = 1
override fun startRecording(
roomToken: String
): Observable<StartCallRecordingModel> {
override fun startRecording(roomToken: String): Observable<StartCallRecordingModel> {
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<StopCallRecordingModel> {
override fun stopRecording(roomToken: String): Observable<StopCallRecordingModel> {
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

View File

@ -27,15 +27,7 @@ import io.reactivex.Observable
interface ReactionsRepository {
fun addReaction(
roomToken: String,
message: ChatMessage,
emoji: String
): Observable<ReactionAddedModel>
fun addReaction(roomToken: String, message: ChatMessage, emoji: String): Observable<ReactionAddedModel>
fun deleteReaction(
roomToken: String,
message: ChatMessage,
emoji: String
): Observable<ReactionDeletedModel>
fun deleteReaction(roomToken: String, message: ChatMessage, emoji: String): Observable<ReactionDeletedModel>
}

View File

@ -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<ReactionAddedModel> {
override fun addReaction(roomToken: String, message: ChatMessage, emoji: String): Observable<ReactionAddedModel> {
return ncApi.sendReaction(
credentials,
ApiUtils.getUrlForMessageReaction(

View File

@ -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<SearchMessageEntry> {
private fun mapToMessageResults(
data: UnifiedSearchResponseData,
searchTerm: String,
limit: Int
): UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry> {
val entries = data.entries?.map { it -> mapToMessage(it, searchTerm) }
val cursor = data.cursor ?: 0
val hasMore = entries?.size == limit

View File

@ -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

View File

@ -28,16 +28,9 @@ import io.reactivex.Observable
interface SharedItemsRepository {
fun media(
parameters: Parameters,
type: SharedItemType
): Observable<SharedItems>?
fun media(parameters: Parameters, type: SharedItemType): Observable<SharedItems>?
fun media(
parameters: Parameters,
type: SharedItemType,
lastKnownMessageId: Int?
): Observable<SharedItems>?
fun media(parameters: Parameters, type: SharedItemType, lastKnownMessageId: Int?): Observable<SharedItems>?
fun availableTypes(parameters: Parameters): Observable<Set<SharedItemType>>

View File

@ -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<SharedItems>? {
override fun media(parameters: SharedItemsRepository.Parameters, type: SharedItemType): Observable<SharedItems>? {
return media(parameters, type, null)
}

View File

@ -98,10 +98,11 @@ class SharedItemsViewModel @Inject constructor(
})
}
private fun chooseInitialType(newTypes: Set<SharedItemType>): SharedItemType = when {
newTypes.contains(SharedItemType.MEDIA) -> SharedItemType.MEDIA
else -> newTypes.toList().first()
}
private fun chooseInitialType(newTypes: Set<SharedItemType>): SharedItemType =
when {
newTypes.contains(SharedItemType.MEDIA) -> SharedItemType.MEDIA
else -> newTypes.toList().first()
}
fun initialLoadItems(type: SharedItemType) {
val state = _viewState.value

View File

@ -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<String>
fun getLanguages(authorization: String, url: String): Observable<List<Language>>
}

Some files were not shown because too many files have changed in this diff Show More