diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..1da62f192 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:focal + +ARG DEBIAN_FRONTEND=noninteractive +ENV ANDROID_HOME=/usr/lib/android-sdk + +RUN apt-get update -y +RUN apt-get install -y unzip wget openjdk-8-jdk vim + +RUN wget https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip -O /tmp/commandlinetools.zip +RUN cd /tmp && unzip commandlinetools.zip +RUN mkdir -p /usr/lib/android-sdk/cmdline-tools/ +RUN cd /tmp/ && mv cmdline-tools/ latest/ && mv latest/ /usr/lib/android-sdk/cmdline-tools/ +RUN mkdir /usr/lib/android-sdk/licenses/ +RUN chmod -R 755 /usr/lib/android-sdk/ +RUN mkdir -p $HOME/.gradle +RUN echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > $HOME/.gradle/gradle.properties diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 000000000..d1c77f7f5 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,5 @@ +# Instructions + +1. Start a DevContainer either on GitHub Codespaces or locally in VSCode +2. Accept all licenses by running `yes | /usr/lib/android-sdk/cmdline-tools/latest/bin/sdkmanager --licenses` +3. You can now build the app using `./gradlew clean build` diff --git a/.devcontainer/devcontainer.env b/.devcontainer/devcontainer.env new file mode 100644 index 000000000..369163cf4 --- /dev/null +++ b/.devcontainer/devcontainer.env @@ -0,0 +1,3 @@ +ANDROID_HOME=/usr/lib/android-sdk +JAVA_OPTS="-Xmx8192M" +GRADLE_OPTS="-Dorg.gradle.daemon=true" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..eb2bcab3b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "NextcloudTalkAndroid", + "dockerFile": "Dockerfile", +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..56dbc10e5 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# You can add one username per supported platform and one custom link +custom: https://nextcloud.com/include/ diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ca927340c..da58e1a6c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,16 @@ version: 2 updates: -- package-ecosystem: gradle - directory: "/" - schedule: - interval: daily - time: "03:00" - timezone: Europe/Paris - open-pull-requests-limit: 10 - labels: - - 3. to review + - package-ecosystem: "github-actions" + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: gradle + directory: "/" + schedule: + interval: daily + time: "03:00" + timezone: Europe/Paris + open-pull-requests-limit: 10 + labels: + - 3. to review + - dependencies diff --git a/.github/workflows/assembleFlavors.yml b/.github/workflows/assembleFlavors.yml new file mode 100644 index 000000000..fc3b946c6 --- /dev/null +++ b/.github/workflows/assembleFlavors.yml @@ -0,0 +1,30 @@ +name: "Assemble" + +on: + pull_request: + branches: [ master, stable-* ] + +jobs: + flavor: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + flavor: [ Generic, Gplay ] + steps: + - uses: actions/checkout@v2 + - name: set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Build ${{ matrix.flavor }} + run: | + echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" >> gradle.properties + ./gradlew assemble${{ matrix.flavor }} + - name: Archive apk + uses: actions/upload-artifact@v2 + if: ${{ always() }} + with: + name: Nextcloud-APK + path: app/build/outputs/apk/**/**/*.apk + retention-days: 5 diff --git a/.github/workflows/autoApproveDependabot.yml b/.github/workflows/autoApproveDependabot.yml new file mode 100644 index 000000000..ce4498d7e --- /dev/null +++ b/.github/workflows/autoApproveDependabot.yml @@ -0,0 +1,13 @@ +name: Auto approve +on: + pull_request_target: + branches: [ master, stable-* ] + +jobs: + auto-approve: + runs-on: ubuntu-latest + steps: + - uses: hmarr/auto-approve-action@v2.0.0 + if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 000000000..261effcd2 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,21 @@ +name: Check + +on: + pull_request: + branches: [ master, stable-* ] + +jobs: + check: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + task: [ detekt, ktlint ] + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Check ${{ matrix.task }} + run: ./gradlew ${{ matrix.task }} diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 000000000..09307118b --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,13 @@ +name: "Validate Gradle Wrapper" + +on: + pull_request: + branches: [ master, stable-* ] + +jobs: + validation: + name: "Validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 000000000..431267bb4 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,29 @@ +name: "QA" + +on: + pull_request: + branches: [ master, stable-* ] + +jobs: + qa: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Build QA + env: + KS_PASS: ${{ secrets.KS_PASS }} + KEY_PASS: ${{ secrets.KEY_PASS }} + LOG_USERNAME: ${{ secrets.LOG_USERNAME }} + LOG_PASSWORD: ${{ secrets.LOG_PASSWORD }} + run: | + mkdir -p $HOME/.gradle + echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > $HOME/.gradle/gradle.properties + sed -i "/qa/,/\}/ s/versionCode .*/versionCode ${{github.event.number}} /" app/build.gradle + sed -i "/qa/,/\}/ s/versionName .*/versionName \"${{github.event.number}}\"/" app/build.gradle + ./gradlew assembleQaDebug + $(find /usr/local/lib/android/sdk/build-tools/*/apksigner | sort | tail -n1) sign --ks-pass pass:$KS_PASS --key-pass pass:$KEY_PASS --ks-key-alias key0 --ks scripts/QA_keystore.jks app/build/outputs/apk/qa/debug/app-qa-*.apk + sudo scripts/uploadArtifact.sh $LOG_USERNAME $LOG_PASSWORD ${{github.event.number}} ${{github.event.number}} ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..3a6af4421 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,19 @@ +name: 'Close stale issues' +on: + schedule: + - cron: '* */2 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + days-before-stale: 28 + days-before-close: 14 + days-before-pr-close: -1 + only-labels: 'bug,needs info/discussion' + stale-issue-message: 'This bug report did not receive an update in the last 4 weeks. + Please take a look again and update the issue with new details, + otherwise the issue will be automatically closed in 2 weeks. Thank you!' + exempt-all-pr-milestones: true diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..59163c1e4 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,219 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml new file mode 100644 index 000000000..7d04a74be --- /dev/null +++ b/.idea/inspectionProfiles/ktlint.xml @@ -0,0 +1,7 @@ + + + + diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 000000000..64580d143 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/build.gradle b/app/build.gradle index 357dbe3c5..7f9f495e1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,6 +23,11 @@ apply plugin: 'kotlin-android' apply plugin: 'findbugs' apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-android-extensions' +apply plugin: 'io.gitlab.arturbosch.detekt' + +configurations { + ktlint +} def taskRequest = getGradle().getStartParameter().getTaskRequests().toString() if (taskRequest.contains("Gplay") || taskRequest.contains("findbugs") || taskRequest.contains("lint")) { @@ -33,7 +38,6 @@ android { compileSdkVersion 29 buildToolsVersion '28.0.3' defaultConfig { - applicationId "com.nextcloud.talk2" minSdkVersion 21 targetSdkVersion 29 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -49,9 +53,20 @@ android { productFlavors { // used for f-droid - generic - gplay - + generic { + applicationId 'com.nextcloud.talk2' + dimension "default" + } + gplay { + applicationId 'com.nextcloud.talk2' + dimension "default" + } + qa { + applicationId "com.nextcloud.talk2.qa" + dimension "default" + versionCode 1 + versionName "1" + } } // Enabling multidex support. @@ -86,6 +101,7 @@ android { } packagingOptions { + exclude 'META-INF/LICENSE.txt' exclude 'META-INF/LICENSE' exclude 'META-INF/NOTICE' exclude 'META-INF/rxjava.properties' @@ -161,6 +177,7 @@ dependencies { implementation ('com.gitlab.bitfireAT:dav4jvm:f2078bc846', { exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser }) + ktlint "com.pinterest:ktlint:0.41.0" implementation 'org.conscrypt:conscrypt-android:2.5.1' @@ -272,6 +289,30 @@ dependencies { findbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.4.6' } +task ktlint(type: JavaExec, group: "verification") { + description = "Check Kotlin code style." + main = "com.pinterest.ktlint.Main" + classpath = configurations.ktlint + args "--reporter=plain", "--reporter=plain,output=${buildDir}/ktlint.txt,src/**/*.kt" +} + +task ktlintFormat(type: JavaExec, group: "formatting") { + description = "Fix Kotlin code style deviations." + main = "com.pinterest.ktlint.Main" + classpath = configurations.ktlint + args "-F", "src/**/*.kt" +} + +detekt { + reports { + xml { + enabled = false + } + } + config = files("../detekt.yml") + input = files("src/") +} + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { jvmTarget = "1.8" diff --git a/app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt b/app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt index 32ac819e6..f94110196 100644 --- a/app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt +++ b/app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt @@ -104,7 +104,6 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() { @Inject var eventBus: EventBus? = null - override fun onCreate() { super.onCreate() sharedApplication!!.componentApplication.inject(this) @@ -148,18 +147,26 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() { val pushUtils = PushUtils() val privateKey = pushUtils.readKeyFromFile(false) as PrivateKey try { - signatureVerification = pushUtils.verifySignature(base64DecodedSignature, - base64DecodedSubject) + signatureVerification = pushUtils.verifySignature( + base64DecodedSignature, + base64DecodedSubject + ) if (signatureVerification!!.signatureValid) { val cipher = Cipher.getInstance("RSA/None/PKCS1Padding") cipher.init(Cipher.DECRYPT_MODE, privateKey) val decryptedSubject = cipher.doFinal(base64DecodedSubject) - decryptedPushMessage = LoganSquare.parse(String(decryptedSubject), - DecryptedPushMessage::class.java) + decryptedPushMessage = LoganSquare.parse( + String(decryptedSubject), + DecryptedPushMessage::class.java + ) decryptedPushMessage?.apply { timestamp = System.currentTimeMillis() if (delete) { - cancelExistingNotificationWithId(applicationContext, signatureVerification!!.userEntity, notificationId) + cancelExistingNotificationWithId( + applicationContext, + signatureVerification!!.userEntity, + notificationId + ) } else if (deleteAll) { cancelAllNotificationsForAccount(applicationContext, signatureVerification!!.userEntity) } else if (type == "call") { @@ -171,39 +178,66 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() { fullScreenIntent.putExtras(bundle) fullScreenIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK - val fullScreenPendingIntent = PendingIntent.getActivity(this@MagicFirebaseMessagingService, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val fullScreenPendingIntent = PendingIntent.getActivity( + this@MagicFirebaseMessagingService, + 0, + fullScreenIntent, + PendingIntent.FLAG_UPDATE_CURRENT + ) - val audioAttributesBuilder = AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + val audioAttributesBuilder = + AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST) val ringtonePreferencesString: String? = appPreferences!!.callRingtoneUri val soundUri = if (TextUtils.isEmpty(ringtonePreferencesString)) { - Uri.parse("android.resource://" + applicationContext.packageName + - "/raw/librem_by_feandesign_call") + Uri.parse( + "android.resource://" + applicationContext.packageName + + "/raw/librem_by_feandesign_call" + ) } else { try { - val ringtoneSettings = LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java) + val ringtoneSettings = + LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java) ringtoneSettings.ringtoneUri } catch (exception: IOException) { Uri.parse("android.resource://" + applicationContext.packageName + "/raw/librem_by_feandesign_call") } } - val notificationChannelId = NotificationUtils.getNotificationChannelId(applicationContext.resources - .getString(R.string.nc_notification_channel_calls), applicationContext.resources - .getString(R.string.nc_notification_channel_calls_description), true, - NotificationManagerCompat.IMPORTANCE_HIGH, soundUri!!, audioAttributesBuilder.build(), null, false) + val notificationChannelId = NotificationUtils.getNotificationChannelId( + applicationContext.resources + .getString(R.string.nc_notification_channel_calls), + applicationContext.resources + .getString(R.string.nc_notification_channel_calls_description), + true, + NotificationManagerCompat.IMPORTANCE_HIGH, + soundUri!!, + audioAttributesBuilder.build(), + null, + false + ) - createNotificationChannel(applicationContext!!, - notificationChannelId, applicationContext.resources - .getString(R.string.nc_notification_channel_calls), applicationContext.resources - .getString(R.string.nc_notification_channel_calls_description), true, - NotificationManagerCompat.IMPORTANCE_HIGH, soundUri, audioAttributesBuilder.build(), null, false) + createNotificationChannel( + applicationContext!!, + notificationChannelId, + applicationContext.resources + .getString(R.string.nc_notification_channel_calls), + applicationContext.resources + .getString(R.string.nc_notification_channel_calls_description), + true, + NotificationManagerCompat.IMPORTANCE_HIGH, + soundUri, + audioAttributesBuilder.build(), + null, + false + ) val uri = Uri.parse(signatureVerification!!.userEntity.baseUrl) val baseUrl = uri.host - val notification = NotificationCompat.Builder(this@MagicFirebaseMessagingService, notificationChannelId) + val notification = + NotificationCompat.Builder(this@MagicFirebaseMessagingService, notificationChannelId) .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_CALL) .setSmallIcon(R.drawable.ic_call_black_24dp) @@ -213,7 +247,7 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() { .setContentTitle(EmojiCompat.get().process(decryptedPushMessage!!.subject)) .setAutoCancel(true) .setOngoing(true) - //.setTimeoutAfter(45000L) + // .setTimeoutAfter(45000L) .setContentIntent(fullScreenPendingIntent) .setFullScreenIntent(fullScreenPendingIntent, true) .setSound(soundUri) @@ -224,10 +258,12 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() { startForeground(decryptedPushMessage!!.timestamp.toInt(), notification) } else { val messageData = Data.Builder() - .putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject) - .putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature) + .putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject) + .putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature) + .build() + val pushNotificationWork = + OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData) .build() - val pushNotificationWork = OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData).build() WorkManager.getInstance().enqueue(pushNotificationWork) } } @@ -244,47 +280,59 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() { } } - private fun checkIfCallIsActive(signatureVerification: SignatureVerification, decryptedPushMessage: DecryptedPushMessage) { - val ncApi = retrofit!!.newBuilder().client(okHttpClient!!.newBuilder().cookieJar(JavaNetCookieJar(CookieManager())).build()).build().create(NcApi::class.java) + private fun checkIfCallIsActive( + signatureVerification: SignatureVerification, + decryptedPushMessage: DecryptedPushMessage + ) { + val ncApi = retrofit!!.newBuilder() + .client(okHttpClient!!.newBuilder().cookieJar(JavaNetCookieJar(CookieManager())).build()).build() + .create(NcApi::class.java) var hasParticipantsInCall = false var inCallOnDifferentDevice = false - ncApi.getPeersForCall(ApiUtils.getCredentials(signatureVerification.userEntity.username, signatureVerification.userEntity.token), - ApiUtils.getUrlForCall(signatureVerification.userEntity.baseUrl, - decryptedPushMessage.id)) - .takeWhile { - isServiceInForeground + ncApi.getPeersForCall( + ApiUtils.getCredentials(signatureVerification.userEntity.username, signatureVerification.userEntity.token), + ApiUtils.getUrlForCall( + signatureVerification.userEntity.baseUrl, + decryptedPushMessage.id + ) + ) + .takeWhile { + isServiceInForeground + } + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { } - .subscribeOn(Schedulers.io()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } - override fun onNext(participantsOverall: ParticipantsOverall) { - val participantList: List = participantsOverall.ocs.data - hasParticipantsInCall = participantList.isNotEmpty() - if (!hasParticipantsInCall) { - for (participant in participantList) { - if (participant.userId == signatureVerification.userEntity.userId) { - inCallOnDifferentDevice = true - break - } + override fun onNext(participantsOverall: ParticipantsOverall) { + val participantList: List = participantsOverall.ocs.data + hasParticipantsInCall = participantList.isNotEmpty() + if (!hasParticipantsInCall) { + for (participant in participantList) { + if (participant.userId == signatureVerification.userEntity.userId) { + inCallOnDifferentDevice = true + break } } + } - if (!hasParticipantsInCall || inCallOnDifferentDevice) { - stopForeground(true) - handler.removeCallbacksAndMessages(null) - } else if (isServiceInForeground) { - handler.postDelayed({ + if (!hasParticipantsInCall || inCallOnDifferentDevice) { + stopForeground(true) + handler.removeCallbacksAndMessages(null) + } else if (isServiceInForeground) { + handler.postDelayed( + { checkIfCallIsActive(signatureVerification, decryptedPushMessage) - }, 5000) - } + }, + 5000 + ) } + } - override fun onError(e: Throwable) {} - override fun onComplete() { - } - }) + override fun onError(e: Throwable) {} + override fun onComplete() { + } + }) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt index f55f66caa..7a36c608a 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt @@ -76,8 +76,11 @@ open class BaseActivity : AppCompatActivity() { } } - fun showCertificateDialog(cert: X509Certificate, magicTrustManager: MagicTrustManager, - sslErrorHandler: SslErrorHandler?) { + fun showCertificateDialog( + cert: X509Certificate, + magicTrustManager: MagicTrustManager, + sslErrorHandler: SslErrorHandler? + ) { val formatter = DateFormat.getDateInstance(DateFormat.LONG) val validFrom = formatter.format(cert.notBefore) val validUntil = formatter.format(cert.notAfter) @@ -101,30 +104,30 @@ open class BaseActivity : AppCompatActivity() { issuedFor = cert.subjectDN.name } - @SuppressLint("StringFormatMatches") val dialogText = String.format(resources + @SuppressLint("StringFormatMatches") val dialogText = String.format( + resources .getString(R.string.nc_certificate_dialog_text), - issuedBy, issuedFor, validFrom, validUntil) + issuedBy, issuedFor, validFrom, validUntil + ) LovelyStandardDialog(this) - .setTopColorRes(R.color.nc_darkRed) - .setNegativeButtonColorRes(R.color.nc_darkRed) - .setPositiveButtonColorRes(R.color.colorPrimary) - .setIcon(R.drawable.ic_security_white_24dp) - .setTitle(R.string.nc_certificate_dialog_title) - .setMessage(dialogText) - .setPositiveButton(R.string.nc_yes) { v -> - magicTrustManager.addCertInTrustStore(cert) - sslErrorHandler?.proceed() - } - .setNegativeButton(R.string.nc_no) { view1 -> - sslErrorHandler?.cancel() - } - .show() - + .setTopColorRes(R.color.nc_darkRed) + .setNegativeButtonColorRes(R.color.nc_darkRed) + .setPositiveButtonColorRes(R.color.colorPrimary) + .setIcon(R.drawable.ic_security_white_24dp) + .setTitle(R.string.nc_certificate_dialog_title) + .setMessage(dialogText) + .setPositiveButton(R.string.nc_yes) { v -> + magicTrustManager.addCertInTrustStore(cert) + sslErrorHandler?.proceed() + } + .setNegativeButton(R.string.nc_no) { view1 -> + sslErrorHandler?.cancel() + } + .show() } catch (e: CertificateParsingException) { Log.d(TAG, "Failed to parse the certificate") } - } @Subscribe(threadMode = ThreadMode.MAIN) diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt index efb703f70..61ebf843b 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt @@ -38,7 +38,6 @@ import pl.droidsonroids.gif.GifDrawable import pl.droidsonroids.gif.GifImageView import java.io.File - class FullScreenImageActivity : AppCompatActivity() { private lateinit var path: String @@ -55,9 +54,11 @@ class FullScreenImageActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { return if (item.itemId == R.id.share) { - val shareUri = FileProvider.getUriForFile(this, - BuildConfig.APPLICATION_ID, - File(path)) + val shareUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID, + File(path) + ) val shareIntent: Intent = Intent().apply { action = Intent.ACTION_SEND @@ -78,19 +79,19 @@ class FullScreenImageActivity : AppCompatActivity() { setContentView(R.layout.activity_full_screen_image) setSupportActionBar(findViewById(R.id.imageview_toolbar)) - supportActionBar?.setDisplayShowTitleEnabled(false); + supportActionBar?.setDisplayShowTitleEnabled(false) imageWrapperView = findViewById(R.id.image_wrapper_view) photoView = findViewById(R.id.photo_view) gifView = findViewById(R.id.gif_view) - photoView.setOnPhotoTapListener{ view, x, y -> + photoView.setOnPhotoTapListener { view, x, y -> toggleFullscreen() } - photoView.setOnOutsidePhotoTapListener{ + photoView.setOnOutsidePhotoTapListener { toggleFullscreen() } - gifView.setOnClickListener{ + gifView.setOnClickListener { toggleFullscreen() } @@ -115,29 +116,33 @@ class FullScreenImageActivity : AppCompatActivity() { } } - private fun toggleFullscreen(){ - showFullscreen = !showFullscreen; - if (showFullscreen){ + private fun toggleFullscreen() { + showFullscreen = !showFullscreen + if (showFullscreen) { hideSystemUI() supportActionBar?.hide() - } else{ + } else { showSystemUI() supportActionBar?.show() } } private fun hideSystemUI() { - window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE + window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_IMMERSIVE or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_FULLSCREEN) + or View.SYSTEM_UI_FLAG_FULLSCREEN + ) } private fun showSystemUI() { - window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt index 640f581e6..d5a689624 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt @@ -22,7 +22,6 @@ package com.nextcloud.talk.activities import android.content.Intent import android.os.Bundle -import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View @@ -33,7 +32,6 @@ import autodagger.AutoInjector import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.SimpleExoPlayer -import com.google.android.exoplayer2.ui.PlayerControlView import com.google.android.exoplayer2.ui.StyledPlayerView import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R @@ -54,9 +52,11 @@ class FullScreenMediaActivity : AppCompatActivity(), Player.EventListener { override fun onOptionsItemSelected(item: MenuItem): Boolean { return if (item.itemId == R.id.share) { - val shareUri = FileProvider.getUriForFile(this, - BuildConfig.APPLICATION_ID, - File(path)) + val shareUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID, + File(path) + ) val shareIntent: Intent = Intent().apply { action = Intent.ACTION_SEND @@ -82,11 +82,11 @@ class FullScreenMediaActivity : AppCompatActivity(), Player.EventListener { setContentView(R.layout.activity_full_screen_media) setSupportActionBar(findViewById(R.id.mediaview_toolbar)) - supportActionBar?.setDisplayShowTitleEnabled(false); + supportActionBar?.setDisplayShowTitleEnabled(false) playerView = findViewById(R.id.player_view) - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) playerView.showController() if (isAudioOnly) { @@ -121,7 +121,7 @@ class FullScreenMediaActivity : AppCompatActivity(), Player.EventListener { private fun initializePlayer() { player = SimpleExoPlayer.Builder(applicationContext).build() - playerView.player = player; + playerView.player = player player.playWhenReady = true player.addListener(this) } @@ -131,17 +131,21 @@ class FullScreenMediaActivity : AppCompatActivity(), Player.EventListener { } private fun hideSystemUI() { - window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE + window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_IMMERSIVE or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_FULLSCREEN) + or View.SYSTEM_UI_FLAG_FULLSCREEN + ) } private fun showSystemUI() { - window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + ) } } diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt index c550c49dc..867fb9b39 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt @@ -34,7 +34,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import io.noties.markwon.Markwon import java.io.File - @AutoInjector(NextcloudTalkApplication::class) class FullScreenTextViewerActivity : AppCompatActivity() { @@ -48,9 +47,11 @@ class FullScreenTextViewerActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { return if (item.itemId == R.id.share) { - val shareUri = FileProvider.getUriForFile(this, - BuildConfig.APPLICATION_ID, - File(path)) + val shareUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID, + File(path) + ) val shareIntent: Intent = Intent().apply { action = Intent.ACTION_SEND @@ -71,8 +72,7 @@ class FullScreenTextViewerActivity : AppCompatActivity() { setContentView(R.layout.activity_full_screen_text) setSupportActionBar(findViewById(R.id.textview_toolbar)) - supportActionBar?.setDisplayShowTitleEnabled(false); - + supportActionBar?.setDisplayShowTitleEnabled(false) textView = findViewById(R.id.text_view) val fileName = intent.getStringExtra("FILE_NAME") @@ -81,13 +81,12 @@ class FullScreenTextViewerActivity : AppCompatActivity() { var text = readFile(path) if (isMarkdown) { - val markwon = Markwon.create(applicationContext); - markwon.setMarkdown(textView, text); + val markwon = Markwon.create(applicationContext) + markwon.setMarkdown(textView, text) } else { textView.text = text } } private fun readFile(fileName: String) = File(fileName).inputStream().readBytes().toString(Charsets.UTF_8) - } diff --git a/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt index 5421d24bd..3a0ee85da 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt @@ -48,7 +48,7 @@ class MagicCallActivity : BaseActivity() { @BindView(R.id.controller_container) lateinit var container: ViewGroup - + @BindView(R.id.chatControllerView) lateinit var chatContainer: ViewGroup @@ -60,10 +60,12 @@ class MagicCallActivity : BaseActivity() { NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) requestWindowFeature(Window.FEATURE_NO_TITLE) - window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN or + window.addFlags( + WindowManager.LayoutParams.FLAG_FULLSCREEN or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) + WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + ) window.decorView.systemUiVisibility = systemUiVisibility setContentView(R.layout.activity_magic_call) @@ -74,26 +76,32 @@ class MagicCallActivity : BaseActivity() { if (!router!!.hasRootController()) { if (intent.getBooleanExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false)) { - router!!.setRoot(RouterTransaction.with(CallNotificationController(intent.extras)) + router!!.setRoot( + RouterTransaction.with(CallNotificationController(intent.extras)) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) + .popChangeHandler(HorizontalChangeHandler()) + ) } else { - router!!.setRoot(RouterTransaction.with(CallController(intent.extras)) + router!!.setRoot( + RouterTransaction.with(CallController(intent.extras)) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) + .popChangeHandler(HorizontalChangeHandler()) + ) } } val extras = intent.extras ?: Bundle() extras.putBoolean("showToggleChat", true) - + chatController = ChatController(extras) chatRouter = Conductor.attachRouter(this, chatContainer, savedInstanceState) - chatRouter!!.setRoot(RouterTransaction.with(chatController) + chatRouter!!.setRoot( + RouterTransaction.with(chatController) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) + .popChangeHandler(HorizontalChangeHandler()) + ) } - + fun showChat() { chatContainer.visibility = View.VISIBLE container.visibility = View.GONE diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index 430eca8a8..897ebbb15 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -79,23 +79,31 @@ class MainActivity : BaseActivity(), ActionBarProvider { @BindView(R.id.appBar) lateinit var appBar: AppBarLayout + @BindView(R.id.toolbar) lateinit var toolbar: MaterialToolbar + @BindView(R.id.home_toolbar) lateinit var searchCardView: MaterialCardView + @BindView(R.id.search_text) lateinit var searchInputText: MaterialTextView + @BindView(R.id.switch_account_button) lateinit var settingsButton: MaterialButton + @BindView(R.id.controller_container) lateinit var container: ViewGroup @Inject lateinit var userUtils: UserUtils + @Inject lateinit var dataStore: ReactiveEntityStore + @Inject lateinit var sqlCipherDatabaseSource: SqlCipherDatabaseSource + @Inject lateinit var ncApi: NcApi @@ -124,39 +132,53 @@ class MainActivity : BaseActivity(), ActionBarProvider { if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { if (!router!!.hasRootController()) { - router!!.setRoot(RouterTransaction.with(ConversationsListController()) + router!!.setRoot( + RouterTransaction.with(ConversationsListController()) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) + .popChangeHandler(HorizontalChangeHandler()) + ) } onNewIntent(intent) } else if (!router!!.hasRootController()) { if (hasDb) { if (userUtils.anyUserExists()) { - router!!.setRoot(RouterTransaction.with(ConversationsListController()) + router!!.setRoot( + RouterTransaction.with(ConversationsListController()) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) + .popChangeHandler(HorizontalChangeHandler()) + ) } else { if (!TextUtils.isEmpty(resources.getString(R.string.weblogin_url))) { - router!!.pushController(RouterTransaction.with( - WebViewLoginController(resources.getString(R.string.weblogin_url), false)) + router!!.pushController( + RouterTransaction.with( + WebViewLoginController(resources.getString(R.string.weblogin_url), false) + ) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) + .popChangeHandler(HorizontalChangeHandler()) + ) } else { - router!!.setRoot(RouterTransaction.with(ServerSelectionController()) + router!!.setRoot( + RouterTransaction.with(ServerSelectionController()) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) + .popChangeHandler(HorizontalChangeHandler()) + ) } } } else { if (!TextUtils.isEmpty(resources.getString(R.string.weblogin_url))) { - router!!.pushController(RouterTransaction.with( - WebViewLoginController(resources.getString(R.string.weblogin_url), false)) + router!!.pushController( + RouterTransaction.with( + WebViewLoginController(resources.getString(R.string.weblogin_url), false) + ) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) + .popChangeHandler(HorizontalChangeHandler()) + ) } else { - router!!.setRoot(RouterTransaction.with(ServerSelectionController()) + router!!.setRoot( + RouterTransaction.with(ServerSelectionController()) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) + .popChangeHandler(HorizontalChangeHandler()) + ) } } } @@ -167,7 +189,7 @@ class MainActivity : BaseActivity(), ActionBarProvider { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { checkIfWeAreSecure() } - + handleActionFromContact(intent) } @@ -182,7 +204,7 @@ class MainActivity : BaseActivity(), ActionBarProvider { // userId @ server userId = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DATA1)) } - + cursor.close() } @@ -193,65 +215,82 @@ class MainActivity : BaseActivity(), ActionBarProvider { if (userUtils.currentUser?.baseUrl?.endsWith(baseUrl) == true) { startConversation(user) } else { - Snackbar.make(container, R.string.nc_phone_book_integration_account_not_found, Snackbar - .LENGTH_LONG).show() + Snackbar.make( + container, R.string.nc_phone_book_integration_account_not_found, + Snackbar.LENGTH_LONG + ).show() } } } } } - + private fun startConversation(userId: String) { val roomType = "1" val currentUser = userUtils.currentUser ?: return val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) - val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(currentUser.baseUrl, roomType, - userId, null) - ncApi.createRoom(credentials, - retrofitBucket.url, retrofitBucket.queryMap) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) {} - override fun onNext(roomOverall: RoomOverall) { - val conversationIntent = Intent(context, MagicCallActivity::class.java) - val bundle = Bundle() - bundle.putParcelable(KEY_USER_ENTITY, currentUser) - bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs.data.token) - bundle.putString(KEY_ROOM_ID, roomOverall.ocs.data.roomId) - if (currentUser.hasSpreedFeatureCapability("chat-v2")) { - ncApi.getRoom(credentials, - ApiUtils.getRoom(currentUser.baseUrl, - roomOverall.ocs.data.token)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) {} - override fun onNext(roomOverall: RoomOverall) { - bundle.putParcelable(KEY_ACTIVE_CONVERSATION, - Parcels.wrap(roomOverall.ocs.data)) - remapChatController(router!!, currentUser.id, - roomOverall.ocs.data.token, bundle, true) - } + val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + currentUser.baseUrl, roomType, + userId, null + ) + ncApi.createRoom( + credentials, + retrofitBucket.url, retrofitBucket.queryMap + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + override fun onNext(roomOverall: RoomOverall) { + val conversationIntent = Intent(context, MagicCallActivity::class.java) + val bundle = Bundle() + bundle.putParcelable(KEY_USER_ENTITY, currentUser) + bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs.data.token) + bundle.putString(KEY_ROOM_ID, roomOverall.ocs.data.roomId) + if (currentUser.hasSpreedFeatureCapability("chat-v2")) { + ncApi.getRoom( + credentials, + ApiUtils.getRoom( + currentUser.baseUrl, + roomOverall.ocs.data.token + ) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + override fun onNext(roomOverall: RoomOverall) { + bundle.putParcelable( + KEY_ACTIVE_CONVERSATION, + Parcels.wrap(roomOverall.ocs.data) + ) + remapChatController( + router!!, currentUser.id, + roomOverall.ocs.data.token, bundle, true + ) + } - override fun onError(e: Throwable) {} - override fun onComplete() {} - }) - } else { - conversationIntent.putExtras(bundle) - startActivity(conversationIntent) - Handler().postDelayed({ + override fun onError(e: Throwable) {} + override fun onComplete() {} + }) + } else { + conversationIntent.putExtras(bundle) + startActivity(conversationIntent) + Handler().postDelayed( + { if (!isDestroyed) { router!!.popCurrentController() } - }, 100) - } + }, + 100 + ) } + } - override fun onError(e: Throwable) {} - override fun onComplete() {} - }) + override fun onError(e: Throwable) {} + override fun onComplete() {} + }) } @RequiresApi(api = Build.VERSION_CODES.M) @@ -260,16 +299,17 @@ class MainActivity : BaseActivity(), ActionBarProvider { if (keyguardManager.isKeyguardSecure && appPreferences.isScreenLocked) { if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout)) { if (router != null && router!!.getControllerWithTag(LockedController.TAG) == null) { - router!!.pushController(RouterTransaction.with(LockedController()) + router!!.pushController( + RouterTransaction.with(LockedController()) .pushChangeHandler(VerticalChangeHandler()) .popChangeHandler(VerticalChangeHandler()) - .tag(LockedController.TAG)) + .tag(LockedController.TAG) + ) } } } } - override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) @@ -277,12 +317,16 @@ class MainActivity : BaseActivity(), ActionBarProvider { if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { if (intent.getBooleanExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false)) { - router!!.pushController(RouterTransaction.with(CallNotificationController(intent.extras)) + router!!.pushController( + RouterTransaction.with(CallNotificationController(intent.extras)) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) + .popChangeHandler(HorizontalChangeHandler()) + ) } else { - ConductorRemapping.remapChatController(router!!, intent.getLongExtra(BundleKeys.KEY_INTERNAL_USER_ID, -1), - intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN), intent.extras!!, false) + ConductorRemapping.remapChatController( + router!!, intent.getLongExtra(BundleKeys.KEY_INTERNAL_USER_ID, -1), + intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN), intent.extras!!, false + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt index aa8392f89..1de435c3a 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt @@ -39,7 +39,6 @@ import autodagger.AutoInjector import butterknife.BindView import butterknife.ButterKnife import coil.load -import coil.transform.CircleCropTransformation import com.amulyakhare.textdrawable.TextDrawable import com.facebook.drawee.view.SimpleDraweeView import com.nextcloud.talk.R @@ -51,7 +50,6 @@ import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.TextMatchers import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.messages.MessageHolders -import com.stfalcon.chatkit.utils.DateFormatter import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -104,8 +102,8 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders init { ButterKnife.bind( - this, - itemView + this, + itemView ) } @@ -131,13 +129,13 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders messageUserAvatarView?.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable)) } else if (message.actorType == "bots") { val drawable = TextDrawable.builder() - .beginConfig() - .bold() - .endConfig() - .buildRound( - ">", - context!!.resources.getColor(R.color.black) - ) + .beginConfig() + .bold() + .endConfig() + .buildRound( + ">", + context!!.resources.getColor(R.color.black) + ) messageUserAvatarView!!.visibility = View.VISIBLE messageUserAvatarView?.setImageDrawable(drawable) } @@ -165,9 +163,9 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders } val bubbleDrawable = DisplayUtils.getMessageSelector( - bgBubbleColor, - resources.getColor(R.color.transparent), - bgBubbleColor, bubbleResource + bgBubbleColor, + resources.getColor(R.color.transparent), + bgBubbleColor, bubbleResource ) ViewCompat.setBackground(bubble, bubbleDrawable) @@ -187,23 +185,23 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders if (individualHashMap["type"] == "user" || individualHashMap["type"] == "guest" || individualHashMap["type"] == "call") { if (individualHashMap["id"] == message.activeUser!!.userId) { messageString = DisplayUtils.searchAndReplaceWithMentionSpan( - messageText!!.context, - messageString, - individualHashMap["id"]!!, - individualHashMap["name"]!!, - individualHashMap["type"]!!, - message.activeUser!!, - R.xml.chip_you + messageText!!.context, + messageString, + individualHashMap["id"]!!, + individualHashMap["name"]!!, + individualHashMap["type"]!!, + message.activeUser!!, + R.xml.chip_you ) } else { messageString = DisplayUtils.searchAndReplaceWithMentionSpan( - messageText!!.context, - messageString, - individualHashMap["id"]!!, - individualHashMap["name"]!!, - individualHashMap["type"]!!, - message.activeUser!!, - R.xml.chip_others + messageText!!.context, + messageString, + individualHashMap["id"]!!, + individualHashMap["name"]!!, + individualHashMap["type"]!!, + message.activeUser!!, + R.xml.chip_others ) } } else if (individualHashMap["type"] == "file") { @@ -231,18 +229,21 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders parentChatMessage.imageUrl?.let { quotedMessagePreview?.visibility = View.VISIBLE quotedMessagePreview?.load(it) { - addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)) + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token) + ) } } ?: run { quotedMessagePreview?.visibility = View.GONE } quotedUserName?.text = parentChatMessage.actorDisplayName - ?: context!!.getText(R.string.nc_nick_guest) + ?: context!!.getText(R.string.nc_nick_guest) quotedMessage?.text = parentChatMessage.text quotedUserName?.setTextColor(context!!.resources.getColor(R.color.textColorMaxContrast)) - if(parentChatMessage.actorId?.equals(message.activeUser.userId) == true) { + if (parentChatMessage.actorId?.equals(message.activeUser.userId) == true) { quoteColoredView?.setBackgroundResource(R.color.colorPrimary) } else { quoteColoredView?.setBackgroundResource(R.color.textColorMaxContrast) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt index ab03b7e69..84a22d33b 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt @@ -36,7 +36,6 @@ import autodagger.AutoInjector import butterknife.BindView import butterknife.ButterKnife import coil.load -import coil.transform.CircleCropTransformation import com.google.android.flexbox.FlexboxLayout import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication @@ -48,8 +47,7 @@ import com.nextcloud.talk.utils.DisplayUtils.getMessageSelector import com.nextcloud.talk.utils.DisplayUtils.searchAndReplaceWithMentionSpan import com.nextcloud.talk.utils.TextMatchers import com.stfalcon.chatkit.messages.MessageHolders.OutcomingTextMessageViewHolder -import com.stfalcon.chatkit.utils.DateFormatter -import java.util.* +import java.util.HashMap import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -57,6 +55,7 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage @JvmField @BindView(R.id.messageText) var messageText: EmojiTextView? = null + @JvmField @BindView(R.id.messageTime) var messageTimeView: TextView? = null @@ -104,20 +103,26 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage for (key in messageParameters.keys) { val individualHashMap: HashMap? = message.messageParameters[key] if (individualHashMap != null) { - if (individualHashMap["type"] == "user" || (individualHashMap["type"] - == "guest") || individualHashMap["type"] == "call") { - messageString = searchAndReplaceWithMentionSpan(messageText!!.context, - messageString, - individualHashMap["id"]!!, - individualHashMap["name"]!!, - individualHashMap["type"]!!, - message.activeUser, - R.xml.chip_others) + if (individualHashMap["type"] == "user" || ( + individualHashMap["type"] == "guest" + ) || individualHashMap["type"] == "call" + ) { + messageString = searchAndReplaceWithMentionSpan( + messageText!!.context, + messageString, + individualHashMap["id"]!!, + individualHashMap["name"]!!, + individualHashMap["type"]!!, + message.activeUser, + R.xml.chip_others + ) } else if (individualHashMap["type"] == "file") { - realView.setOnClickListener(View.OnClickListener { v: View? -> - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap["link"])) - context!!.startActivity(browserIntent) - }) + realView.setOnClickListener( + View.OnClickListener { v: View? -> + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap["link"])) + context!!.startActivity(browserIntent) + } + ) } } } @@ -135,17 +140,19 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage } if (message.isGrouped) { val bubbleDrawable = getMessageSelector( - bgBubbleColor, - resources.getColor(R.color.transparent), - bgBubbleColor, - R.drawable.shape_grouped_outcoming_message) + bgBubbleColor, + resources.getColor(R.color.transparent), + bgBubbleColor, + R.drawable.shape_grouped_outcoming_message + ) ViewCompat.setBackground(bubble, bubbleDrawable) } else { val bubbleDrawable = getMessageSelector( - bgBubbleColor, - resources.getColor(R.color.transparent), - bgBubbleColor, - R.drawable.shape_outcoming_message) + bgBubbleColor, + resources.getColor(R.color.transparent), + bgBubbleColor, + R.drawable.shape_outcoming_message + ) ViewCompat.setBackground(bubble, bubbleDrawable) } messageText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) @@ -160,13 +167,16 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage parentChatMessage.imageUrl?.let { quotedMessagePreview?.visibility = View.VISIBLE quotedMessagePreview?.load(it) { - addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)) + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token) + ) } } ?: run { quotedMessagePreview?.visibility = View.GONE } quotedUserName?.text = parentChatMessage.actorDisplayName - ?: context!!.getText(R.string.nc_nick_guest) + ?: context!!.getText(R.string.nc_nick_guest) quotedMessage?.text = parentChatMessage.text quotedMessage?.setTextColor(context!!.resources.getColor(R.color.nc_outcoming_text_default)) quotedUserName?.setTextColor(context!!.resources.getColor(R.color.nc_grey)) diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt index ce8e1f912..3ecad94cf 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt @@ -90,6 +90,7 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver { @Inject lateinit var appPreferences: AppPreferences + @Inject lateinit var okHttpClient: OkHttpClient //endregion @@ -105,8 +106,10 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver { WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true) } - PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(this) - .createInitializationOptions()) + PeerConnectionFactory.initialize( + PeerConnectionFactory.InitializationOptions.builder(this) + .createInitializationOptions() + ) } catch (e: UnsatisfiedLinkError) { Log.w(TAG, e) } @@ -120,8 +123,8 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver { val securityKeyManager = SecurityKeyManager.getInstance() val securityKeyConfig = SecurityKeyManagerConfig.Builder() - .setEnableDebugLogging(BuildConfig.DEBUG) - .build() + .setEnableDebugLogging(BuildConfig.DEBUG) + .build() securityKeyManager.init(this, securityKeyConfig) initializeWebRtc() @@ -136,13 +139,15 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver { super.onCreate() val imagePipelineConfig = ImagePipelineConfig.newBuilder(this) - .setNetworkFetcher(OkHttpNetworkFetcherWithCache(okHttpClient)) - .setMainDiskCacheConfig(DiskCacheConfig.newBuilder(this) - .setMaxCacheSize(0) - .setMaxCacheSizeOnLowDiskSpace(0) - .setMaxCacheSizeOnVeryLowDiskSpace(0) - .build()) - .build() + .setNetworkFetcher(OkHttpNetworkFetcherWithCache(okHttpClient)) + .setMainDiskCacheConfig( + DiskCacheConfig.newBuilder(this) + .setMaxCacheSize(0) + .setMaxCacheSizeOnLowDiskSpace(0) + .setMaxCacheSizeOnVeryLowDiskSpace(0) + .build() + ) + .build() Fresco.initialize(this, imagePipelineConfig) Security.insertProviderAt(Conscrypt.newProvider(), 1) @@ -152,8 +157,10 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver { val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java).build() val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() - val periodicCapabilitiesUpdateWork = PeriodicWorkRequest.Builder(CapabilitiesWorker::class.java, - 12, TimeUnit.HOURS).build() + val periodicCapabilitiesUpdateWork = PeriodicWorkRequest.Builder( + CapabilitiesWorker::class.java, + 12, TimeUnit.HOURS + ).build() val capabilitiesUpdateWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java).build() val signalingSettingsWork = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java).build() @@ -161,7 +168,11 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver { WorkManager.getInstance().enqueue(accountRemovalWork) WorkManager.getInstance().enqueue(capabilitiesUpdateWork) WorkManager.getInstance().enqueue(signalingSettingsWork) - WorkManager.getInstance().enqueueUniquePeriodicWork("DailyCapabilitiesUpdateWork", ExistingPeriodicWorkPolicy.REPLACE, periodicCapabilitiesUpdateWork) + WorkManager.getInstance().enqueueUniquePeriodicWork( + "DailyCapabilitiesUpdateWork", + ExistingPeriodicWorkPolicy.REPLACE, + periodicCapabilitiesUpdateWork + ) val config = BundledEmojiCompatConfig(this) config.setReplaceAll(true) @@ -176,17 +187,16 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver { } //endregion - //region Protected methods protected fun buildComponent() { componentApplication = DaggerNextcloudTalkApplicationComponent.builder() - .busModule(BusModule()) - .contextModule(ContextModule(applicationContext)) - .databaseModule(DatabaseModule()) - .restModule(RestModule(applicationContext)) - .userModule(UserModule()) - .arbitraryStorageModule(ArbitraryStorageModule()) - .build() + .busModule(BusModule()) + .contextModule(ContextModule(applicationContext)) + .databaseModule(DatabaseModule()) + .restModule(RestModule(applicationContext)) + .userModule(UserModule()) + .arbitraryStorageModule(ArbitraryStorageModule()) + .build() } override fun attachBaseContext(base: Context) { @@ -196,19 +206,20 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver { private fun buildDefaultImageLoader(): ImageLoader { return ImageLoader.Builder(applicationContext) - .availableMemoryPercentage(0.5) // Use 50% of the application's available memory. - .crossfade(true) // Show a short crossfade when loading images from network or disk into an ImageView. - .componentRegistry { - if (SDK_INT >= P) { - add(ImageDecoderDecoder(applicationContext)) - } else { - add(GifDecoder()) - } - add(SvgDecoder(applicationContext)) + .availableMemoryPercentage(0.5) // Use 50% of the application's available memory. + .crossfade(true) // Show a short crossfade when loading images from network or disk into an ImageView. + .componentRegistry { + if (SDK_INT >= P) { + add(ImageDecoderDecoder(applicationContext)) + } else { + add(GifDecoder()) } - .okHttpClient(okHttpClient) - .build() + add(SvgDecoder(applicationContext)) + } + .okHttpClient(okHttpClient) + .build() } + companion object { private val TAG = NextcloudTalkApplication::class.java.simpleName //region Singleton diff --git a/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java b/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java index 7fbb99d02..64b8e77ba 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java @@ -44,10 +44,6 @@ import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.constraintlayout.widget.ConstraintLayout; - import com.bluelinelabs.conductor.RouterTransaction; import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; import com.bluelinelabs.logansquare.LoganSquare; @@ -71,7 +67,6 @@ import com.nextcloud.talk.models.RingtoneSettings; import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.models.json.conversations.Conversation; import com.nextcloud.talk.models.json.conversations.RoomOverall; -import com.nextcloud.talk.models.json.conversations.RoomsOverall; import com.nextcloud.talk.models.json.participants.Participant; import com.nextcloud.talk.models.json.participants.ParticipantsOverall; import com.nextcloud.talk.utils.ApiUtils; @@ -94,6 +89,9 @@ import java.util.List; import javax.inject.Inject; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; import autodagger.AutoInjector; import butterknife.BindView; import butterknife.OnClick; @@ -206,13 +204,13 @@ public class CallNotificationController extends BaseController { originalBundle.putString(BundleKeys.INSTANCE.getKEY_CONVERSATION_NAME(), currentConversation.getDisplayName()); getRouter().replaceTopController(RouterTransaction.with(new CallController(originalBundle)) - .popChangeHandler(new HorizontalChangeHandler()) - .pushChangeHandler(new HorizontalChangeHandler())); + .popChangeHandler(new HorizontalChangeHandler()) + .pushChangeHandler(new HorizontalChangeHandler())); } private void checkIfAnyParticipantsRemainInRoom() { ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(userBeingCalled.getBaseUrl(), - currentConversation.getToken())) + currentConversation.getToken())) .subscribeOn(Schedulers.io()) .takeWhile(observable -> !leavingScreen) .subscribe(new Observer() { @@ -261,7 +259,7 @@ public class CallNotificationController extends BaseController { private void handleFromNotification() { boolean isConversationApiV3 = userBeingCalled.hasSpreedFeatureCapability("conversation-v3"); - if(isConversationApiV3) { + if (isConversationApiV3) { ncApi.getRoom(credentials, ApiUtils.getRoomV3(userBeingCalled.getBaseUrl(), roomId)) .subscribeOn(Schedulers.io()) .retry(3) @@ -279,12 +277,12 @@ public class CallNotificationController extends BaseController { boolean hasCallFlags = userBeingCalled.hasSpreedFeatureCapability("conversation-call-flags"); if (hasCallFlags) { - if (isInCallWithVideo(currentConversation.callFlag)){ + if (isInCallWithVideo(currentConversation.callFlag)) { incomingCallVoiceOrVideoTextView.setText(String.format(getResources().getString(R.string.nc_call_video), - getResources().getString(R.string.nc_app_name))); + getResources().getString(R.string.nc_app_name))); } else { incomingCallVoiceOrVideoTextView.setText(String.format(getResources().getString(R.string.nc_call_voice), - getResources().getString(R.string.nc_app_name))); + getResources().getString(R.string.nc_app_name))); } } } @@ -412,7 +410,7 @@ public class CallNotificationController extends BaseController { ImageRequest imageRequest = DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userBeingCalled.getBaseUrl(), - currentConversation.getName(), R.dimen.avatar_size_very_big), null); + currentConversation.getName(), R.dimen.avatar_size_very_big), null); ImagePipeline imagePipeline = Fresco.getImagePipeline(); DataSource> dataSource = imagePipeline.fetchDecodedImage(imageRequest, null); @@ -422,11 +420,11 @@ public class CallNotificationController extends BaseController { protected void onNewResultImpl(@Nullable Bitmap bitmap) { if (avatarImageView != null) { avatarImageView.getHierarchy().setImage(new BitmapDrawable(bitmap), 100, - true); + true); if (getResources() != null) { incomingTextRelativeLayout.setBackground(getResources().getDrawable(R.drawable - .incoming_gradient)); + .incoming_gradient)); } if ((AvatarStatusCodeHolder.getInstance().getStatusCode() == 200 || AvatarStatusCodeHolder.getInstance().getStatusCode() == 0) && @@ -512,7 +510,7 @@ public class CallNotificationController extends BaseController { if (TextUtils.isEmpty(callRingtonePreferenceString)) { // play default sound ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() + - "/raw/librem_by_feandesign_call"); + "/raw/librem_by_feandesign_call"); } else { try { RingtoneSettings ringtoneSettings = LoganSquare.parse(callRingtonePreferenceString, RingtoneSettings.class); @@ -520,7 +518,7 @@ public class CallNotificationController extends BaseController { } catch (IOException e) { Log.e(TAG, "Failed to parse ringtone settings"); ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() + - "/raw/librem_by_feandesign_call"); + "/raw/librem_by_feandesign_call"); } } @@ -531,7 +529,7 @@ public class CallNotificationController extends BaseController { mediaPlayer.setLooping(true); AudioAttributes audioAttributes = new AudioAttributes.Builder().setContentType(AudioAttributes - .CONTENT_TYPE_SONIFICATION).setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build(); + .CONTENT_TYPE_SONIFICATION).setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build(); mediaPlayer.setAudioAttributes(audioAttributes); mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start()); diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index 15427beec..7facc123a 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -40,8 +40,22 @@ import android.text.TextUtils import android.text.TextWatcher import android.util.Log import android.util.TypedValue -import android.view.* -import android.widget.* +import android.view.Gravity +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.AbsListView +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.PopupMenu +import android.widget.ProgressBar +import android.widget.RelativeLayout +import android.widget.Space +import android.widget.TextView +import android.widget.Toast import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.emoji.text.EmojiCompat import androidx.emoji.widget.EmojiEditText @@ -55,7 +69,6 @@ import autodagger.AutoInjector import butterknife.BindView import butterknife.OnClick import coil.load -import coil.transform.CircleCropTransformation import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler @@ -69,7 +82,12 @@ import com.facebook.imagepipeline.image.CloseableImage import com.google.android.flexbox.FlexboxLayout import com.nextcloud.talk.R import com.nextcloud.talk.activities.MagicCallActivity -import com.nextcloud.talk.adapters.messages.* +import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder +import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder +import com.nextcloud.talk.adapters.messages.MagicPreviewMessageViewHolder +import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder +import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder +import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.callbacks.MentionAutocompleteCallback @@ -91,7 +109,14 @@ import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.mention.Mention import com.nextcloud.talk.presenters.MentionAutocompletePresenter import com.nextcloud.talk.ui.dialog.AttachmentDialog -import com.nextcloud.talk.utils.* +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ConductorRemapping +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.KeyboardUtils +import com.nextcloud.talk.utils.MagicCharPolicy +import com.nextcloud.talk.utils.NotificationUtils +import com.nextcloud.talk.utils.UriUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.database.user.UserUtils import com.nextcloud.talk.utils.preferences.AppPreferences @@ -121,14 +146,20 @@ import org.parceler.Parcels import retrofit2.HttpException import retrofit2.Response import java.net.HttpURLConnection -import java.util.* +import java.util.ArrayList +import java.util.Date +import java.util.HashMap +import java.util.Objects import java.util.concurrent.TimeUnit import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) -class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter -.OnLoadMoreListener, MessagesListAdapter.Formatter, MessagesListAdapter -.OnMessageViewLongClickListener, MessageHolders.ContentChecker { +class ChatController(args: Bundle) : + BaseController(args), + MessagesListAdapter.OnLoadMoreListener, + MessagesListAdapter.Formatter, + MessagesListAdapter.OnMessageViewLongClickListener, + MessageHolders.ContentChecker { @Inject @JvmField @@ -236,7 +267,8 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter this.roomToken = args.getString(BundleKeys.KEY_ROOM_TOKEN, "") if (args.containsKey(BundleKeys.KEY_ACTIVE_CONVERSATION)) { - this.currentConversation = Parcels.unwrap(args.getParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION)) + this.currentConversation = + Parcels.unwrap(args.getParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION)) } this.roomPassword = args.getString(BundleKeys.KEY_CONVERSATION_PASSWORD, "") @@ -260,74 +292,71 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter checkingLobbyStatus = true } - if (conversationUser != null) { - ncApi?.getRoom(credentials, ApiUtils.getRoom(conversationUser.baseUrl, roomToken))?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - disposableList.add(d) + ncApi?.getRoom(credentials, ApiUtils.getRoom(conversationUser.baseUrl, roomToken)) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableList.add(d) + } + + override fun onNext(roomOverall: RoomOverall) { + currentConversation = roomOverall.ocs.data + loadAvatarForStatusBar() + + setTitle() + setupMentionAutocomplete() + checkReadOnlyState() + checkLobbyState() + + if (!inConversation) { + joinRoomWithPassword() } + } - override fun onNext(roomOverall: RoomOverall) { - currentConversation = roomOverall.ocs.data - loadAvatarForStatusBar() + override fun onError(e: Throwable) { + } - setTitle() - setupMentionAutocomplete() - checkReadOnlyState() - checkLobbyState() - - if (!inConversation) { - joinRoomWithPassword() + override fun onComplete() { + if (shouldRepeat) { + if (lobbyTimerHandler == null) { + lobbyTimerHandler = Handler() } + lobbyTimerHandler?.postDelayed({ getRoomInfo() }, 5000) } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - if (shouldRepeat) { - if (lobbyTimerHandler == null) { - lobbyTimerHandler = Handler() - } - - lobbyTimerHandler?.postDelayed({ getRoomInfo() }, 5000) - } - } - }) + } + }) } } private fun handleFromNotification() { ncApi?.getRooms(credentials, ApiUtils.getUrlForGetRooms(conversationUser?.baseUrl)) - ?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - disposableList.add(d) - } + ?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableList.add(d) + } - override fun onNext(roomsOverall: RoomsOverall) { - for (conversation in roomsOverall.ocs.data) { - if (roomId == conversation.roomId) { - roomToken = conversation.token - currentConversation = conversation - setTitle() - getRoomInfo() - break - } + override fun onNext(roomsOverall: RoomsOverall) { + for (conversation in roomsOverall.ocs.data) { + if (roomId == conversation.roomId) { + roomToken = conversation.token + currentConversation = conversation + setTitle() + getRoomInfo() + break } } + } - override fun onError(e: Throwable) { + override fun onError(e: Throwable) { + } - } - - override fun onComplete() { - - } - }) + override fun onComplete() { + } + }) } override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { @@ -336,33 +365,44 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter private fun loadAvatarForStatusBar() { if (inOneToOneCall() && activity != null && conversationVoiceCallMenuItem != null) { - val avatarSize = DisplayUtils.convertDpToPixel(conversationVoiceCallMenuItem?.icon!! - .intrinsicWidth.toFloat(), activity).toInt() + val avatarSize = DisplayUtils.convertDpToPixel( + conversationVoiceCallMenuItem?.icon!! + .intrinsicWidth.toFloat(), + activity + ).toInt() - val imageRequest = DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithNameAndPixels(conversationUser?.baseUrl, - currentConversation?.name, avatarSize / 2), conversationUser!!) + val imageRequest = DisplayUtils.getImageRequestForUrl( + ApiUtils.getUrlForAvatarWithNameAndPixels( + conversationUser?.baseUrl, + currentConversation?.name, avatarSize / 2 + ), + conversationUser!! + ) val imagePipeline = Fresco.getImagePipeline() val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null) - dataSource.subscribe(object : BaseBitmapDataSubscriber() { - override fun onNewResultImpl(bitmap: Bitmap?) { - if (actionBar != null && bitmap != null && resources != null) { - val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(resources!!, bitmap) - roundedBitmapDrawable.isCircular = true - roundedBitmapDrawable.setAntiAlias(true) - actionBar?.setIcon(roundedBitmapDrawable) + dataSource.subscribe( + object : BaseBitmapDataSubscriber() { + override fun onNewResultImpl(bitmap: Bitmap?) { + if (actionBar != null && bitmap != null && resources != null) { + val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(resources!!, bitmap) + roundedBitmapDrawable.isCircular = true + roundedBitmapDrawable.setAntiAlias(true) + actionBar?.setIcon(roundedBitmapDrawable) + } } - } - override fun onFailureImpl(dataSource: DataSource>) {} - }, UiThreadImmediateExecutorService.getInstance()) + override fun onFailureImpl(dataSource: DataSource>) {} + }, + UiThreadImmediateExecutorService.getInstance() + ) } } private fun inOneToOneCall() = currentConversation != null && currentConversation?.type != null && - currentConversation?.type == Conversation.ConversationType - .ROOM_TYPE_ONE_TO_ONE_CALL + currentConversation?.type == Conversation.ConversationType + .ROOM_TYPE_ONE_TO_ONE_CALL override fun onViewBound(view: View) { actionBar?.show() @@ -374,29 +414,49 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter adapterWasNull = true val messageHolders = MessageHolders() - messageHolders.setIncomingTextConfig(MagicIncomingTextMessageViewHolder::class.java, R.layout.item_custom_incoming_text_message) - messageHolders.setOutcomingTextConfig(MagicOutcomingTextMessageViewHolder::class.java, R.layout.item_custom_outcoming_text_message) + messageHolders.setIncomingTextConfig( + MagicIncomingTextMessageViewHolder::class.java, + R.layout.item_custom_incoming_text_message + ) + messageHolders.setOutcomingTextConfig( + MagicOutcomingTextMessageViewHolder::class.java, + R.layout.item_custom_outcoming_text_message + ) - messageHolders.setIncomingImageConfig(MagicPreviewMessageViewHolder::class.java, R.layout.item_custom_incoming_preview_message) - messageHolders.setOutcomingImageConfig(MagicPreviewMessageViewHolder::class.java, R.layout.item_custom_outcoming_preview_message) + messageHolders.setIncomingImageConfig( + MagicPreviewMessageViewHolder::class.java, + R.layout.item_custom_incoming_preview_message + ) + messageHolders.setOutcomingImageConfig( + MagicPreviewMessageViewHolder::class.java, + R.layout.item_custom_outcoming_preview_message + ) - messageHolders.registerContentType(CONTENT_TYPE_SYSTEM_MESSAGE, MagicSystemMessageViewHolder::class.java, - R.layout.item_system_message, MagicSystemMessageViewHolder::class.java, R.layout.item_system_message, - this) + messageHolders.registerContentType( + CONTENT_TYPE_SYSTEM_MESSAGE, MagicSystemMessageViewHolder::class.java, + R.layout.item_system_message, MagicSystemMessageViewHolder::class.java, R.layout.item_system_message, + this + ) - messageHolders.registerContentType(CONTENT_TYPE_UNREAD_NOTICE_MESSAGE, - MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, - MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, this) + messageHolders.registerContentType( + CONTENT_TYPE_UNREAD_NOTICE_MESSAGE, + MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, + MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, this + ) - adapter = TalkMessagesListAdapter(conversationUser?.userId, messageHolders, ImageLoader { imageView, url, payload -> - val draweeController = Fresco.newDraweeControllerBuilder() + adapter = TalkMessagesListAdapter( + conversationUser?.userId, + messageHolders, + ImageLoader { imageView, url, payload -> + val draweeController = Fresco.newDraweeControllerBuilder() .setImageRequest(DisplayUtils.getImageRequestForUrl(url, conversationUser)) .setControllerListener(DisplayUtils.getImageControllerListener(imageView)) .setOldController(imageView.controller) .setAutoPlayAnimations(true) .build() - imageView.controller = draweeController - }) + imageView.controller = draweeController + } + ) } else { messagesListView?.visibility = View.VISIBLE } @@ -449,7 +509,6 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } }) - val filters = arrayOfNulls(1) val lengthFilter = conversationUser?.messageMaxLength ?: 1000 @@ -458,27 +517,34 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter messageInput?.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - } override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { if (s.length >= lengthFilter) { - messageInput?.error = String.format(Objects.requireNonNull - (resources).getString(R.string.nc_limit_hit), Integer.toString(lengthFilter)) + messageInput?.error = String.format( + Objects.requireNonNull + (resources).getString(R.string.nc_limit_hit), + Integer.toString(lengthFilter) + ) } else { messageInput?.error = null } val editable = messageInput?.editableText if (editable != null && messageInput != null) { - val mentionSpans = editable.getSpans(0, messageInput!!.length(), - Spans.MentionChipSpan::class.java) + val mentionSpans = editable.getSpans( + 0, messageInput!!.length(), + Spans.MentionChipSpan::class.java + ) var mentionSpan: Spans.MentionChipSpan for (i in mentionSpans.indices) { mentionSpan = mentionSpans[i] if (start >= editable.getSpanStart(mentionSpan) && start < editable.getSpanEnd(mentionSpan)) { - if (editable.subSequence(editable.getSpanStart(mentionSpan), - editable.getSpanEnd(mentionSpan)).toString().trim { it <= ' ' } != mentionSpan.label) { + if (editable.subSequence( + editable.getSpanStart(mentionSpan), + editable.getSpanEnd(mentionSpan) + ).toString().trim { it <= ' ' } != mentionSpan.label + ) { editable.removeSpan(mentionSpan) } } @@ -487,18 +553,19 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } override fun afterTextChanged(s: Editable) { - } }) messageInputView?.setAttachmentsListener { - activity?.let { AttachmentDialog(it, this).show() }; + activity?.let { AttachmentDialog(it, this).show() } } messageInputView?.button?.setOnClickListener { v -> submitMessage() } - messageInputView?.button?.contentDescription = resources?.getString(R.string - .nc_description_send_message_button) + messageInputView?.button?.contentDescription = resources?.getString( + R.string + .nc_description_send_message_button + ) if (currentConversation != null && currentConversation?.roomId != null) { loadAvatarForStatusBar() @@ -516,7 +583,6 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter super.onViewBound(view) } - private fun checkReadOnlyState() { if (currentConversation != null) { if (currentConversation?.shouldShowLobby(conversationUser) ?: false || currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState == Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY) { @@ -524,7 +590,6 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter conversationVoiceCallMenuItem?.icon?.alpha = 99 conversationVideoMenuItem?.icon?.alpha = 99 messageInputView?.visibility = View.GONE - } else { if (conversationVoiceCallMenuItem != null) { conversationVoiceCallMenuItem?.icon?.alpha = 255 @@ -535,7 +600,8 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } if (currentConversation != null && currentConversation!!.shouldShowLobby - (conversationUser)) { + (conversationUser) + ) { messageInputView?.visibility = View.GONE } else { messageInputView?.visibility = View.VISIBLE @@ -558,9 +624,15 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter loadingProgressBar?.visibility = View.GONE if (currentConversation?.lobbyTimer != null && currentConversation?.lobbyTimer != - 0L) { - conversationLobbyText?.text = String.format(resources!!.getString(R.string.nc_lobby_waiting_with_date), DateUtils.getLocalDateStringFromTimestampForLobby(currentConversation?.lobbyTimer - ?: 0)) + 0L + ) { + conversationLobbyText?.text = String.format( + resources!!.getString(R.string.nc_lobby_waiting_with_date), + DateUtils.getLocalDateStringFromTimestampForLobby( + currentConversation?.lobbyTimer + ?: 0 + ) + ) } else { conversationLobbyText?.setText(R.string.nc_lobby_waiting) } @@ -607,7 +679,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter filenamesWithLinebreaks += filename + "\n" } - val confirmationQuestion = when(files.size) { + val confirmationQuestion = when (files.size) { 1 -> context?.resources?.getString(R.string.nc_upload_confirm_send_single)?.let { String.format(it, title) } @@ -617,21 +689,25 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } LovelyStandardDialog(activity) - .setPositiveButtonColorRes(R.color.nc_darkGreen) - .setTitle(confirmationQuestion) - .setMessage(filenamesWithLinebreaks) - .setPositiveButton(R.string.nc_yes) { v -> - uploadFiles(files) - Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_in_progess), Toast - .LENGTH_LONG).show(); - } - .setNegativeButton(R.string.nc_no) {} - .show() + .setPositiveButtonColorRes(R.color.nc_darkGreen) + .setTitle(confirmationQuestion) + .setMessage(filenamesWithLinebreaks) + .setPositiveButton(R.string.nc_yes) { v -> + uploadFiles(files) + Toast.makeText( + context, context?.resources?.getString(R.string.nc_upload_in_progess), + Toast.LENGTH_LONG + ).show() + } + .setNegativeButton(R.string.nc_no) {} + .show() } catch (e: IllegalStateException) { - Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show() + Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG) + .show() Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e) } catch (e: IllegalArgumentException) { - Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show() + Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG) + .show() Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e) } } @@ -642,13 +718,13 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter try { require(files.isNotEmpty()) val data: Data = Data.Builder() - .putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, files.toTypedArray()) - .putString(UploadAndShareFilesWorker.NC_TARGETPATH, conversationUser?.getAttachmentFolder()) - .putString(UploadAndShareFilesWorker.ROOM_TOKEN, roomToken) - .build() + .putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, files.toTypedArray()) + .putString(UploadAndShareFilesWorker.NC_TARGETPATH, conversationUser?.getAttachmentFolder()) + .putString(UploadAndShareFilesWorker.ROOM_TOKEN, roomToken) + .build() val uploadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadAndShareFilesWorker::class.java) - .setInputData(data) - .build() + .setInputData(data) + .build() WorkManager.getInstance().enqueue(uploadWorker) } catch (e: IllegalArgumentException) { Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show() @@ -662,8 +738,15 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter addCategory(Intent.CATEGORY_OPENABLE) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) } - startActivityForResult(Intent.createChooser(action, context?.resources?.getString( - R.string.nc_upload_choose_local_files)), REQUEST_CODE_CHOOSE_FILE); + startActivityForResult( + Intent.createChooser( + action, + context?.resources?.getString( + R.string.nc_upload_choose_local_files + ) + ), + REQUEST_CODE_CHOOSE_FILE + ) } fun showBrowserScreen(browserType: BrowserController.BrowserType) { @@ -671,19 +754,23 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter bundle.putParcelable(BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap(browserType)) bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(conversationUser)) bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) - router.pushController(RouterTransaction.with(BrowserForSharingController(bundle)) + router.pushController( + RouterTransaction.with(BrowserForSharingController(bundle)) .pushChangeHandler(VerticalChangeHandler()) - .popChangeHandler(VerticalChangeHandler())) + .popChangeHandler(VerticalChangeHandler()) + ) } private fun showConversationInfoScreen() { val bundle = Bundle() bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) - bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, inOneToOneCall()); - router.pushController(RouterTransaction.with(ConversationInfoController(bundle)) + bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, inOneToOneCall()) + router.pushController( + RouterTransaction.with(ConversationInfoController(bundle)) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) + .popChangeHandler(HorizontalChangeHandler()) + ) } private fun setupMentionAutocomplete() { @@ -691,17 +778,19 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter resources?.let { val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default)) val presenter = MentionAutocompletePresenter(applicationContext, roomToken) - val callback = MentionAutocompleteCallback(activity, - conversationUser, messageInput) + val callback = MentionAutocompleteCallback( + activity, + conversationUser, messageInput + ) if (mentionAutocomplete == null && messageInput != null) { mentionAutocomplete = Autocomplete.on(messageInput) - .with(elevation) - .with(backgroundDrawable) - .with(MagicCharPolicy('@')) - .with(presenter) - .with(callback) - .build() + .with(elevation) + .with(backgroundDrawable) + .with(MagicCharPolicy('@')) + .with(presenter) + .with(callback) + .build() } } } @@ -730,8 +819,10 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter smileyButton?.setColorFilter(resources!!.getColor(R.color.colorPrimary), PorterDuff.Mode.SRC_IN) } }.setOnEmojiPopupDismissListener { - smileyButton?.setColorFilter(resources!!.getColor(R.color.emoji_icons), - PorterDuff.Mode.SRC_IN) + smileyButton?.setColorFilter( + resources!!.getColor(R.color.emoji_icons), + PorterDuff.Mode.SRC_IN + ) }.setOnEmojiClickListener { emoji, imageView -> messageInput?.editableText?.append(" ") }.build(it) } @@ -753,11 +844,15 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter private fun cancelNotificationsForCurrentConversation() { if (conversationUser != null) { if (!conversationUser.hasSpreedFeatureCapability("no-ping") && !TextUtils.isEmpty(roomId)) { - NotificationUtils.cancelExistingNotificationsForRoom(applicationContext, - conversationUser, roomId) + NotificationUtils.cancelExistingNotificationsForRoom( + applicationContext, + conversationUser, roomId + ) } else if (!TextUtils.isEmpty(roomToken)) { - NotificationUtils.cancelExistingNotificationsForRoom(applicationContext, - conversationUser, roomToken!!) + NotificationUtils.cancelExistingNotificationsForRoom( + applicationContext, + conversationUser, roomToken!! + ) } } } @@ -775,8 +870,12 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter activity?.findViewById(R.id.toolbar)?.setOnClickListener(null) } - if (conversationUser != null && conversationUser?.hasSpreedFeatureCapability("no-ping") - && activity != null && !activity?.isChangingConfigurations!! && !isLeavingForConversation) { + if (conversationUser != null && + conversationUser.hasSpreedFeatureCapability("no-ping") && + activity != null && + !activity?.isChangingConfigurations!! && + !isLeavingForConversation + ) { wasDetached = true leaveRoom() } @@ -819,26 +918,30 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter private fun startPing() { if (conversationUser != null && !conversationUser.hasSpreedFeatureCapability("no-ping")) { - ncApi?.pingCall(credentials, ApiUtils.getUrlForCallPing(conversationUser.baseUrl, - roomToken)) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.repeatWhen { observable -> observable.delay(5000, TimeUnit.MILLISECONDS) } - ?.takeWhile { observable -> inConversation } - ?.retry(3) { observable -> inConversation } - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - disposableList.add(d) - } + ncApi?.pingCall( + credentials, + ApiUtils.getUrlForCallPing( + conversationUser.baseUrl, + roomToken + ) + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.repeatWhen { observable -> observable.delay(5000, TimeUnit.MILLISECONDS) } + ?.takeWhile { observable -> inConversation } + ?.retry(3) { observable -> inConversation } + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableList.add(d) + } - override fun onNext(genericOverall: GenericOverall) { + override fun onNext(genericOverall: GenericOverall) { + } - } + override fun onError(e: Throwable) {} - override fun onError(e: Throwable) {} - - override fun onComplete() {} - }) + override fun onComplete() {} + }) } } @@ -850,57 +953,63 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter private fun joinRoomWithPassword() { if (currentConversation == null || TextUtils.isEmpty(currentConversation?.sessionId) || - currentConversation?.sessionId == "0") { - ncApi?.joinRoom(credentials, - ApiUtils.getUrlForSettingMyselfAsActiveParticipant(conversationUser?.baseUrl, roomToken), roomPassword) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.retry(3) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - disposableList.add(d) + currentConversation?.sessionId == "0" + ) { + ncApi?.joinRoom( + credentials, + ApiUtils.getUrlForSettingMyselfAsActiveParticipant(conversationUser?.baseUrl, roomToken), roomPassword + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.retry(3) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableList.add(d) + } + + override fun onNext(roomOverall: RoomOverall) { + inConversation = true + currentConversation?.sessionId = roomOverall.ocs.data.sessionId + + ApplicationWideCurrentRoomHolder.getInstance().session = + currentConversation?.sessionId + startPing() + + setupWebsocket() + checkLobbyState() + + if (isFirstMessagesProcessing) { + pullChatMessages(0) + } else { + pullChatMessages(1, 0) } - override fun onNext(roomOverall: RoomOverall) { - inConversation = true - currentConversation?.sessionId = roomOverall.ocs.data.sessionId - - ApplicationWideCurrentRoomHolder.getInstance().session = - currentConversation?.sessionId - startPing() - - setupWebsocket() - checkLobbyState() - - if (isFirstMessagesProcessing) { - pullChatMessages(0) - } else { - pullChatMessages(1, 0) - } - - if (magicWebSocketInstance != null) { - magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(roomToken, currentConversation?.sessionId) - } - if (startCallFromNotification != null && startCallFromNotification ?: false) { - startCallFromNotification = false - startACall(voiceOnly) - } + if (magicWebSocketInstance != null) { + magicWebSocketInstance?.joinRoomWithRoomTokenAndSession( + roomToken, + currentConversation?.sessionId + ) } - - override fun onError(e: Throwable) { - + if (startCallFromNotification != null && startCallFromNotification ?: false) { + startCallFromNotification = false + startACall(voiceOnly) } + } - override fun onComplete() { + override fun onError(e: Throwable) { + } - } - }) + override fun onComplete() { + } + }) } else { inConversation = true ApplicationWideCurrentRoomHolder.getInstance().session = currentConversation?.sessionId if (magicWebSocketInstance != null) { - magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(roomToken, - currentConversation?.sessionId) + magicWebSocketInstance?.joinRoomWithRoomTokenAndSession( + roomToken, + currentConversation?.sessionId + ) } startPing() if (isFirstMessagesProcessing) { @@ -912,39 +1021,45 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } private fun leaveRoom() { - ncApi?.leaveRoom(credentials, - ApiUtils.getUrlForSettingMyselfAsActiveParticipant(conversationUser?.baseUrl, - roomToken)) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - disposableList.add(d) + ncApi?.leaveRoom( + credentials, + ApiUtils.getUrlForSettingMyselfAsActiveParticipant( + conversationUser?.baseUrl, + roomToken + ) + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableList.add(d) + } + + override fun onNext(genericOverall: GenericOverall) { + checkingLobbyStatus = false + + if (lobbyTimerHandler != null) { + lobbyTimerHandler?.removeCallbacksAndMessages(null) } - override fun onNext(genericOverall: GenericOverall) { - checkingLobbyStatus = false - - if (lobbyTimerHandler != null) { - lobbyTimerHandler?.removeCallbacksAndMessages(null) - } - - if (magicWebSocketInstance != null && currentConversation != null) { - magicWebSocketInstance?.joinRoomWithRoomTokenAndSession("", - currentConversation?.sessionId) - } - - if (!isDestroyed && !isBeingDestroyed && !wasDetached) { - router.popCurrentController() - } + if (magicWebSocketInstance != null && currentConversation != null) { + magicWebSocketInstance?.joinRoomWithRoomTokenAndSession( + "", + currentConversation?.sessionId + ) } - override fun onError(e: Throwable) {} - - override fun onComplete() { - dispose() + if (!isDestroyed && !isBeingDestroyed && !wasDetached) { + router.popCurrentController() } - }) + } + + override fun onError(e: Throwable) {} + + override fun onComplete() { + dispose() + } + }) } private fun setSenderId() { @@ -957,14 +1072,15 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } catch (e: IllegalAccessException) { Log.w(TAG, "Failed to access and set field") } - } private fun submitMessage() { if (messageInput != null) { val editable = messageInput!!.editableText - val mentionSpans = editable.getSpans(0, editable.length, - Spans.MentionChipSpan::class.java) + val mentionSpans = editable.getSpans( + 0, editable.length, + Spans.MentionChipSpan::class.java + ) var mentionSpan: Spans.MentionChipSpan for (i in mentionSpans.indices) { mentionSpan = mentionSpans[i] @@ -977,7 +1093,10 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter messageInput?.setText("") val replyMessageId: Int? = view?.findViewById(R.id.quotedChatMessageView)?.tag as Int? - sendMessage(editable, if (view?.findViewById(R.id.quotedChatMessageView)?.visibility == View.VISIBLE) replyMessageId else null) + sendMessage( + editable, + if (view?.findViewById(R.id.quotedChatMessageView)?.visibility == View.VISIBLE) replyMessageId else null + ) cancelReply() } } @@ -986,55 +1105,54 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter if (conversationUser != null) { ncApi!!.sendChatMessage( - credentials, - ApiUtils.getUrlForChat(conversationUser.baseUrl, roomToken), - message, - conversationUser.displayName, - replyTo + credentials, + ApiUtils.getUrlForChat(conversationUser.baseUrl, roomToken), + message, + conversationUser.displayName, + replyTo ) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + } + override fun onNext(genericOverall: GenericOverall) { + myFirstMessage = message + + if (popupBubble?.isShown ?: false) { + popupBubble?.hide() } - override fun onNext(genericOverall: GenericOverall) { - myFirstMessage = message + messagesListView?.smoothScrollToPosition(0) + } - if (popupBubble?.isShown ?: false) { - popupBubble?.hide() - } + override fun onError(e: Throwable) { + if (e is HttpException) { + val code = e.code() + if (Integer.toString(code).startsWith("2")) { + myFirstMessage = message - messagesListView?.smoothScrollToPosition(0) - } - - override fun onError(e: Throwable) { - if (e is HttpException) { - val code = e.code() - if (Integer.toString(code).startsWith("2")) { - myFirstMessage = message - - if (popupBubble?.isShown ?: false) { - popupBubble?.hide() - } - - messagesListView?.smoothScrollToPosition(0) + if (popupBubble?.isShown ?: false) { + popupBubble?.hide() } + + messagesListView?.smoothScrollToPosition(0) } } + } - override fun onComplete() { - - } - }) + override fun onComplete() { + } + }) } } private fun setupWebsocket() { if (conversationUser != null) { if (WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id) != null) { - magicWebSocketInstance = WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id) + magicWebSocketInstance = + WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id) } else { magicWebSocketInstance = null } @@ -1047,7 +1165,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } if (currentConversation != null && currentConversation!!.shouldShowLobby(conversationUser)) { - //return + // return } val fieldMap = HashMap() @@ -1090,60 +1208,61 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter if (!wasDetached) { if (lookIntoFuture > 0) { val finalTimeout = timeout - ncApi?.pullChatMessages(credentials, - ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.takeWhile { observable -> inConversation && !wasDetached } - ?.subscribe(object : Observer> { - override fun onSubscribe(d: Disposable) { - disposableList.add(d) + ncApi?.pullChatMessages( + credentials, + ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.takeWhile { observable -> inConversation && !wasDetached } + ?.subscribe(object : Observer> { + override fun onSubscribe(d: Disposable) { + disposableList.add(d) + } + + override fun onNext(response: Response<*>) { + if (response.code() == 304) { + pullChatMessages(1, setReadMarker, xChatLastCommonRead) + } else if (response.code() == 412) { + futurePreconditionFailed = true + } else { + processMessages(response, true, finalTimeout) } + } - override fun onNext(response: Response<*>) { - if (response.code() == 304) { - pullChatMessages(1, setReadMarker, xChatLastCommonRead) - } else if (response.code() == 412) { - futurePreconditionFailed = true - } else { - processMessages(response, true, finalTimeout) - } - } - - override fun onError(e: Throwable) { - } - - override fun onComplete() { - - } - }) + override fun onError(e: Throwable) { + } + override fun onComplete() { + } + }) } else { - ncApi?.pullChatMessages(credentials, - ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.takeWhile { observable -> inConversation && !wasDetached } - ?.subscribe(object : Observer> { - override fun onSubscribe(d: Disposable) { - disposableList.add(d) - } + ncApi?.pullChatMessages( + credentials, + ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.takeWhile { observable -> inConversation && !wasDetached } + ?.subscribe(object : Observer> { + override fun onSubscribe(d: Disposable) { + disposableList.add(d) + } - override fun onNext(response: Response<*>) { - if (response.code() == 412) { - pastPreconditionFailed = true - } else { - processMessages(response, false, 0) - } + override fun onNext(response: Response<*>) { + if (response.code() == 412) { + pastPreconditionFailed = true + } else { + processMessages(response, false, 0) } + } - override fun onError(e: Throwable) { - } + override fun onError(e: Throwable) { + } - override fun onComplete() { - - } - }) + override fun onComplete() { + } + }) } } } @@ -1180,7 +1299,6 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter loadingProgressBar?.visibility = View.GONE messagesListView?.visibility = View.VISIBLE - } var countGroupedMessages = 0 @@ -1189,11 +1307,14 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter for (i in chatMessageList.indices) { if (chatMessageList.size > i + 1) { if (TextUtils.isEmpty(chatMessageList[i].systemMessage) && - TextUtils.isEmpty(chatMessageList[i + 1].systemMessage) && - chatMessageList[i + 1].actorId == chatMessageList[i].actorId && - countGroupedMessages < 4 && DateFormatter.isSameDay(chatMessageList[i].createdAt, - chatMessageList[i + 1].createdAt)) { - chatMessageList[i].isGrouped = true; + TextUtils.isEmpty(chatMessageList[i + 1].systemMessage) && + chatMessageList[i + 1].actorId == chatMessageList[i].actorId && + countGroupedMessages < 4 && DateFormatter.isSameDay( + chatMessageList[i].createdAt, + chatMessageList[i + 1].createdAt + ) + ) { + chatMessageList[i].isGrouped = true countGroupedMessages++ } else { countGroupedMessages = 0 @@ -1201,7 +1322,8 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } val chatMessage = chatMessageList[i] - chatMessage.isOneToOneConversation = currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + chatMessage.isOneToOneConversation = + currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed chatMessage.activeUser = conversationUser } @@ -1209,7 +1331,6 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter if (adapter != null) { adapter?.addToEnd(chatMessageList, false) } - } else { var chatMessage: ChatMessage @@ -1225,7 +1346,8 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter adapter?.addToStart(unreadChatMessage, false) } - val isThereANewNotice = shouldAddNewMessagesNotice || adapter?.getMessagePositionByIdInReverse("-1") != -1 + val isThereANewNotice = + shouldAddNewMessagesNotice || adapter?.getMessagePositionByIdInReverse("-1") != -1 for (i in chatMessageList.indices) { chatMessage = chatMessageList[i] @@ -1241,7 +1363,8 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } } - val shouldScroll = !isThereANewNotice && !shouldAddNewMessagesNotice && layoutManager?.findFirstVisibleItemPosition() == 0 || adapter != null && adapter?.itemCount == 0 + val shouldScroll = + !isThereANewNotice && !shouldAddNewMessagesNotice && layoutManager?.findFirstVisibleItemPosition() == 0 || adapter != null && adapter?.itemCount == 0 if (!shouldAddNewMessagesNotice && !shouldScroll && popupBubble != null) { if (!popupBubble!!.isShown) { @@ -1255,18 +1378,24 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } if (adapter != null) { - chatMessage.isGrouped = (adapter!!.isPreviousSameAuthor(chatMessage - .actorId, -1) && adapter!!.getSameAuthorLastMessagesCount(chatMessage.actorId) % 5 > 0) - chatMessage.isOneToOneConversation = (currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) + chatMessage.isGrouped = ( + adapter!!.isPreviousSameAuthor( + chatMessage.actorId, + -1 + ) && adapter!!.getSameAuthorLastMessagesCount(chatMessage.actorId) % 5 > 0 + ) + chatMessage.isOneToOneConversation = + (currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) adapter?.addToStart(chatMessage, shouldScroll) } - } if (shouldAddNewMessagesNotice && adapter != null && messagesListView != null) { - layoutManager?.scrollToPositionWithOffset(adapter!!.getMessagePositionByIdInReverse("-1"), messagesListView!!.height / 2) + layoutManager?.scrollToPositionWithOffset( + adapter!!.getMessagePositionByIdInReverse("-1"), + messagesListView!!.height / 2 + ) } - } // update read status of all messages @@ -1310,7 +1439,6 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } } - override fun format(date: Date): String { return if (DateFormatter.isToday(date)) { resources!!.getString(R.string.nc_date_header_today) @@ -1344,7 +1472,6 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> { @@ -1394,10 +1521,9 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry): Boolean { return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage - .SystemMessageType.PARENT_MESSAGE_DELETED + .SystemMessageType.PARENT_MESSAGE_DELETED } - private fun startACall(isVoiceOnlyCall: Boolean) { isLeavingForConversation = true val callIntent = getIntentForCall(isVoiceOnlyCall) @@ -1427,7 +1553,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } else { null } - } ?:run { + } ?: run { return null } } @@ -1440,13 +1566,17 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } override fun onMessageViewLongClick(view: View?, message: IMessage?) { - PopupMenu(this.context, view, if (message?.user?.id == conversationUser?.userId) Gravity.END else Gravity.START).apply { + PopupMenu( + this.context, + view, + if (message?.user?.id == conversationUser?.userId) Gravity.END else Gravity.START + ).apply { setOnMenuItemClickListener { item -> when (item?.itemId) { R.id.action_copy_message -> { val clipboardManager = - activity?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager + activity?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager val clipData = ClipData.newPlainText(resources?.getString(R.string.nc_app_name), message?.text) clipboardManager.setPrimaryClip(clipData) true @@ -1456,28 +1586,40 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter chatMessage?.let { messageInputView?.findViewById(R.id.attachmentButton)?.visibility = View.GONE messageInputView?.findViewById(R.id.attachmentButtonSpace)?.visibility = View.GONE - messageInputView?.findViewById(R.id.cancelReplyButton)?.visibility = View.VISIBLE + messageInputView?.findViewById(R.id.cancelReplyButton)?.visibility = + View.VISIBLE messageInputView?.findViewById(R.id.quotedMessage)?.maxLines = 2 - messageInputView?.findViewById(R.id.quotedMessage)?.ellipsize = TextUtils.TruncateAt.END + messageInputView?.findViewById(R.id.quotedMessage)?.ellipsize = + TextUtils.TruncateAt.END messageInputView?.findViewById(R.id.quotedMessage)?.text = it.text - messageInputView?.findViewById(R.id.quotedMessageAuthor)?.text = it.actorDisplayName - ?: context!!.getText(R.string.nc_nick_guest) + messageInputView?.findViewById(R.id.quotedMessageAuthor)?.text = + it.actorDisplayName ?: context!!.getText(R.string.nc_nick_guest) conversationUser?.let { currentUser -> chatMessage.imageUrl?.let { previewImageUrl -> - messageInputView?.findViewById(R.id.quotedMessageImage)?.visibility = View.VISIBLE + messageInputView?.findViewById(R.id.quotedMessageImage)?.visibility = + View.VISIBLE - val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96f, resources?.displayMetrics) - messageInputView?.findViewById(R.id.quotedMessageImage)?.maxHeight = px.toInt() - val layoutParams = messageInputView?.findViewById(R.id.quotedMessageImage)?.layoutParams as FlexboxLayout.LayoutParams + val px = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 96f, + resources?.displayMetrics + ) + messageInputView?.findViewById(R.id.quotedMessageImage)?.maxHeight = + px.toInt() + val layoutParams = + messageInputView?.findViewById(R.id.quotedMessageImage)?.layoutParams as FlexboxLayout.LayoutParams layoutParams.flexGrow = 0f - messageInputView?.findViewById(R.id.quotedMessageImage)?.layoutParams = layoutParams - messageInputView?.findViewById(R.id.quotedMessageImage)?.load(previewImageUrl) { - addHeader("Authorization", credentials!!) - } + messageInputView?.findViewById(R.id.quotedMessageImage)?.layoutParams = + layoutParams + messageInputView?.findViewById(R.id.quotedMessageImage) + ?.load(previewImageUrl) { + addHeader("Authorization", credentials!!) + } } ?: run { - messageInputView?.findViewById(R.id.quotedMessageImage)?.visibility = View.GONE + messageInputView?.findViewById(R.id.quotedMessageImage)?.visibility = + View.GONE } } @@ -1488,30 +1630,36 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter } R.id.action_delete_message -> { ncApi?.deleteChatMessage( - credentials, - ApiUtils.getUrlForMessageDeletion(conversationUser?.baseUrl, roomToken, message?.id) + credentials, + ApiUtils.getUrlForMessageDeletion(conversationUser?.baseUrl, roomToken, message?.id) )?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + } - override fun onNext(t: ChatOverallSingleMessage) { - if (t.ocs.meta.statusCode == HttpURLConnection.HTTP_ACCEPTED) { - Toast.makeText(context, R.string.nc_delete_message_leaked_to_matterbridge, - Toast.LENGTH_LONG).show() - } + override fun onNext(t: ChatOverallSingleMessage) { + if (t.ocs.meta.statusCode == HttpURLConnection.HTTP_ACCEPTED) { + Toast.makeText( + context, R.string.nc_delete_message_leaked_to_matterbridge, + Toast.LENGTH_LONG + ).show() } + } - override fun onError(e: Throwable) { - Log.e(TAG, "Something went wrong when trying to delete message with id " + - message?.id, e) - Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() - } + override fun onError(e: Throwable) { + Log.e( + TAG, + "Something went wrong when trying to delete message with id " + + message?.id, + e + ) + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } - override fun onComplete() { - } - }) + override fun onComplete() { + } + }) true } else -> false @@ -1531,7 +1679,8 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter val messageTemp = message as ChatMessage messageTemp.isDeleted = true - messageTemp.isOneToOneConversation = currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + messageTemp.isOneToOneConversation = + currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL messageTemp.isLinkPreviewAllowed = isLinkPreviewAllowed messageTemp.activeUser = conversationUser @@ -1561,7 +1710,6 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter return true } - override fun hasContentFor(message: IMessage, type: Byte): Boolean { when (type) { CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage) @@ -1589,55 +1737,63 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter @Subscribe(threadMode = ThreadMode.BACKGROUND) fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) { - if (currentConversation?.type != Conversation.ConversationType - .ROOM_TYPE_ONE_TO_ONE_CALL || currentConversation?.name != - userMentionClickEvent.userId) { - val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(conversationUser?.baseUrl, "1", - userMentionClickEvent.userId, null) + if (currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || + currentConversation?.name != userMentionClickEvent.userId + ) { + val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + conversationUser?.baseUrl, "1", + userMentionClickEvent.userId, null + ) - ncApi?.createRoom(credentials, - retrofitBucket.url, retrofitBucket.queryMap) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { + ncApi?.createRoom( + credentials, + retrofitBucket.url, retrofitBucket.queryMap + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + } - } + override fun onNext(roomOverall: RoomOverall) { + val conversationIntent = Intent(activity, MagicCallActivity::class.java) + val bundle = Bundle() + bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs.data.token) + bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs.data.roomId) - override fun onNext(roomOverall: RoomOverall) { - val conversationIntent = Intent(activity, MagicCallActivity::class.java) - val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) - bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs.data.token) - bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs.data.roomId) - - if (conversationUser != null) { - if (conversationUser.hasSpreedFeatureCapability("chat-v2")) { - bundle.putParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION, - Parcels.wrap(roomOverall.ocs.data)) - conversationIntent.putExtras(bundle) - - ConductorRemapping.remapChatController(router, conversationUser.id, - roomOverall.ocs.data.token, bundle, false) - } - - } else { + if (conversationUser != null) { + if (conversationUser.hasSpreedFeatureCapability("chat-v2")) { + bundle.putParcelable( + BundleKeys.KEY_ACTIVE_CONVERSATION, + Parcels.wrap(roomOverall.ocs.data) + ) conversationIntent.putExtras(bundle) - startActivity(conversationIntent) - Handler().postDelayed({ + + ConductorRemapping.remapChatController( + router, conversationUser.id, + roomOverall.ocs.data.token, bundle, false + ) + } + } else { + conversationIntent.putExtras(bundle) + startActivity(conversationIntent) + Handler().postDelayed( + { if (!isDestroyed && !isBeingDestroyed) { router.popCurrentController() } - }, 100) - } + }, + 100 + ) } + } - override fun onError(e: Throwable) { + override fun onError(e: Throwable) { + } - } - - override fun onComplete() {} - }) + override fun onComplete() {} + }) } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt index 84d03acd2..ac2d2f262 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt @@ -85,50 +85,69 @@ import io.reactivex.schedulers.Schedulers import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import java.util.* +import java.util.Calendar +import java.util.Collections +import java.util.Comparator +import java.util.Locale import javax.inject.Inject -import kotlin.collections.ArrayList - @AutoInjector(NextcloudTalkApplication::class) class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleAdapter.OnItemClickListener { @BindView(R.id.notification_settings) lateinit var notificationsPreferenceScreen: MaterialPreferenceScreen + @BindView(R.id.progressBar) lateinit var progressBar: ProgressBar + @BindView(R.id.conversation_info_message_notifications) lateinit var messageNotificationLevel: MaterialChoicePreference + @BindView(R.id.webinar_settings) lateinit var conversationInfoWebinar: MaterialPreferenceScreen + @BindView(R.id.conversation_info_lobby) lateinit var conversationInfoLobby: MaterialSwitchPreference + @BindView(R.id.conversation_info_name) lateinit var nameCategoryView: MaterialPreferenceCategory + @BindView(R.id.start_time_preferences) lateinit var startTimeView: MaterialStandardPreference + @BindView(R.id.avatar_image) lateinit var conversationAvatarImageView: SimpleDraweeView + @BindView(R.id.display_name_text) lateinit var conversationDisplayName: EmojiTextView + @BindView(R.id.participants_list_category) lateinit var participantsListCategory: MaterialPreferenceCategory + @BindView(R.id.addParticipantsAction) - lateinit var addParticipantsAction: MaterialStandardPreference; + lateinit var addParticipantsAction: MaterialStandardPreference + @BindView(R.id.recycler_view) lateinit var recyclerView: RecyclerView + @BindView(R.id.deleteConversationAction) lateinit var deleteConversationAction: MaterialStandardPreference + @BindView(R.id.leaveConversationAction) lateinit var leaveConversationAction: MaterialStandardPreference + @BindView(R.id.ownOptions) lateinit var ownOptionsCategory: MaterialPreferenceCategory + @BindView(R.id.muteCalls) lateinit var muteCalls: MaterialSwitchPreference + @set:Inject lateinit var ncApi: NcApi + @set:Inject lateinit var context: Context + @set:Inject lateinit var eventBus: EventBus @@ -164,7 +183,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) conversationUser = args.getParcelable(BundleKeys.KEY_USER_ENTITY) conversationToken = args.getString(BundleKeys.KEY_ROOM_TOKEN) - hasAvatarSpacing = args.getBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, false); + hasAvatarSpacing = args.getBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, false) credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token) } @@ -207,14 +226,19 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA } private fun setupWebinaryView() { - if (conversationUser!!.hasSpreedFeatureCapability("webinary-lobby") && (conversation!!.type - == Conversation.ConversationType.ROOM_GROUP_CALL || conversation!!.type == - Conversation.ConversationType.ROOM_PUBLIC_CALL) && conversation!!.canModerate(conversationUser)) { + if (conversationUser!!.hasSpreedFeatureCapability("webinary-lobby") && + ( + conversation!!.type == Conversation.ConversationType.ROOM_GROUP_CALL || + conversation!!.type == Conversation.ConversationType.ROOM_PUBLIC_CALL + ) && + conversation!!.canModerate(conversationUser) + ) { conversationInfoWebinar.visibility = View.VISIBLE - val isLobbyOpenToModeratorsOnly = conversation!!.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY + val isLobbyOpenToModeratorsOnly = + conversation!!.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY (conversationInfoLobby.findViewById(R.id.mp_checkable) as SwitchCompat) - .isChecked = isLobbyOpenToModeratorsOnly + .isChecked = isLobbyOpenToModeratorsOnly reconfigureLobbyTimerView() @@ -225,12 +249,17 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA currentTimeCalendar.timeInMillis = conversation!!.lobbyTimer * 1000 } - dateTimePicker(minDateTime = Calendar.getInstance(), requireFutureDateTime = - true, currentDateTime = currentTimeCalendar, dateTimeCallback = { _, - dateTime -> - reconfigureLobbyTimerView(dateTime) - submitLobbyChanges() - }) + dateTimePicker( + minDateTime = Calendar.getInstance(), + requireFutureDateTime = + true, + currentDateTime = currentTimeCalendar, + dateTimeCallback = { _, + dateTime -> + reconfigureLobbyTimerView(dateTime) + submitLobbyChanges() + } + ) } } @@ -253,7 +282,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA } conversation!!.lobbyState = if (isChecked) Conversation.LobbyState - .LOBBY_STATE_MODERATORS_ONLY else Conversation.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS + .LOBBY_STATE_MODERATORS_ONLY else Conversation.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS if (conversation!!.lobbyTimer != null && conversation!!.lobbyTimer != java.lang.Long.MIN_VALUE && conversation!!.lobbyTimer != 0L) { startTimeView.setSummary(DateUtils.getLocalDateStringFromTimestampForLobby(conversation!!.lobbyTimer)) @@ -269,27 +298,34 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA } fun submitLobbyChanges() { - val state = if ((conversationInfoLobby.findViewById(R.id - .mp_checkable) as SwitchCompat).isChecked) 1 else 0 - ncApi.setLobbyForConversation(ApiUtils.getCredentials(conversationUser!!.username, - conversationUser.token), ApiUtils.getUrlForLobbyForConversation - (conversationUser.baseUrl, conversation!!.token), state, conversation!!.lobbyTimer) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onComplete() { - } + val state = if ( + ( + conversationInfoLobby.findViewById( + R.id.mp_checkable + ) as SwitchCompat + ).isChecked + ) 1 else 0 + ncApi.setLobbyForConversation( + ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token), + ApiUtils.getUrlForLobbyForConversation(conversationUser.baseUrl, conversation!!.token), + state, + conversation!!.lobbyTimer + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onComplete() { + } - override fun onSubscribe(d: Disposable) { - } + override fun onSubscribe(d: Disposable) { + } - override fun onNext(t: GenericOverall) { - } + override fun onNext(t: GenericOverall) { + } - override fun onError(e: Throwable) { - } - - }) + override fun onError(e: Throwable) { + } + }) } private fun showLovelyDialog(dialogId: Int, savedInstanceState: Bundle) { @@ -313,17 +349,21 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA private fun showDeleteConversationDialog(savedInstanceState: Bundle?) { if (activity != null) { LovelyStandardDialog(activity, LovelyStandardDialog.ButtonLayout.HORIZONTAL) - .setTopColorRes(R.color.nc_darkRed) - .setIcon(DisplayUtils.getTintedDrawable(context!!.resources, - R.drawable.ic_delete_black_24dp, R.color.bg_default)) - .setPositiveButtonColor(context!!.resources.getColor(R.color.nc_darkRed)) - .setTitle(R.string.nc_delete_call) - .setMessage(conversation!!.deleteWarningMessage) - .setPositiveButton(R.string.nc_delete) { deleteConversation() } - .setNegativeButton(R.string.nc_cancel, null) - .setInstanceStateHandler(ID_DELETE_CONVERSATION_DIALOG, saveStateHandler!!) - .setSavedInstanceState(savedInstanceState) - .show() + .setTopColorRes(R.color.nc_darkRed) + .setIcon( + DisplayUtils.getTintedDrawable( + context!!.resources, + R.drawable.ic_delete_black_24dp, R.color.bg_default + ) + ) + .setPositiveButtonColor(context!!.resources.getColor(R.color.nc_darkRed)) + .setTitle(R.string.nc_delete_call) + .setMessage(conversation!!.deleteWarningMessage) + .setPositiveButton(R.string.nc_delete) { deleteConversation() } + .setNegativeButton(R.string.nc_cancel, null) + .setInstanceStateHandler(ID_DELETE_CONVERSATION_DIALOG, saveStateHandler!!) + .setSavedInstanceState(savedInstanceState) + .show() } } @@ -335,8 +375,8 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA override fun onRestoreViewState(view: View, savedViewState: Bundle) { super.onRestoreViewState(view, savedViewState) if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) { - //Dialog won't be restarted automatically, so we need to call this method. - //Each dialog knows how to restore its state + // Dialog won't be restarted automatically, so we need to call this method. + // Each dialog knows how to restore its state showLovelyDialog(LovelySaveStateHandler.getSavedDialogId(savedViewState), savedViewState) } } @@ -397,27 +437,28 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA } private fun getListOfParticipants() { - ncApi.getPeersForCall(credentials, ApiUtils.getUrlForParticipants(conversationUser!!.baseUrl, conversationToken)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - participantsDisposable = d - } + ncApi.getPeersForCall( + credentials, + ApiUtils.getUrlForParticipants(conversationUser!!.baseUrl, conversationToken) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + participantsDisposable = d + } - override fun onNext(participantsOverall: ParticipantsOverall) { - handleParticipants(participantsOverall.ocs.data) - } + override fun onNext(participantsOverall: ParticipantsOverall) { + handleParticipants(participantsOverall.ocs.data) + } - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - participantsDisposable!!.dispose() - } - }) + override fun onError(e: Throwable) { + } + override fun onComplete() { + participantsDisposable!!.dispose() + } + }) } @OnClick(R.id.addParticipantsAction) @@ -430,21 +471,33 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA existingParticipantsId.add(userItem.model.userId) } - bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true); + bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true) bundle.putStringArrayList(BundleKeys.KEY_EXISTING_PARTICIPANTS, existingParticipantsId) bundle.putString(BundleKeys.KEY_TOKEN, conversation!!.token) - getRouter().pushController((RouterTransaction.with(ContactsController(bundle)) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()))) + getRouter().pushController( + ( + RouterTransaction.with( + ContactsController(bundle) + ) + .pushChangeHandler( + HorizontalChangeHandler() + ) + .popChangeHandler( + HorizontalChangeHandler() + ) + ) + ) } @OnClick(R.id.leaveConversationAction) internal fun leaveConversation() { workerData?.let { - WorkManager.getInstance().enqueue(OneTimeWorkRequest.Builder - (LeaveConversationWorker::class - .java).setInputData(it).build() + WorkManager.getInstance().enqueue( + OneTimeWorkRequest.Builder( + LeaveConversationWorker::class + .java + ).setInputData(it).build() ) popTwoLastControllers() } @@ -452,8 +505,11 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA private fun deleteConversation() { workerData?.let { - WorkManager.getInstance().enqueue(OneTimeWorkRequest.Builder - (DeleteConversationWorker::class.java).setInputData(it).build()) + WorkManager.getInstance().enqueue( + OneTimeWorkRequest.Builder( + DeleteConversationWorker::class.java + ).setInputData(it).build() + ) popTwoLastControllers() } } @@ -471,69 +527,67 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA private fun fetchRoomInfo() { ncApi.getRoom(credentials, ApiUtils.getRoom(conversationUser!!.baseUrl, conversationToken)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - roomDisposable = d + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + roomDisposable = d + } + + override fun onNext(roomOverall: RoomOverall) { + conversation = roomOverall.ocs.data + + val conversationCopy = conversation + + if (conversationCopy!!.canModerate(conversationUser)) { + addParticipantsAction.visibility = View.VISIBLE + } else { + addParticipantsAction.visibility = View.GONE } - override fun onNext(roomOverall: RoomOverall) { - conversation = roomOverall.ocs.data + if (isAttached && (!isBeingDestroyed || !isDestroyed)) { + ownOptionsCategory.visibility = View.VISIBLE - val conversationCopy = conversation + setupWebinaryView() - if (conversationCopy!!.canModerate(conversationUser)) { - addParticipantsAction.visibility = View.VISIBLE + if (!conversation!!.canLeave(conversationUser)) { + leaveConversationAction.visibility = View.GONE } else { - addParticipantsAction.visibility = View.GONE + leaveConversationAction.visibility = View.VISIBLE } - if (isAttached && (!isBeingDestroyed || !isDestroyed)) { - ownOptionsCategory.visibility = View.VISIBLE - - setupWebinaryView() - - if (!conversation!!.canLeave(conversationUser)) { - leaveConversationAction.visibility = View.GONE - } else { - leaveConversationAction.visibility = View.VISIBLE - } - - if (!conversation!!.canModerate(conversationUser)) { - deleteConversationAction.visibility = View.GONE - } else { - deleteConversationAction.visibility = View.VISIBLE - } - - if (Conversation.ConversationType.ROOM_SYSTEM == conversation!!.type) { - muteCalls.visibility = View.GONE - } - - getListOfParticipants() - - progressBar.visibility = View.GONE - - nameCategoryView.visibility = View.VISIBLE - - conversationDisplayName.text = conversation!!.displayName - - - loadConversationAvatar() - adjustNotificationLevelUI() - - notificationsPreferenceScreen.visibility = View.VISIBLE + if (!conversation!!.canModerate(conversationUser)) { + deleteConversationAction.visibility = View.GONE + } else { + deleteConversationAction.visibility = View.VISIBLE } - } - override fun onError(e: Throwable) { + if (Conversation.ConversationType.ROOM_SYSTEM == conversation!!.type) { + muteCalls.visibility = View.GONE + } - } + getListOfParticipants() - override fun onComplete() { - roomDisposable!!.dispose() + progressBar.visibility = View.GONE + + nameCategoryView.visibility = View.VISIBLE + + conversationDisplayName.text = conversation!!.displayName + + loadConversationAvatar() + adjustNotificationLevelUI() + + notificationsPreferenceScreen.visibility = View.VISIBLE } - }) + } + + override fun onError(e: Throwable) { + } + + override fun onComplete() { + roomDisposable!!.dispose() + } + }) } private fun adjustNotificationLevelUI() { @@ -543,12 +597,13 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA messageNotificationLevel.alpha = 1.0f if (conversation!!.notificationLevel != Conversation.NotificationLevel.DEFAULT) { - val stringValue: String = when (EnumNotificationLevelConverter().convertToInt(conversation!!.notificationLevel)) { - 1 -> "always" - 2 -> "mention" - 3 -> "never" - else -> "mention" - } + val stringValue: String = + when (EnumNotificationLevelConverter().convertToInt(conversation!!.notificationLevel)) { + 1 -> "always" + 2 -> "mention" + 3 -> "never" + else -> "mention" + } messageNotificationLevel.value = stringValue } else { @@ -577,22 +632,38 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA private fun loadConversationAvatar() { when (conversation!!.type) { - Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty - (conversation!!.name)) { + Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if ( + !TextUtils.isEmpty(conversation!!.name) + ) { val draweeController = Fresco.newDraweeControllerBuilder() - .setOldController(conversationAvatarImageView.controller) - .setAutoPlayAnimations(true) - .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(conversationUser!!.baseUrl, - conversation!!.name, R.dimen.avatar_size_big), conversationUser)) - .build() + .setOldController(conversationAvatarImageView.controller) + .setAutoPlayAnimations(true) + .setImageRequest( + DisplayUtils.getImageRequestForUrl( + ApiUtils.getUrlForAvatarWithName( + conversationUser!!.baseUrl, + conversation!!.name, R.dimen.avatar_size_big + ), + conversationUser + ) + ) + .build() conversationAvatarImageView.controller = draweeController } - Conversation.ConversationType.ROOM_GROUP_CALL -> conversationAvatarImageView.hierarchy.setPlaceholderImage(DisplayUtils - .getRoundedBitmapDrawableFromVectorDrawableResource(resources, - R.drawable.ic_people_group_white_24px)) - Conversation.ConversationType.ROOM_PUBLIC_CALL -> conversationAvatarImageView.hierarchy.setPlaceholderImage(DisplayUtils - .getRoundedBitmapDrawableFromVectorDrawableResource(resources, - R.drawable.ic_link_white_24px)) + Conversation.ConversationType.ROOM_GROUP_CALL -> conversationAvatarImageView.hierarchy.setPlaceholderImage( + DisplayUtils + .getRoundedBitmapDrawableFromVectorDrawableResource( + resources, + R.drawable.ic_people_group_white_24px + ) + ) + Conversation.ConversationType.ROOM_PUBLIC_CALL -> conversationAvatarImageView.hierarchy.setPlaceholderImage( + DisplayUtils + .getRoundedBitmapDrawableFromVectorDrawableResource( + resources, + R.drawable.ic_link_white_24px + ) + ) Conversation.ConversationType.ROOM_SYSTEM -> { val layers = arrayOfNulls(2) layers[0] = context.getDrawable(R.drawable.ic_launcher_background) @@ -610,13 +681,14 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA val userItem = adapter?.getItem(position) as UserItem val participant = userItem.model - if (participant.userId != conversationUser!!.userId) { var items = mutableListOf( - BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_promote)), - BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_demote)), - BasicListItemWithImage(R.drawable.ic_delete_grey600_24dp, - context.getString(R.string.nc_remove_participant)) + BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_promote)), + BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_demote)), + BasicListItemWithImage( + R.drawable.ic_delete_grey600_24dp, + context.getString(R.string.nc_remove_participant) + ) ) if (!conversation!!.canModerate(conversationUser)) { @@ -629,7 +701,6 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA } } - if (items.isNotEmpty()) { MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show { cornerRadius(res = R.dimen.corner_radius) @@ -639,38 +710,62 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA if (index == 0) { if (participant.type == Participant.ParticipantType.MODERATOR) { - ncApi.demoteModeratorToUser(credentials, ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token), participant.userId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - getListOfParticipants() - } + ncApi.demoteModeratorToUser( + credentials, + ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token), + participant.userId + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + getListOfParticipants() + } } else if (participant.type == Participant.ParticipantType.USER) { - ncApi.promoteUserToModerator(credentials, ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token), participant.userId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - getListOfParticipants() - } + ncApi.promoteUserToModerator( + credentials, + ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token), + participant.userId + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + getListOfParticipants() + } } } else if (index == 1) { if (participant.type == Participant.ParticipantType.GUEST || - participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK) { - ncApi.removeParticipantFromConversation(credentials, ApiUtils.getUrlForRemovingParticipantFromConversation(conversationUser.baseUrl, conversation!!.token, true), participant.sessionId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - getListOfParticipants() - } - + participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK + ) { + ncApi.removeParticipantFromConversation( + credentials, + ApiUtils.getUrlForRemovingParticipantFromConversation( + conversationUser.baseUrl, + conversation!!.token, + true + ), + participant.sessionId + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + getListOfParticipants() + } } else { - ncApi.removeParticipantFromConversation(credentials, ApiUtils.getUrlForRemovingParticipantFromConversation(conversationUser.baseUrl, conversation!!.token, false), participant.userId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - getListOfParticipants() - // get participants again - } + ncApi.removeParticipantFromConversation( + credentials, + ApiUtils.getUrlForRemovingParticipantFromConversation( + conversationUser.baseUrl, + conversation!!.token, + false + ), + participant.userId + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + getListOfParticipants() + // get participants again + } } } } @@ -678,7 +773,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA } } - return true; + return true } companion object { @@ -709,7 +804,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA } return left.model.displayName.toLowerCase(Locale.ROOT).compareTo( - right.model.displayName.toLowerCase(Locale.ROOT) + right.model.displayName.toLowerCase(Locale.ROOT) ) } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/ButterKnifeController.kt b/app/src/main/java/com/nextcloud/talk/controllers/base/ButterKnifeController.kt index e6191f49f..8f813a4b4 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/base/ButterKnifeController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/base/ButterKnifeController.kt @@ -53,5 +53,4 @@ abstract class ButterKnifeController : Controller { unbinder!!.unbind() unbinder = null } - } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/BasicListItemWithImage.kt b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/BasicListItemWithImage.kt index 8d75b08ba..80399135b 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/BasicListItemWithImage.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/BasicListItemWithImage.kt @@ -29,10 +29,11 @@ interface ListItemWithImage { } data class BasicListItemWithImage( - @DrawableRes val iconRes: Int, - override val title: String) : ListItemWithImage { + @DrawableRes val iconRes: Int, + override val title: String +) : ListItemWithImage { override fun populateIcon(imageView: ImageView) { imageView.setImageResource(iconRes) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/ListIconDialogAdapter.kt b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/ListIconDialogAdapter.kt index 43c66fd1a..fb67eed04 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/ListIconDialogAdapter.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/ListIconDialogAdapter.kt @@ -23,7 +23,6 @@ package com.nextcloud.talk.controllers.bottomsheet.items import android.view.View import android.view.ViewGroup import android.widget.ImageView -import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.WhichButton @@ -34,14 +33,14 @@ import com.afollestad.materialdialogs.internal.rtl.RtlTextView import com.afollestad.materialdialogs.list.getItemSelector import com.afollestad.materialdialogs.utils.MDUtil.inflate import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor -import com.google.android.material.textview.MaterialTextView import com.nextcloud.talk.R private const val KEY_ACTIVATED_INDEX = "activated_index" internal class ListItemViewHolder( - itemView: View, - private val adapter: ListIconDialogAdapter<*>) : RecyclerView.ViewHolder(itemView), View.OnClickListener { + itemView: View, + private val adapter: ListIconDialogAdapter<*> +) : RecyclerView.ViewHolder(itemView), View.OnClickListener { init { itemView.setOnClickListener(this) } @@ -53,11 +52,12 @@ internal class ListItemViewHolder( } internal class ListIconDialogAdapter( - private var dialog: MaterialDialog, - private var items: List, - disabledItems: IntArray?, - private var waitForPositiveButton: Boolean, - private var selection: ListItemListener) : RecyclerView.Adapter(), DialogAdapter> { + private var dialog: MaterialDialog, + private var items: List, + disabledItems: IntArray?, + private var waitForPositiveButton: Boolean, + private var selection: ListItemListener +) : RecyclerView.Adapter(), DialogAdapter> { private var disabledIndices: IntArray = disabledItems ?: IntArray(0) @@ -81,12 +81,13 @@ internal class ListIconDialogAdapter( } override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int): ListItemViewHolder { + parent: ViewGroup, + viewType: Int + ): ListItemViewHolder { val listItemView: View = parent.inflate(dialog.windowContext, R.layout.menu_item_sheet) val viewHolder = ListItemViewHolder( - itemView = listItemView, - adapter = this + itemView = listItemView, + adapter = this ) viewHolder.titleView.maybeSetTextColor(dialog.windowContext, R.attr.md_color_content) return viewHolder @@ -95,8 +96,9 @@ internal class ListIconDialogAdapter( override fun getItemCount() = items.size override fun onBindViewHolder( - holder: ListItemViewHolder, - position: Int) { + holder: ListItemViewHolder, + position: Int + ) { holder.itemView.isEnabled = !disabledIndices.contains(position) val currentItem = items[position] @@ -121,8 +123,9 @@ internal class ListIconDialogAdapter( } override fun replaceItems( - items: List, - listener: ListItemListener) { + items: List, + listener: ListItemListener + ) { this.items = items if (listener != null) { this.selection = listener diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/MagicBottomSheets.kt b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/MagicBottomSheets.kt index ab2be552a..6d70f3f8c 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/MagicBottomSheets.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/items/MagicBottomSheets.kt @@ -28,37 +28,40 @@ import com.afollestad.materialdialogs.list.customListAdapter import com.afollestad.materialdialogs.list.getListAdapter typealias ListItemListener = - ((dialog: MaterialDialog, index: Int, item: IT) -> Unit)? + ((dialog: MaterialDialog, index: Int, item: IT) -> Unit)? -@CheckResult fun MaterialDialog.listItemsWithImage( - items: List, - disabledIndices: IntArray? = null, - waitForPositiveButton: Boolean = true, - selection: ListItemListener = null): MaterialDialog { +@CheckResult +fun MaterialDialog.listItemsWithImage( + items: List, + disabledIndices: IntArray? = null, + waitForPositiveButton: Boolean = true, + selection: ListItemListener = null +): MaterialDialog { if (getListAdapter() != null) { return updateListItemsWithImage( - items = items, - disabledIndices = disabledIndices + items = items, + disabledIndices = disabledIndices ) } val layoutManager = LinearLayoutManager(windowContext) return customListAdapter( - adapter = ListIconDialogAdapter( - dialog = this, - items = items, - disabledItems = disabledIndices, - waitForPositiveButton = waitForPositiveButton, - selection = selection - ), - layoutManager = layoutManager + adapter = ListIconDialogAdapter( + dialog = this, + items = items, + disabledItems = disabledIndices, + waitForPositiveButton = waitForPositiveButton, + selection = selection + ), + layoutManager = layoutManager ) } fun MaterialDialog.updateListItemsWithImage( - items: List, - disabledIndices: IntArray? = null): MaterialDialog { + items: List, + disabledIndices: IntArray? = null +): MaterialDialog { val adapter = getListAdapter() check(adapter != null) { "updateGridItems(...) can't be used before you've created a bottom sheet grid dialog." diff --git a/app/src/main/java/com/nextcloud/talk/events/CallNotificationClick.kt b/app/src/main/java/com/nextcloud/talk/events/CallNotificationClick.kt index c883ee04a..ad8c52fde 100644 --- a/app/src/main/java/com/nextcloud/talk/events/CallNotificationClick.kt +++ b/app/src/main/java/com/nextcloud/talk/events/CallNotificationClick.kt @@ -20,6 +20,4 @@ package com.nextcloud.talk.events -class CallNotificationClick { - -} \ No newline at end of file +class CallNotificationClick diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt index 38be955fa..f7703623c 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt @@ -33,7 +33,11 @@ import android.provider.ContactsContract import android.util.Log import androidx.core.content.ContextCompat import androidx.core.os.ConfigurationCompat -import androidx.work.* +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters import autodagger.AutoInjector import com.bluelinelabs.conductor.Controller import com.google.gson.Gson @@ -54,10 +58,9 @@ import okhttp3.MediaType import okhttp3.RequestBody import javax.inject.Inject - @AutoInjector(NextcloudTalkApplication::class) class ContactAddressBookWorker(val context: Context, workerParameters: WorkerParameters) : - Worker(context, workerParameters) { + Worker(context, workerParameters) { @Inject lateinit var ncApi: NcApi @@ -85,7 +88,7 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar } val deleteAll = inputData.getBoolean(DELETE_ALL, false) - if(deleteAll){ + if (deleteAll) { deleteAllLinkedAccounts() return Result.success() } @@ -99,7 +102,7 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar } } - if(AccountManager.get(context).getAccountsByType(accountType).isEmpty()){ + if (AccountManager.get(context).getAccountsByType(accountType).isEmpty()) { AccountManager.get(context).addAccountExplicitly(Account(accountName, accountType), "", null) } else { Log.d(TAG, "Account already exists") @@ -107,7 +110,7 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar val deviceContactsWithNumbers = collectContactsWithPhoneNumbersFromDevice() - if(deviceContactsWithNumbers.isNotEmpty()){ + if (deviceContactsWithNumbers.isNotEmpty()) { val currentLocale = ConfigurationCompat.getLocales(context.resources.configuration)[0].country val map = mutableMapOf() @@ -117,28 +120,29 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar val json = Gson().toJson(map) ncApi.searchContactsByPhoneNumber( - ApiUtils.getCredentials(currentUser.username, currentUser.token), - ApiUtils.getUrlForSearchByNumber(currentUser.baseUrl), - RequestBody.create(MediaType.parse("application/json"), json)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onComplete() { - } + ApiUtils.getCredentials(currentUser.username, currentUser.token), + ApiUtils.getUrlForSearchByNumber(currentUser.baseUrl), + RequestBody.create(MediaType.parse("application/json"), json) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onComplete() { + } - override fun onSubscribe(d: Disposable) { - } + override fun onSubscribe(d: Disposable) { + } - override fun onNext(foundContacts: ContactsByNumberOverall) { - val contactsWithAssociatedPhoneNumbers = foundContacts.ocs.map - deleteLinkedAccounts(contactsWithAssociatedPhoneNumbers) - createLinkedAccounts(contactsWithAssociatedPhoneNumbers) - } + override fun onNext(foundContacts: ContactsByNumberOverall) { + val contactsWithAssociatedPhoneNumbers = foundContacts.ocs.map + deleteLinkedAccounts(contactsWithAssociatedPhoneNumbers) + createLinkedAccounts(contactsWithAssociatedPhoneNumbers) + } - override fun onError(e: Throwable) { - Log.e(javaClass.simpleName, "Failed to searchContactsByPhoneNumber", e) - } - }) + override fun onError(e: Throwable) { + Log.e(javaClass.simpleName, "Failed to searchContactsByPhoneNumber", e) + } + }) } // store timestamp @@ -151,11 +155,11 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar val deviceContactsWithNumbers: MutableMap> = mutableMapOf() val contactCursor = context.contentResolver.query( - ContactsContract.Contacts.CONTENT_URI, - null, - null, - null, - null + ContactsContract.Contacts.CONTENT_URI, + null, + null, + null, + null ) if (contactCursor != null) { @@ -163,7 +167,8 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar contactCursor.moveToFirst() for (i in 0 until contactCursor.count) { val id = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts._ID)) - val lookup = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)) + val lookup = + contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)) deviceContactsWithNumbers[lookup] = getPhoneNumbersFromDeviceContact(id) contactCursor.moveToNext() } @@ -178,39 +183,50 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar Log.d(TAG, "deleteLinkedAccount") fun deleteLinkedAccount(id: String) { val rawContactUri = ContactsContract.RawContacts.CONTENT_URI - .buildUpon() - .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) - .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) - .build() - val count = context.contentResolver.delete(rawContactUri, ContactsContract.RawContacts.CONTACT_ID + " " + - "LIKE \"" + id + "\"", null) - Log.d(TAG, "deleted $count linked accounts for id $id") - } - - val rawContactUri = ContactsContract.Data.CONTENT_URI .buildUpon() .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) - .appendQueryParameter(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat") .build() + val count = context.contentResolver.delete( + rawContactUri, + ContactsContract.RawContacts.CONTACT_ID + " " + "LIKE \"" + id + "\"", + null + ) + Log.d(TAG, "deleted $count linked accounts for id $id") + } + + val rawContactUri = ContactsContract.Data.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + .appendQueryParameter( + ContactsContract.Data.MIMETYPE, + "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" + ) + .build() val rawContactsCursor = context.contentResolver.query( - rawContactUri, - null, - null, - null, - null + rawContactUri, + null, + null, + null, + null ) if (rawContactsCursor != null) { if (rawContactsCursor.count > 0) { while (rawContactsCursor.moveToNext()) { - val lookupKey = rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY)) - val contactId = rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.CONTACT_ID)) + val lookupKey = + rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY)) + val contactId = + rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.CONTACT_ID)) - if (contactsWithAssociatedPhoneNumbers == null || !contactsWithAssociatedPhoneNumbers.containsKey(lookupKey)) { + if (contactsWithAssociatedPhoneNumbers == null || !contactsWithAssociatedPhoneNumbers.containsKey( + lookupKey + ) + ) { deleteLinkedAccount(contactId) } } @@ -222,25 +238,29 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar } private fun createLinkedAccounts(contactsWithAssociatedPhoneNumbers: Map?) { - fun hasLinkedAccount(id: String) : Boolean { + fun hasLinkedAccount(id: String): Boolean { var hasLinkedAccount = false - val where = ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?" + val where = + ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?" val params = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id) val rawContactUri = ContactsContract.Data.CONTENT_URI - .buildUpon() - .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) - .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) - .appendQueryParameter(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat") - .build() + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + .appendQueryParameter( + ContactsContract.Data.MIMETYPE, + "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" + ) + .build() val rawContactsCursor = context.contentResolver.query( - rawContactUri, - null, - where, - params, - null + rawContactUri, + null, + where, + params, + null ) if (rawContactsCursor != null) { @@ -259,18 +279,19 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar val lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey) val lookupContactUri = ContactsContract.Contacts.lookupContact(context.contentResolver, lookupUri) val contactCursor = context.contentResolver.query( - lookupContactUri, - null, - null, - null, - null) + lookupContactUri, + null, + null, + null, + null + ) if (contactCursor != null) { if (contactCursor.count > 0) { contactCursor.moveToFirst() val id = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts._ID)) - if(hasLinkedAccount(id)){ + if (hasLinkedAccount(id)) { return } @@ -285,34 +306,60 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar val rawContactsUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon().build() val dataUri = ContactsContract.Data.CONTENT_URI.buildUpon().build() - ops.add(ContentProviderOperation + ops.add( + ContentProviderOperation .newInsert(rawContactsUri) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) - .withValue(ContactsContract.RawContacts.AGGREGATION_MODE, - ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT) + .withValue( + ContactsContract.RawContacts.AGGREGATION_MODE, + ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT + ) .withValue(ContactsContract.RawContacts.SYNC2, cloudId) - .build()) - ops.add(ContentProviderOperation + .build() + ) + ops.add( + ContentProviderOperation .newInsert(dataUri) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue( + ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + ) .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, numbers[0]) - .build()) - ops.add(ContentProviderOperation + .build() + ) + ops.add( + ContentProviderOperation .newInsert(dataUri) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + .withValue( + ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + ) .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName) - .build()) - ops.add(ContentProviderOperation + .build() + ) + ops.add( + ContentProviderOperation .newInsert(dataUri) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) - .withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat") + .withValue( + ContactsContract.Data.MIMETYPE, + "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" + ) .withValue(ContactsContract.Data.DATA1, cloudId) - .withValue(ContactsContract.Data.DATA2, String.format(context.resources.getString(R - .string.nc_phone_book_integration_chat_via), accountName)) - .build()) + .withValue( + ContactsContract.Data.DATA2, + String.format( + context.resources.getString( + R.string.nc_phone_book_integration_chat_via + ), + accountName + ) + ) + .build() + ) try { context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) @@ -322,8 +369,11 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar Log.e(javaClass.simpleName, "", e) } - Log.d(TAG, "added new entry for contact $displayName (cloudId: $cloudId | lookupKey: $lookupKey" + - " | id: $id)") + Log.d( + TAG, + "added new entry for contact $displayName (cloudId: $cloudId | lookupKey: $lookupKey" + + " | id: $id)" + ) } contactCursor.close() } @@ -341,18 +391,21 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar } private fun getDisplayNameFromDeviceContact(id: String?): String? { - var displayName:String? = null - val whereName = ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?" + var displayName: String? = null + val whereName = + ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?" val whereNameParams = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id) val nameCursor = context.contentResolver.query( - ContactsContract.Data.CONTENT_URI, - null, - whereName, - whereNameParams, - ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME) + ContactsContract.Data.CONTENT_URI, + null, + whereName, + whereNameParams, + ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME + ) if (nameCursor != null) { while (nameCursor.moveToNext()) { - displayName = nameCursor.getString(nameCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)) + displayName = + nameCursor.getString(nameCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)) } nameCursor.close() } @@ -362,11 +415,12 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar private fun getPhoneNumbersFromDeviceContact(id: String?): MutableList { val numbers = mutableListOf() val phonesNumbersCursor = context.contentResolver.query( - ContactsContract.CommonDataKinds.Phone.CONTENT_URI, - null, - ContactsContract.Data.CONTACT_ID + " = " + id, - null, - null) + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + null, + ContactsContract.Data.CONTACT_ID + " = " + id, + null, + null + ) if (phonesNumbersCursor != null) { while (phonesNumbersCursor.moveToNext()) { @@ -374,7 +428,7 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar } phonesNumbersCursor.close() } - if(numbers.size > 0){ + if (numbers.size > 0) { Log.d(TAG, "Found ${numbers.size} phone numbers for contact with id $id") } return numbers @@ -382,11 +436,11 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar fun deleteAllLinkedAccounts() { val rawContactUri = ContactsContract.RawContacts.CONTENT_URI - .buildUpon() - .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) - .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) - .build() + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + .build() context.contentResolver.delete(rawContactUri, null, null) Log.d(TAG, "deleted all linked accounts") } @@ -398,42 +452,63 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar const val DELETE_ALL = "DELETE_ALL" fun run(context: Context) { - if (ContextCompat.checkSelfPermission(context, - Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED && - ContextCompat.checkSelfPermission(context, - Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.WRITE_CONTACTS + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_CONTACTS + ) == PackageManager.PERMISSION_GRANTED + ) { WorkManager - .getInstance() - .enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java) - .setInputData(Data.Builder().putBoolean(KEY_FORCE, false).build()) - .build()) + .getInstance() + .enqueue( + OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java) + .setInputData(Data.Builder().putBoolean(KEY_FORCE, false).build()) + .build() + ) } } fun checkPermission(controller: Controller, context: Context): Boolean { - if (ContextCompat.checkSelfPermission(context, - Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(context, - Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { - controller.requestPermissions(arrayOf(Manifest.permission.WRITE_CONTACTS, - Manifest.permission.READ_CONTACTS), REQUEST_PERMISSION) + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.WRITE_CONTACTS + ) != PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_CONTACTS + ) != PackageManager.PERMISSION_GRANTED + ) { + controller.requestPermissions( + arrayOf( + Manifest.permission.WRITE_CONTACTS, + Manifest.permission.READ_CONTACTS + ), + REQUEST_PERMISSION + ) return false } else { WorkManager - .getInstance() - .enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java) - .setInputData(Data.Builder().putBoolean(KEY_FORCE, true).build()) - .build()) + .getInstance() + .enqueue( + OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java) + .setInputData(Data.Builder().putBoolean(KEY_FORCE, true).build()) + .build() + ) return true } } fun deleteAll() { - WorkManager - .getInstance() - .enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java) - .setInputData(Data.Builder().putBoolean(DELETE_ALL, true).build()) - .build()) + WorkManager + .getInstance() + .enqueue( + OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java) + .setInputData(Data.Builder().putBoolean(DELETE_ALL, true).build()) + .build() + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt index a01046c6e..08584e431 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt @@ -33,13 +33,16 @@ import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.database.user.UserUtils import com.nextcloud.talk.utils.preferences.AppPreferences import okhttp3.ResponseBody -import java.io.* +import java.io.BufferedInputStream +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream import javax.inject.Inject - @AutoInjector(NextcloudTalkApplication::class) class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerParameters) : - Worker(context, workerParameters) { + Worker(context, workerParameters) { private var totalFileSize: Int = -1 @@ -86,8 +89,9 @@ class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerPa private fun downloadFile(currentUser: UserEntity, url: String, fileName: String): Result { val downloadCall = ncApi.downloadFile( - ApiUtils.getCredentials(currentUser.username, currentUser.token), - url) + ApiUtils.getCredentials(currentUser.username, currentUser.token), + url + ) return executeDownload(downloadCall.execute().body(), fileName) } @@ -152,6 +156,5 @@ class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerPa const val KEY_FILE_SIZE = "KEY_FILE_SIZE" const val PROGRESS = "PROGRESS" const val SUCCESS = "SUCCESS" - } } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt index 33a97451a..4eb4a9157 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt @@ -23,7 +23,11 @@ package com.nextcloud.talk.jobs import android.content.Context import android.net.Uri import android.util.Log -import androidx.work.* +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters import autodagger.AutoInjector import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication @@ -46,13 +50,12 @@ import retrofit2.Response import java.io.File import java.io.FileOutputStream import java.io.InputStream -import java.util.* +import java.util.ArrayList import javax.inject.Inject - @AutoInjector(NextcloudTalkApplication::class) class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerParameters) : - Worker(context, workerParameters) { + Worker(context, workerParameters) { @Inject lateinit var ncApi: NcApi @@ -107,31 +110,37 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa return requestBody } - private fun uploadFile(currentUser: UserEntity, ncTargetpath: String?, filename: String, roomToken: String?, - requestBody: RequestBody?, sourcefileUri: Uri) { + private fun uploadFile( + currentUser: UserEntity, + ncTargetpath: String?, + filename: String, + roomToken: String?, + requestBody: RequestBody?, + sourcefileUri: Uri + ) { ncApi.uploadFile( - ApiUtils.getCredentials(currentUser.username, currentUser.token), - ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, ncTargetpath, filename), - requestBody + ApiUtils.getCredentials(currentUser.username, currentUser.token), + ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, ncTargetpath, filename), + requestBody ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer> { - override fun onSubscribe(d: Disposable) { - } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer> { + override fun onSubscribe(d: Disposable) { + } - override fun onNext(t: Response) { - } + override fun onNext(t: Response) { + } - override fun onError(e: Throwable) { - Log.e(TAG, "failed to upload file $filename") - } + override fun onError(e: Throwable) { + Log.e(TAG, "failed to upload file $filename") + } - override fun onComplete() { - shareFile(roomToken, currentUser, ncTargetpath, filename) - copyFileToCache(sourcefileUri, filename) - } - }) + override fun onComplete() { + shareFile(roomToken, currentUser, ncTargetpath, filename) + copyFileToCache(sourcefileUri, filename) + } + }) } private fun copyFileToCache(sourceFileUri: Uri, filename: String) { @@ -151,13 +160,13 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa paths.add("$ncTargetpath/$filename") val data = Data.Builder() - .putLong(KEY_INTERNAL_USER_ID, currentUser.id) - .putString(KEY_ROOM_TOKEN, roomToken) - .putStringArray(KEY_FILE_PATHS, paths.toTypedArray()) - .build() + .putLong(KEY_INTERNAL_USER_ID, currentUser.id) + .putString(KEY_ROOM_TOKEN, roomToken) + .putStringArray(KEY_FILE_PATHS, paths.toTypedArray()) + .build() val shareWorker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java) - .setInputData(data) - .build() + .setInputData(data) + .build() WorkManager.getInstance().enqueue(shareWorker) } @@ -167,4 +176,4 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa const val NC_TARGETPATH = "NC_TARGETPATH" const val ROOM_TOKEN = "ROOM_TOKEN" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt index 24ee28e11..9e060409f 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt @@ -25,7 +25,7 @@ package com.nextcloud.talk.models.json.chat class ChatUtils { companion object { fun getParsedMessage(message: String?, messageParameters: HashMap>?): - String? { + String? { var resultMessage = message if (messageParameters != null && messageParameters.size > 0) { for (key in messageParameters.keys) { @@ -46,4 +46,3 @@ class ChatUtils { } } } - diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt index cc791068c..8bb830c8d 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt @@ -22,11 +22,28 @@ package com.nextcloud.talk.models.json.converters import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter import com.nextcloud.talk.models.json.chat.ChatMessage - -import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.* +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_ENDED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_JOINED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_LEFT +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_STARTED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CONVERSATION_CREATED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CONVERSATION_RENAMED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.DUMMY +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.FILE_SHARED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUESTS_ALLOWED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUESTS_DISALLOWED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_NONE +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_NON_MODERATORS +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_OPEN_TO_EVERYONE +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MODERATOR_DEMOTED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MODERATOR_PROMOTED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PARENT_MESSAGE_DELETED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_REMOVED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_SET +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_ADDED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_REMOVED /* - conversation_created - {actor} created the conversation conversation_renamed - {actor} renamed the conversation from "foo" to "bar" call_joined - {actor} joined the call @@ -40,7 +57,6 @@ import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.* user_removed - {actor} removed {user} from the conversation moderator_promoted - {actor} promoted {user} to moderator moderator_demoted - {actor} demoted {user} from moderator - */ class EnumSystemMessageTypeConverter : StringBasedTypeConverter() { override fun getFromString(string: String): ChatMessage.SystemMessageType { diff --git a/app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt index e7bfd5c25..714eab4f5 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt +++ b/app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt @@ -20,12 +20,10 @@ package com.nextcloud.talk.receivers -import android.app.NotificationChannelGroup import android.app.NotificationManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build import android.util.Log @@ -34,7 +32,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.database.user.UserUtils import com.nextcloud.talk.utils.preferences.AppPreferences - import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -50,16 +47,20 @@ class PackageReplacedReceiver : BroadcastReceiver() { NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) if (intent != null && intent.action != null && - intent.action == "android.intent.action.MY_PACKAGE_REPLACED") { + intent.action == "android.intent.action.MY_PACKAGE_REPLACED" + ) { try { val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) if (packageInfo.versionCode > 43 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notificationManager = context.getSystemService(Context - .NOTIFICATION_SERVICE) as NotificationManager + val notificationManager = context.getSystemService( + Context + .NOTIFICATION_SERVICE + ) as NotificationManager if (!appPreferences.isNotificationChannelUpgradedToV2) { - for (notificationChannelGroup in notificationManager - .notificationChannelGroups) { + for ( + notificationChannelGroup in notificationManager.notificationChannelGroups + ) { notificationManager.deleteNotificationChannelGroup(notificationChannelGroup.id) } @@ -80,7 +81,6 @@ class PackageReplacedReceiver : BroadcastReceiver() { } catch (e: PackageManager.NameNotFoundException) { Log.e(TAG, "Failed to fetch package info") } - } } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt index 569fffbc2..921f2f2ab 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt @@ -32,7 +32,6 @@ import com.nextcloud.talk.R import com.nextcloud.talk.components.filebrowser.controllers.BrowserController import com.nextcloud.talk.controllers.ChatController - class AttachmentDialog(val activity: Activity, var chatController: ChatController) : BottomSheetDialog(activity) { @BindView(R.id.txt_attach_file_from_local) @@ -54,7 +53,7 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle var serverName = chatController.conversationUser?.serverName attachFromCloud?.text = chatController.resources?.let { - if(serverName.isNullOrEmpty()){ + if (serverName.isNullOrEmpty()) { serverName = it.getString(R.string.nc_server_product_name) } String.format(it.getString(R.string.nc_upload_from_cloud), serverName) diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt index 207c3d478..d573aa255 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt @@ -30,12 +30,13 @@ import com.nextcloud.talk.R import com.nextcloud.talk.controllers.ProfileController import com.nextcloud.talk.models.json.userprofile.Scope - -class ScopeDialog(con: Context, - private val userInfoAdapter: ProfileController.UserInfoAdapter, - private val field: ProfileController.Field, - private val position: Int) : - BottomSheetDialog(con) { +class ScopeDialog( + con: Context, + private val userInfoAdapter: ProfileController.UserInfoAdapter, + private val field: ProfileController.Field, + private val position: Int +) : + BottomSheetDialog(con) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val view = layoutInflater.inflate(R.layout.dialog_scope, null) diff --git a/app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt index 83e12c9df..9fc105c81 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt @@ -32,7 +32,8 @@ import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.models.ImportAccount import com.nextcloud.talk.models.database.UserEntity -import java.util.* +import java.util.ArrayList +import java.util.Arrays object AccountUtils { @@ -60,14 +61,15 @@ object AccountUtils { break } } else { - if (internalUserEntity.username == importAccount.username && (internalUserEntity - .baseUrl == "http://" + importAccount.baseUrl || - internalUserEntity.baseUrl == "https://" + importAccount - .baseUrl)) { + if (internalUserEntity.username == importAccount.username && + ( + internalUserEntity.baseUrl == "http://" + importAccount.baseUrl || + internalUserEntity.baseUrl == "https://" + importAccount.baseUrl + ) + ) { accountFound = true break } - } } else { accountFound = true @@ -88,8 +90,12 @@ object AccountUtils { val packageManager = context.packageManager var appName = "" try { - appName = packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName, - PackageManager.GET_META_DATA)) as String + appName = packageManager.getApplicationLabel( + packageManager.getApplicationInfo( + packageName, + PackageManager.GET_META_DATA + ) + ) as String } catch (e: PackageManager.NameNotFoundException) { Log.e(TAG, "Failed to get app name based on package") } @@ -103,7 +109,10 @@ object AccountUtils { val packageInfo = pm.getPackageInfo(context.getString(R.string.nc_import_accounts_from), 0) if (packageInfo.versionCode >= 30060151) { val ownSignatures = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES).signatures - val filesAppSignatures = pm.getPackageInfo(context.getString(R.string.nc_import_accounts_from), PackageManager.GET_SIGNATURES).signatures + val filesAppSignatures = pm.getPackageInfo( + context.getString(R.string.nc_import_accounts_from), + PackageManager.GET_SIGNATURES + ).signatures if (Arrays.equals(ownSignatures, filesAppSignatures)) { val accMgr = AccountManager.get(context) @@ -118,7 +127,7 @@ object AccountUtils { } } } catch (appNotFoundException: PackageManager.NameNotFoundException) { - + // ignore } return false @@ -146,4 +155,3 @@ object AccountUtils { return ImportAccount(username, password, urlString) } } - diff --git a/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt b/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt index e2845a0b7..0f64722e2 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt @@ -27,7 +27,13 @@ import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.nextcloud.talk.controllers.ChatController object ConductorRemapping { - fun remapChatController(router: Router, internalUserId: Long, roomTokenOrId: String, bundle: Bundle, replaceTop: Boolean) { + fun remapChatController( + router: Router, + internalUserId: Long, + roomTokenOrId: String, + bundle: Bundle, + replaceTop: Boolean + ) { val tag = "$internalUserId@$roomTokenOrId" if (router.getControllerWithTag(tag) != null) { val backstack = router.backstack @@ -44,13 +50,17 @@ object ConductorRemapping { router.setBackstack(backstack, HorizontalChangeHandler()) } else { if (!replaceTop) { - router.pushController(RouterTransaction.with(ChatController(bundle)) + router.pushController( + RouterTransaction.with(ChatController(bundle)) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()).tag(tag)) + .popChangeHandler(HorizontalChangeHandler()).tag(tag) + ) } else { - router.replaceTopController(RouterTransaction.with(ChatController(bundle)) + router.replaceTopController( + RouterTransaction.with(ChatController(bundle)) .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()).tag(tag)) + .popChangeHandler(HorizontalChangeHandler()).tag(tag) + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt index 20fceb297..afa477884 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt @@ -21,8 +21,9 @@ package com.nextcloud.talk.utils import java.text.DateFormat -import java.util.* - +import java.util.Calendar +import java.util.Date +import java.util.Locale object DateUtils { fun getLocalDateTimeStringFromTimestamp(timestamp: Long): String { @@ -30,14 +31,16 @@ object DateUtils { val tz = cal.timeZone /* date formatter in local timezone */ - val format = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT, Locale - .getDefault()) + val format = DateFormat.getDateTimeInstance( + DateFormat.DEFAULT, DateFormat.SHORT, + Locale.getDefault() + ) format.timeZone = tz return format.format(Date(timestamp)) } fun getLocalDateStringFromTimestampForLobby(timestamp: Long): String { - return getLocalDateTimeStringFromTimestamp(timestamp * 1000); + return getLocalDateTimeStringFromTimestamp(timestamp * 1000) } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/DrawableUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DrawableUtils.kt index 14afd4298..2deafa3e5 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DrawableUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/DrawableUtils.kt @@ -21,12 +21,10 @@ package com.nextcloud.talk.utils import com.nextcloud.talk.R - import java.util.HashMap object DrawableUtils { - fun getDrawableResourceIdForMimeType(mimetype: String): Int { var localMimetype = mimetype val drawableMap = HashMap() @@ -55,15 +53,20 @@ object DrawableUtils { drawableMap["application/vnd.google-earth.kmz"] = R.drawable.ic_mimetype_location drawableMap["application/vnd.ms-excel"] = R.drawable.ic_mimetype_x_office_spreadsheet drawableMap["application/vnd.ms-excel.addin.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_spreadsheet - drawableMap["application/vnd.ms-excel.sheet.binary.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.ms-excel.sheet.binary.macroEnabled.12"] = + R.drawable.ic_mimetype_x_office_spreadsheet drawableMap["application/vnd.ms-excel.sheet.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_spreadsheet drawableMap["application/vnd.ms-excel.template.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_spreadsheet drawableMap["application/vnd.ms-fontobject"] = R.drawable.ic_mimetype_image drawableMap["application/vnd.ms-powerpoint"] = R.drawable.ic_mimetype_x_office_presentation - drawableMap["application/vnd.ms-powerpoint.addin.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_presentation - drawableMap["application/vnd.ms-powerpoint.presentation.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_presentation - drawableMap["application/vnd.ms-powerpoint.slideshow.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_presentation - drawableMap["application/vnd.ms-powerpoint.template.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.ms-powerpoint.addin.macroEnabled.12"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.ms-powerpoint.presentation.macroEnabled.12"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.ms-powerpoint.slideshow.macroEnabled.12"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.ms-powerpoint.template.macroEnabled.12"] = + R.drawable.ic_mimetype_x_office_presentation drawableMap["application/vnd.ms-visio.drawing.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_document drawableMap["application/vnd.ms-visio.drawing"] = R.drawable.ic_mimetype_x_office_document drawableMap["application/vnd.ms-visio.stencil.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_document @@ -72,20 +75,29 @@ object DrawableUtils { drawableMap["application/vnd.ms-visio.template"] = R.drawable.ic_mimetype_x_office_document drawableMap["application/vnd.ms-word.template.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_document drawableMap["application/vnd.oasis.opendocument.presentation"] = R.drawable.ic_mimetype_x_office_presentation - drawableMap["application/vnd.oasis.opendocument.presentation-template"] = R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.oasis.opendocument.presentation-template"] = + R.drawable.ic_mimetype_x_office_presentation drawableMap["application/vnd.oasis.opendocument.spreadsheet"] = R.drawable.ic_mimetype_x_office_spreadsheet - drawableMap["application/vnd.oasis.opendocument.spreadsheet-template"] = R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.oasis.opendocument.spreadsheet-template"] = + R.drawable.ic_mimetype_x_office_spreadsheet drawableMap["application/vnd.oasis.opendocument.text"] = R.drawable.ic_mimetype_x_office_document drawableMap["application/vnd.oasis.opendocument.text-master"] = R.drawable.ic_mimetype_x_office_document drawableMap["application/vnd.oasis.opendocument.text-template"] = R.drawable.ic_mimetype_x_office_document drawableMap["application/vnd.oasis.opendocument.text-web"] = R.drawable.ic_mimetype_x_office_document - drawableMap["application/vnd.openxmlformats-officedocument.presentationml.presentation"] = R.drawable.ic_mimetype_x_office_presentation - drawableMap["application/vnd.openxmlformats-officedocument.presentationml.slideshow"] = R.drawable.ic_mimetype_x_office_presentation - drawableMap["application/vnd.openxmlformats-officedocument.presentationml.template"] = R.drawable.ic_mimetype_x_office_presentation - drawableMap["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] = R.drawable.ic_mimetype_x_office_spreadsheet - drawableMap["application/vnd.openxmlformats-officedocument.spreadsheetml.template"] = R.drawable.ic_mimetype_x_office_spreadsheet - drawableMap["application/vnd.openxmlformats-officedocument.wordprocessingml.document"] = R.drawable.ic_mimetype_x_office_document - drawableMap["application/vnd.openxmlformats-officedocument.wordprocessingml.template"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.openxmlformats-officedocument.presentationml.presentation"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.openxmlformats-officedocument.presentationml.slideshow"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.openxmlformats-officedocument.presentationml.template"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] = + R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.openxmlformats-officedocument.spreadsheetml.template"] = + R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.openxmlformats-officedocument.wordprocessingml.document"] = + R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.openxmlformats-officedocument.wordprocessingml.template"] = + R.drawable.ic_mimetype_x_office_document drawableMap["application/vnd.visio"] = R.drawable.ic_mimetype_x_office_document drawableMap["application/vnd.wordperfect"] = R.drawable.ic_mimetype_x_office_document drawableMap["application/x-7z-compressed"] = R.drawable.ic_mimetype_package_x_generic diff --git a/app/src/main/java/com/nextcloud/talk/utils/LoggingUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/LoggingUtils.kt index 774a906b1..d3ca89a83 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/LoggingUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/LoggingUtils.kt @@ -29,7 +29,7 @@ import com.nextcloud.talk.BuildConfig import java.io.FileNotFoundException import java.io.IOException import java.text.SimpleDateFormat -import java.util.* +import java.util.Date object LoggingUtils { fun writeLogEntryToFile(context: Context, logEntry: String) { @@ -38,8 +38,10 @@ object LoggingUtils { val logEntryWithDateTime = dateFormat.format(date) + ": " + logEntry + "\n" try { - val outputStream = context.openFileOutput("nc_log.txt", - Context.MODE_PRIVATE or Context.MODE_APPEND) + val outputStream = context.openFileOutput( + "nc_log.txt", + Context.MODE_PRIVATE or Context.MODE_APPEND + ) outputStream.write(logEntryWithDateTime.toByteArray()) outputStream.flush() outputStream.close() @@ -48,13 +50,12 @@ object LoggingUtils { } catch (e: IOException) { e.printStackTrace() } - } fun sendMailWithAttachment(context: Context) { val logFile = context.getFileStreamPath("nc_log.txt") val emailIntent = Intent(Intent.ACTION_SEND) - val mailto = "mario@nextcloud.com" + val mailto = "android@nextcloud.com" emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(mailto)) emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Talk logs") emailIntent.type = "text/plain" diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt index f91f6636a..a4896c6e0 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt @@ -33,7 +33,7 @@ import android.service.notification.StatusBarNotification import com.nextcloud.talk.R import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.utils.bundle.BundleKeys -import java.util.* +import java.util.Objects object NotificationUtils { val NOTIFICATION_CHANNEL_CALLS = "NOTIFICATION_CHANNEL_CALLS" @@ -47,25 +47,50 @@ object NotificationUtils { return longArrayOf(0L, 400L, 800L, 600L, 800L, 800L, 800L, 1000L) } - fun getNotificationChannelId(channelName: String, - channelDescription: String, enableLights: Boolean, - importance: Int, sound: Uri, audioAttributes: AudioAttributes, vibrationPattern: LongArray?, bypassDnd: Boolean): String { - return Objects.hash(channelName, channelDescription, enableLights, importance, sound, audioAttributes, vibrationPattern, bypassDnd).toString() + fun getNotificationChannelId( + channelName: String, + channelDescription: String, + enableLights: Boolean, + importance: Int, + sound: Uri, + audioAttributes: AudioAttributes, + vibrationPattern: LongArray?, + bypassDnd: Boolean + ): String { + return Objects.hash( + channelName, + channelDescription, + enableLights, + importance, + sound, + audioAttributes, + vibrationPattern, + bypassDnd + ).toString() } @TargetApi(Build.VERSION_CODES.O) - fun createNotificationChannel(context: Context, - channelId: String, channelName: String, - channelDescription: String, enableLights: Boolean, - importance: Int, sound: Uri, audioAttributes: AudioAttributes, - vibrationPattern: LongArray?, bypassDnd: Boolean = false) { + fun createNotificationChannel( + context: Context, + channelId: String, + channelName: String, + channelDescription: String, + enableLights: Boolean, + importance: Int, + sound: Uri, + audioAttributes: AudioAttributes, + vibrationPattern: LongArray?, + bypassDnd: Boolean = false + ) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && notificationManager.getNotificationChannel(channelId) == null) { - val channel = NotificationChannel(channelId, channelName, - importance) + val channel = NotificationChannel( + channelId, channelName, + importance + ) channel.description = channelDescription channel.enableLights(enableLights) @@ -84,8 +109,11 @@ object NotificationUtils { } @TargetApi(Build.VERSION_CODES.O) - fun createNotificationChannelGroup(context: Context, - groupId: String, groupName: CharSequence) { + fun createNotificationChannelGroup( + context: Context, + groupId: String, + groupName: CharSequence + ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -113,12 +141,12 @@ object NotificationUtils { } } } - } fun cancelExistingNotificationWithId(context: Context?, conversationUser: UserEntity, notificationId: Long) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L && - context != null) { + context != null + ) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -128,7 +156,10 @@ object NotificationUtils { notification = statusBarNotification.notification if (notification != null && !notification.extras.isEmpty) { - if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID)) { + if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && notificationId == notification.extras.getLong( + BundleKeys.KEY_NOTIFICATION_ID + ) + ) { notificationManager.cancel(statusBarNotification.id) } } @@ -136,11 +167,14 @@ object NotificationUtils { } } - fun findNotificationForRoom(context: Context?, - conversationUser: UserEntity, - roomTokenOrId: String): StatusBarNotification? { + fun findNotificationForRoom( + context: Context?, + conversationUser: UserEntity, + roomTokenOrId: String + ): StatusBarNotification? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L && - context != null) { + context != null + ) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -150,7 +184,10 @@ object NotificationUtils { notification = statusBarNotification.notification if (notification != null && !notification.extras.isEmpty) { - if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && roomTokenOrId == statusBarNotification.notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN)) { + if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && roomTokenOrId == statusBarNotification.notification.extras.getString( + BundleKeys.KEY_ROOM_TOKEN + ) + ) { return statusBarNotification } } @@ -160,10 +197,14 @@ object NotificationUtils { return null } - fun cancelExistingNotificationsForRoom(context: Context?, conversationUser: UserEntity, - roomTokenOrId: String) { + fun cancelExistingNotificationsForRoom( + context: Context?, + conversationUser: UserEntity, + roomTokenOrId: String + ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L && - context != null) { + context != null + ) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -173,7 +214,11 @@ object NotificationUtils { notification = statusBarNotification.notification if (notification != null && !notification.extras.isEmpty) { - if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && roomTokenOrId == statusBarNotification.notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN)) { + if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && + roomTokenOrId == statusBarNotification.notification.extras.getString( + BundleKeys.KEY_ROOM_TOKEN + ) + ) { notificationManager.cancel(statusBarNotification.id) } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt index a4db61cf3..42dc9eb24 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt @@ -25,7 +25,6 @@ import android.database.Cursor import android.net.Uri import android.provider.OpenableColumns import android.util.Log -import com.nextcloud.talk.jobs.UploadAndShareFilesWorker object UriUtils { @@ -51,5 +50,4 @@ object UriUtils { } return filename } - } diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index 9de10f25c..49c400ae7 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -21,8 +21,8 @@ package com.nextcloud.talk.utils.bundle object BundleKeys { - val KEY_SELECTED_USERS = "KEY_SELECTED_USERS"; - val KEY_SELECTED_GROUPS = "KEY_SELECTED_GROUPS"; + val KEY_SELECTED_USERS = "KEY_SELECTED_USERS" + val KEY_SELECTED_GROUPS = "KEY_SELECTED_GROUPS" val KEY_USERNAME = "KEY_USERNAME" val KEY_TOKEN = "KEY_TOKEN" val KEY_BASE_URL = "KEY_BASE_URL" diff --git a/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt b/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt index 699e558e7..83ed2a0e2 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt @@ -13,11 +13,17 @@ import java.io.IOException import java.net.InetAddress import java.net.Socket import java.security.GeneralSecurityException -import java.util.* -import javax.net.ssl.* +import java.util.LinkedList +import javax.net.ssl.KeyManager +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager -class SSLSocketFactoryCompat(keyManager: KeyManager?, - trustManager: X509TrustManager) : SSLSocketFactory() { +class SSLSocketFactoryCompat( + keyManager: KeyManager?, + trustManager: X509TrustManager +) : SSLSocketFactory() { private var delegate: SSLSocketFactory @@ -50,24 +56,24 @@ class SSLSocketFactoryCompat(keyManager: KeyManager?, /* set up reasonable cipher suites */ val knownCiphers = arrayOf( - // TLS 1.2 - "TLS_RSA_WITH_AES_256_GCM_SHA384", - "TLS_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - // maximum interoperability - "TLS_RSA_WITH_3DES_EDE_CBC_SHA", - "SSL_RSA_WITH_3DES_EDE_CBC_SHA", - "TLS_RSA_WITH_AES_128_CBC_SHA", - // additionally - "TLS_RSA_WITH_AES_256_CBC_SHA", - "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", - "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" + // TLS 1.2 + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + // maximum interoperability + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + // additionally + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" ) val availableCiphers = socket.supportedCipherSuites @@ -89,31 +95,31 @@ class SSLSocketFactoryCompat(keyManager: KeyManager?, } catch (e: IOException) { // Exception is to be ignored } finally { - socket?.close() // doesn't implement Closeable on all supported Android versions + socket?.close() // doesn't implement Closeable on all supported Android versions } } } } - init { try { val sslContext = SSLContext.getInstance("TLS") sslContext.init( - if (keyManager != null) arrayOf(keyManager) else null, - arrayOf(trustManager), - null) + if (keyManager != null) arrayOf(keyManager) else null, + arrayOf(trustManager), + null + ) delegate = sslContext.socketFactory } catch (e: GeneralSecurityException) { - throw IllegalStateException() // system has no TLS + throw IllegalStateException() // system has no TLS } } override fun getDefaultCipherSuites(): Array? = cipherSuites - ?: delegate.defaultCipherSuites + ?: delegate.defaultCipherSuites override fun getSupportedCipherSuites(): Array? = cipherSuites - ?: delegate.supportedCipherSuites + ?: delegate.supportedCipherSuites override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket { val ssl = delegate.createSocket(s, host, port, autoClose) @@ -150,10 +156,8 @@ class SSLSocketFactoryCompat(keyManager: KeyManager?, return ssl } - private fun upgradeTLS(ssl: SSLSocket) { protocols?.let { ssl.enabledProtocols = it } cipherSuites?.let { ssl.enabledCipherSuites = it } } - } diff --git a/app/src/qa/ic_launcher-web.png b/app/src/qa/ic_launcher-web.png new file mode 100644 index 000000000..9bde487a3 Binary files /dev/null and b/app/src/qa/ic_launcher-web.png differ diff --git a/app/src/qa/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java b/app/src/qa/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java new file mode 100644 index 000000000..66496bd31 --- /dev/null +++ b/app/src/qa/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java @@ -0,0 +1,36 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2018 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.utils; + + +import com.nextcloud.talk.interfaces.ClosedInterface; + +public class ClosedInterfaceImpl implements ClosedInterface { + @Override + public void providerInstallerInstallIfNeededAsync() { + // does absolutely nothing :) + } + + @Override + public boolean isGooglePlayServicesAvailable() { + return false; + } +} diff --git a/app/src/qa/res/drawable-v24/ic_launcher_background.xml b/app/src/qa/res/drawable-v24/ic_launcher_background.xml new file mode 100644 index 000000000..3f3d4b826 --- /dev/null +++ b/app/src/qa/res/drawable-v24/ic_launcher_background.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/app/src/qa/res/drawable/ic_launcher_background.xml b/app/src/qa/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..a931ebc5f --- /dev/null +++ b/app/src/qa/res/drawable/ic_launcher_background.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/app/src/qa/res/drawable/ic_launcher_foreground.xml b/app/src/qa/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b7fc86b2 --- /dev/null +++ b/app/src/qa/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/qa/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/qa/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..bbd3e0212 --- /dev/null +++ b/app/src/qa/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/qa/res/mipmap-hdpi/ic_launcher.png b/app/src/qa/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..99024db8e Binary files /dev/null and b/app/src/qa/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/qa/res/mipmap-mdpi/ic_launcher.png b/app/src/qa/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..2b6d24b1c Binary files /dev/null and b/app/src/qa/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/qa/res/mipmap-xhdpi/ic_launcher.png b/app/src/qa/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..a0528c11b Binary files /dev/null and b/app/src/qa/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/qa/res/mipmap-xxhdpi/ic_launcher.png b/app/src/qa/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..a2a2f2731 Binary files /dev/null and b/app/src/qa/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/qa/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/qa/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..677ffdd25 Binary files /dev/null and b/app/src/qa/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/qa/res/values/setup.xml b/app/src/qa/res/values/setup.xml new file mode 100644 index 000000000..0650249ab --- /dev/null +++ b/app/src/qa/res/values/setup.xml @@ -0,0 +1,25 @@ + + + + + Nextcloud Talk QA + Nextcloud + diff --git a/build.gradle b/build.gradle index f5d7cb644..fbe358efa 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.5.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" + classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.13.1" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 000000000..7b254e8ec --- /dev/null +++ b/detekt.yml @@ -0,0 +1,472 @@ +build: + maxIssues: 346 + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +processors: + active: true + exclude: + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ClassCountProcessor' + # - 'PackageCountProcessor' + # - 'KtFileCountProcessor' + +console-reports: + active: true + exclude: + # - 'ProjectStatisticsReport' + # - 'ComplexityReport' + # - 'NotificationReport' + # - 'FindingsReport' + # - 'BuildFailureReport' + +comments: + active: true + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$) + UndocumentedPublicClass: + active: false + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + UndocumentedPublicFunction: + active: false + +complexity: + active: true + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + ComplexMethod: + active: true + threshold: 10 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + excludes: ['**/androidTest/**'] + LabeledExpression: + active: false + ignoredLabels: "" + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + excludes: ['**/androidTest/**'] + LongParameterList: + active: true + threshold: 6 + ignoreDefaultParameters: false + MethodOverloading: + active: false + threshold: 6 + NestedBlockDepth: + active: true + threshold: 4 + StringLiteralDuplication: + active: false + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + thresholdInFiles: 15 + thresholdInClasses: 15 + thresholdInInterfaces: 15 + thresholdInObjects: 15 + thresholdInEnums: 11 + ignoreDeprecated: true + ignorePrivate: false + ignoreOverridden: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: false + methodNames: 'toString,hashCode,equals,finalize' + InstanceOfCheckForException: + active: false + NotImplementedDeclaration: + active: false + PrintStackTrace: + active: false + RethrowCaughtException: + active: false + ReturnFromFinally: + active: false + SwallowedException: + active: false + ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException' + ThrowingExceptionFromFinally: + active: false + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: false + exceptions: 'IllegalArgumentException,IllegalStateException,IOException' + ThrowingNewInstanceOfSameException: + active: false + TooGenericExceptionCaught: + active: true + exceptionNames: + - ArrayIndexOutOfBoundsException + - Error + - Exception + - IllegalMonitorStateException + - NullPointerException + - IndexOutOfBoundsException + - RuntimeException + - Throwable + allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + TooGenericExceptionThrown: + active: true + exceptionNames: + - Error + - Exception + - Throwable + - RuntimeException + +formatting: + active: true + android: false + ChainWrapping: + active: true + CommentSpacing: + active: true + Filename: + active: true + FinalNewline: + active: true + ImportOrdering: + active: false + Indentation: + active: true + indentSize: 4 + continuationIndentSize: 4 + MaximumLineLength: + active: true + maxLineLength: 120 + ModifierOrdering: + active: true + NoBlankLineBeforeRbrace: + active: true + NoConsecutiveBlankLines: + active: true + NoEmptyClassBody: + active: true + NoLineBreakAfterElse: + active: true + NoLineBreakBeforeAssignment: + active: true + NoMultipleSpaces: + active: true + NoSemicolons: + active: true + NoTrailingSpaces: + active: true + NoUnitReturn: + active: true + NoUnusedImports: + active: true + NoWildcardImports: + active: true + PackageName: + active: true + ParameterListWrapping: + active: true + indentSize: 4 + SpacingAroundColon: + active: true + SpacingAroundComma: + active: true + SpacingAroundCurly: + active: true + SpacingAroundKeyword: + active: true + SpacingAroundOperators: + active: true + SpacingAroundParens: + active: true + SpacingAroundRangeOperator: + active: true + StringTemplate: + active: true + +naming: + active: true + ClassNaming: + active: true + classPattern: '[A-Z$][a-zA-Z0-9$]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: '' + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' + excludeClassPattern: '$^' + ignoreOverridden: true + excludes: "**/*Test.kt" + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + MatchingDeclarationName: + active: true + MemberNameEqualsClassName: + active: false + ignoreOverridden: true + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + +performance: + active: true + ArrayPrimitive: + active: false + ForEachOnRange: + active: true + SpreadOperator: + active: true + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + DuplicateCaseInWhenExpression: + active: true + EqualsAlwaysReturnsTrueOrFalse: + active: false + EqualsWithHashCodeExist: + active: true + ExplicitGarbageCollectionCall: + active: true + InvalidRange: + active: false + IteratorHasNextCallsNextMethod: + active: false + IteratorNotThrowingNoSuchElementException: + active: false + LateinitUsage: + active: false + excludeAnnotatedProperties: "" + ignoreOnClassesPattern: "" + UnconditionalJumpStatementInLoop: + active: false + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: false + UnsafeCast: + active: false + UselessPostfixExpression: + active: false + WrongEqualsTypeParameter: + active: false + +style: + active: true + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: 'to' + EqualsNullCall: + active: false + EqualsOnSignatureLine: + active: false + ExplicitItLambdaParameter: + active: false + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenComment: + active: true + values: 'TODO:,FIXME:,STOPSHIP:' + ForbiddenImport: + active: false + imports: '' + ForbiddenVoid: + active: false + FunctionOnlyReturningConstant: + active: false + ignoreOverridableFunction: true + excludedFunctions: 'describeContents' + LoopWithTooManyJumpStatements: + active: false + maxJumpCount: 1 + MagicNumber: + active: true + ignoreNumbers: '-1,0,1,2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + excludes: "**/*Test.kt" + MandatoryBracesIfStatements: + active: false + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + MayBeConst: + active: false + ModifierOrder: + active: true + NestedClassesVisibility: + active: false + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + OptionalWhenBraces: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: false + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 2 + excludedFunctions: "equals" + excludeLabeled: false + excludeReturnFromLambda: true + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: false + SpacingBetweenPackageAndImports: + active: false + ThrowsCount: + active: true + max: 2 + TrailingWhitespace: + active: false + UnderscoresInNumericLiterals: + active: false + acceptableDecimalLength: 5 + UnnecessaryAbstractClass: + active: false + excludeAnnotatedClasses: "dagger.Module" + UnnecessaryApply: + active: false + UnnecessaryInheritance: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedPrivateClass: + active: false + UnusedPrivateMember: + active: false + allowedNames: "(_|ignored|expected|serialVersionUID)" + UseDataClass: + active: false + excludeAnnotatedClasses: "" + UtilityClassWithPublicConstructor: + active: false + VarCouldBeVal: + active: false + WildcardImport: + active: true + excludeImports: 'java.util.*,kotlinx.android.synthetic.*' diff --git a/drawable_resources/icon-background.svg b/drawable_resources/icon-background.svg index b2618fccd..fc99ed29e 100644 --- a/drawable_resources/icon-background.svg +++ b/drawable_resources/icon-background.svg @@ -1,309 +1,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/drawable_resources/icon-foreground_qa.svg b/drawable_resources/icon-foreground_qa.svg new file mode 100644 index 000000000..42f765cfa --- /dev/null +++ b/drawable_resources/icon-foreground_qa.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/QA_keystore.jks b/scripts/QA_keystore.jks new file mode 100644 index 000000000..2b8fb9bc2 Binary files /dev/null and b/scripts/QA_keystore.jks differ diff --git a/scripts/uploadArtifact.sh b/scripts/uploadArtifact.sh new file mode 100755 index 000000000..383abf6fe --- /dev/null +++ b/scripts/uploadArtifact.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +#1: LOG_USERNAME +#2: LOG_PASSWORD +#3: DRONE_BUILD_NUMBER +#4: DRONE_PULL_REQUEST +#5: GITHUB_TOKEN + +DAV_URL=https://nextcloud.kaminsky.me/remote.php/webdav/android-artifacts/ +PUBLIC_URL=https://www.kaminsky.me/nc-dev/android-artifacts +USER=$1 +PASS=$2 +BUILD=$3 +PR=$4 +GITHUB_TOKEN=$5 + +if ! test -e app/build/outputs/apk/qa/debug/app-qa-*.apk ; then + exit 1 +fi +echo "Uploaded artifact to $DAV_URL/$BUILD-talk.apk" + +# delete all old comments, starting with "APK file:" +oldComments=$(curl 2>/dev/null --header "authorization: Bearer $GITHUB_TOKEN" -X GET https://api.github.com/repos/nextcloud/talk-android/issues/$PR/comments | jq '.[] | (.id |tostring) + "|" + (.user.login | test("github-actions") | tostring) + "|" + (.body | test("APK file:.*") | tostring)' | grep "true|true" | tr -d "\"" | cut -f1 -d"|") + +echo $oldComments | while read comment ; do + curl 2>/dev/null --header "authorization: Bearer $GITHUB_TOKEN" -X DELETE https://api.github.com/repos/nextcloud/talk-android/issues/comments/$comment +done + +apt-get -y install qrencode + +qrencode -o $PR.png "$PUBLIC_URL/$BUILD-talk.apk" + +curl -u $USER:$PASS -X PUT $DAV_URL/$BUILD-talk.apk --upload-file app/build/outputs/apk/qa/debug/app-qa-*.apk +curl -u $USER:$PASS -X PUT $DAV_URL/$BUILD-talk.png --upload-file $PR.png +curl --header "authorization: Bearer $GITHUB_TOKEN" -X POST https://api.github.com/repos/nextcloud/talk-android/issues/$PR/comments -d "{ \"body\" : \"APK file: $PUBLIC_URL/$BUILD-talk.apk

![qrcode]($PUBLIC_URL/$BUILD-talk.png)

To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud Talk app. \" }"