diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json
new file mode 100644
index 0000000000..08c6493c46
--- /dev/null
+++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json
@@ -0,0 +1,146 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 10,
+ "identityHash": "1b2dab0ea495c45c9c9ee6e64ba74039",
+ "entities": [
+ {
+ "tableName": "User",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "baseUrl",
+ "columnName": "baseUrl",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "pushConfigurationState",
+ "columnName": "pushConfigurationState",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "capabilities",
+ "columnName": "capabilities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "serverVersion",
+ "columnName": "serverVersion",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "clientCertificate",
+ "columnName": "clientCertificate",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "externalSignalingServer",
+ "columnName": "externalSignalingServer",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "current",
+ "columnName": "current",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scheduledForDeletion",
+ "columnName": "scheduledForDeletion",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "ArbitraryStorage",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
+ "fields": [
+ {
+ "fieldPath": "accountIdentifier",
+ "columnName": "accountIdentifier",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "storageObject",
+ "columnName": "object",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "accountIdentifier",
+ "key"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1b2dab0ea495c45c9c9ee6e64ba74039')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt b/app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt
index 258e7a3562..87d6d2c842 100644
--- a/app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt
+++ b/app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt
@@ -29,6 +29,7 @@ class MainActivityTest {
displayName = "Test Name",
pushConfigurationState = null,
capabilities = null,
+ serverVersion = null,
certificateAlias = null,
externalSignalingServer = null
)
diff --git a/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt b/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt
index a2320217e5..c8ef29cbf8 100644
--- a/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt
+++ b/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt
@@ -57,6 +57,7 @@ class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerP
Log.d(TAG, "Fetched firebase push token is: $pushToken")
appPreferences.pushToken = pushToken
+ appPreferences.pushTokenLatestFetch = System.currentTimeMillis()
val data: Data =
Data.Builder().putString(PushRegistrationWorker.ORIGIN, "GetFirebasePushTokenWorker").build()
diff --git a/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt
index 413b2e7970..aa997bced9 100644
--- a/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt
+++ b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt
@@ -78,6 +78,7 @@ class NCFirebaseMessagingService : FirebaseMessagingService() {
Log.d(TAG, "onNewToken. token = $token")
appPreferences.pushToken = token
+ appPreferences.pushTokenLatestGeneration = System.currentTimeMillis()
val data: Data =
Data.Builder().putString(PushRegistrationWorker.ORIGIN, "NCFirebaseMessagingService#onNewToken").build()
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f0823c943c..ba4e7ddcb5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -241,6 +241,10 @@
android:name=".settings.SettingsActivity"
android:theme="@style/AppTheme" />
+
+
diff --git a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt
index bcbf04dfb0..958c1f3ca4 100644
--- a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt
@@ -51,7 +51,6 @@ import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.jobs.CapabilitiesWorker
import com.nextcloud.talk.jobs.SignalingSettingsWorker
import com.nextcloud.talk.jobs.WebsocketConnectionsWorker
-import com.nextcloud.talk.models.json.capabilities.Capabilities
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
import com.nextcloud.talk.models.json.generic.Status
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
@@ -250,7 +249,7 @@ class AccountVerificationActivity : BaseActivity() {
})
}
- private fun storeProfile(displayName: String?, userId: String, capabilities: Capabilities) {
+ private fun storeProfile(displayName: String?, userId: String, capabilitiesOverall: CapabilitiesOverall) {
userManager.storeProfile(
username,
UserManager.UserAttributes(
@@ -261,7 +260,8 @@ class AccountVerificationActivity : BaseActivity() {
token = token,
displayName = displayName,
pushConfigurationState = null,
- capabilities = LoganSquare.serialize(capabilities),
+ capabilities = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.capabilities),
+ serverVersion = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.serverVersion),
certificateAlias = appPreferences.temporaryClientCertAlias,
externalSignalingServer = null
)
@@ -302,7 +302,7 @@ class AccountVerificationActivity : BaseActivity() {
})
}
- private fun fetchProfile(credentials: String, capabilities: CapabilitiesOverall) {
+ private fun fetchProfile(credentials: String, capabilitiesOverall: CapabilitiesOverall) {
ncApi.getUserProfile(
credentials,
ApiUtils.getUrlForUserProfile(baseUrl)
@@ -325,7 +325,7 @@ class AccountVerificationActivity : BaseActivity() {
storeProfile(
displayName,
userProfileOverall.ocs!!.data!!.userId!!,
- capabilities.ocs!!.data!!.capabilities!!
+ capabilitiesOverall
)
} else {
runOnUiThread {
diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt
index 63bf0d8f2b..38a81eebda 100644
--- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt
@@ -41,6 +41,7 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
+import android.provider.Settings
import android.text.InputType
import android.text.TextUtils
import android.util.Log
@@ -107,6 +108,7 @@ import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog
import com.nextcloud.talk.ui.dialog.FilterConversationFragment
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.FileUtils
import com.nextcloud.talk.utils.Mimetype
import com.nextcloud.talk.utils.ParticipantPermissions
@@ -126,6 +128,7 @@ import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isServerEOL
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isUnifiedSearchAvailable
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isUserStatusAvailable
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
+import com.nextcloud.talk.utils.power.PowerManagerUtils
import com.nextcloud.talk.utils.rx.SearchViewObservable.Companion.observeSearchView
import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -234,7 +237,8 @@ class ConversationsListActivity :
// handle notification permission on API level >= 33
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
- !platformPermissionUtil.isPostNotificationsPermissionGranted()
+ !platformPermissionUtil.isPostNotificationsPermissionGranted() &&
+ ClosedInterfaceImpl().isGooglePlayServicesAvailable
) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
@@ -1269,16 +1273,55 @@ class ConversationsListActivity :
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION) {
- if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- Log.d(TAG, "upload starting after permissions were granted")
- showSendFilesConfirmDialog()
- } else {
- Snackbar.make(
- binding.root,
- context.getString(R.string.read_storage_no_permission),
- Snackbar.LENGTH_LONG
- ).show()
+
+ when (requestCode) {
+ UploadAndShareFilesWorker.REQUEST_PERMISSION -> {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "upload starting after permissions were granted")
+ showSendFilesConfirmDialog()
+ } else {
+ Snackbar.make(
+ binding.root,
+ context.getString(R.string.read_storage_no_permission),
+ Snackbar.LENGTH_LONG
+ ).show()
+ }
+ }
+
+ REQUEST_POST_NOTIFICATIONS_PERMISSION -> {
+ // whenever user allowed notifications, also check to ignore battery optimization
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (!PowerManagerUtils().isIgnoringBatteryOptimizations() &&
+ ClosedInterfaceImpl().isGooglePlayServicesAvailable
+ ) {
+ val dialogText = String.format(
+ context.resources.getString(R.string.nc_ignore_battery_optimization_dialog_text),
+ context.resources.getString(R.string.nc_app_name)
+ )
+
+ val dialogBuilder = MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.nc_ignore_battery_optimization_dialog_title)
+ .setMessage(dialogText)
+ .setPositiveButton(R.string.nc_ok) { _, _ ->
+ startActivity(
+ Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
+ )
+ }
+ .setNegativeButton(R.string.nc_common_dismiss, null)
+ viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder)
+ val dialog = dialogBuilder.show()
+ viewThemeUtils.platform.colorTextButtons(
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE),
+ dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
+ )
+ }
+ } else {
+ Log.d(
+ TAG,
+ "Notification permission is denied. Either because user denied it when being asked. " +
+ "Or permission is already denied and android decided to not offer the dialog."
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt
index 65eb7ee5fe..e5862cd279 100644
--- a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt
+++ b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt
@@ -32,6 +32,7 @@ import com.nextcloud.talk.data.source.local.converters.CapabilitiesConverter
import com.nextcloud.talk.data.source.local.converters.ExternalSignalingServerConverter
import com.nextcloud.talk.data.source.local.converters.HashMapHashMapConverter
import com.nextcloud.talk.data.source.local.converters.PushConfigurationConverter
+import com.nextcloud.talk.data.source.local.converters.ServerVersionConverter
import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter
import com.nextcloud.talk.data.storage.ArbitraryStoragesDao
import com.nextcloud.talk.data.storage.model.ArbitraryStorageEntity
@@ -42,15 +43,20 @@ import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteDatabaseHook
import net.sqlcipher.database.SupportFactory
import java.util.Locale
+import androidx.room.AutoMigration
@Database(
entities = [UserEntity::class, ArbitraryStorageEntity::class],
- version = 9,
+ version = 10,
+ autoMigrations = [
+ AutoMigration(from = 9, to = 10)
+ ],
exportSchema = true
)
@TypeConverters(
PushConfigurationConverter::class,
CapabilitiesConverter::class,
+ ServerVersionConverter::class,
ExternalSignalingServerConverter::class,
SignalingSettingsConverter::class,
HashMapHashMapConverter::class
diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ServerVersionConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ServerVersionConverter.kt
new file mode 100644
index 0000000000..800a15d7ca
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ServerVersionConverter.kt
@@ -0,0 +1,47 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * Copyright (C) 2017-2020 Mario Danic
+ * Copyright (C) 2024 Marcel Hibbe
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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.data.source.local.converters
+
+import androidx.room.TypeConverter
+import com.bluelinelabs.logansquare.LoganSquare
+import com.nextcloud.talk.models.json.capabilities.ServerVersion
+
+class ServerVersionConverter {
+ @TypeConverter
+ fun fromServerVersionToString(serverVersion: ServerVersion?): String {
+ return if (serverVersion == null) {
+ ""
+ } else {
+ LoganSquare.serialize(serverVersion)
+ }
+ }
+
+ @TypeConverter
+ fun fromStringToServerVersion(value: String): ServerVersion? {
+ return if (value.isBlank()) {
+ null
+ } else {
+ return LoganSquare.parse(value, ServerVersion::class.java)
+ }
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt b/app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt
index f6a88ac773..48ef06e981 100644
--- a/app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt
+++ b/app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt
@@ -41,6 +41,7 @@ object UserMapper {
entity.displayName,
entity.pushConfigurationState,
entity.capabilities,
+ entity.serverVersion,
entity.clientCertificate,
entity.externalSignalingServer,
entity.current,
@@ -59,6 +60,7 @@ object UserMapper {
displayName = model.displayName
pushConfigurationState = model.pushConfigurationState
capabilities = model.capabilities
+ serverVersion = model.serverVersion
clientCertificate = model.clientCertificate
externalSignalingServer = model.externalSignalingServer
current = model.current
diff --git a/app/src/main/java/com/nextcloud/talk/data/user/model/User.kt b/app/src/main/java/com/nextcloud/talk/data/user/model/User.kt
index 30b8906e15..a10b557052 100644
--- a/app/src/main/java/com/nextcloud/talk/data/user/model/User.kt
+++ b/app/src/main/java/com/nextcloud/talk/data/user/model/User.kt
@@ -22,6 +22,7 @@ package com.nextcloud.talk.data.user.model
import android.os.Parcelable
import com.nextcloud.talk.models.ExternalSignalingServer
import com.nextcloud.talk.models.json.capabilities.Capabilities
+import com.nextcloud.talk.models.json.capabilities.ServerVersion
import com.nextcloud.talk.models.json.push.PushConfigurationState
import com.nextcloud.talk.utils.ApiUtils
import kotlinx.parcelize.Parcelize
@@ -37,6 +38,7 @@ data class User(
var displayName: String? = null,
var pushConfigurationState: PushConfigurationState? = null,
var capabilities: Capabilities? = null,
+ var serverVersion: ServerVersion? = null,
var clientCertificate: String? = null,
var externalSignalingServer: ExternalSignalingServer? = null,
var current: Boolean = FALSE,
diff --git a/app/src/main/java/com/nextcloud/talk/data/user/model/UserEntity.kt b/app/src/main/java/com/nextcloud/talk/data/user/model/UserEntity.kt
index 33144dbe43..cba19b9f53 100644
--- a/app/src/main/java/com/nextcloud/talk/data/user/model/UserEntity.kt
+++ b/app/src/main/java/com/nextcloud/talk/data/user/model/UserEntity.kt
@@ -28,6 +28,7 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
import com.nextcloud.talk.models.ExternalSignalingServer
import com.nextcloud.talk.models.json.capabilities.Capabilities
+import com.nextcloud.talk.models.json.capabilities.ServerVersion
import com.nextcloud.talk.models.json.push.PushConfigurationState
import kotlinx.parcelize.Parcelize
import java.lang.Boolean.FALSE
@@ -60,6 +61,9 @@ data class UserEntity(
@ColumnInfo(name = "capabilities")
var capabilities: Capabilities? = null,
+ @ColumnInfo(name = "serverVersion", defaultValue = "")
+ var serverVersion: ServerVersion? = null,
+
@ColumnInfo(name = "clientCertificate")
var clientCertificate: String? = null,
diff --git a/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseActivity.kt b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseActivity.kt
new file mode 100644
index 0000000000..336be6ac16
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseActivity.kt
@@ -0,0 +1,476 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2023 Marcel Hibbe
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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.diagnose
+
+import android.annotation.SuppressLint
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.graphics.Typeface
+import android.graphics.drawable.ColorDrawable
+import android.net.Uri
+import android.os.Build
+import android.os.Build.MANUFACTURER
+import android.os.Build.MODEL
+import android.os.Bundle
+import android.text.SpannableStringBuilder
+import android.util.TypedValue
+import android.view.Menu
+import android.view.MenuItem
+import android.view.ViewGroup
+import android.widget.TextView
+import android.widget.Toast
+import androidx.core.text.bold
+import androidx.core.view.updateLayoutParams
+import autodagger.AutoInjector
+import com.nextcloud.talk.BuildConfig
+import com.nextcloud.talk.R
+import com.nextcloud.talk.activities.BaseActivity
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
+import com.nextcloud.talk.databinding.ActivityDiagnoseBinding
+import com.nextcloud.talk.users.UserManager
+import com.nextcloud.talk.utils.ClosedInterfaceImpl
+import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.NotificationUtils
+import com.nextcloud.talk.utils.PushUtils.Companion.LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY
+import com.nextcloud.talk.utils.PushUtils.Companion.LATEST_PUSH_REGISTRATION_AT_SERVER
+import com.nextcloud.talk.utils.UserIdUtils
+import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
+import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
+import com.nextcloud.talk.utils.power.PowerManagerUtils
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+@Suppress("TooManyFunctions")
+class DiagnoseActivity : BaseActivity() {
+ private lateinit var binding: ActivityDiagnoseBinding
+
+ @Inject
+ lateinit var arbitraryStorageManager: ArbitraryStorageManager
+
+ @Inject
+ lateinit var ncApi: NcApi
+
+ @Inject
+ lateinit var userManager: UserManager
+
+ @Inject
+ lateinit var currentUserProvider: CurrentUserProviderNew
+
+ @Inject
+ lateinit var platformPermissionUtil: PlatformPermissionUtil
+
+ private var isGooglePlayServicesAvailable: Boolean = false
+
+ private val markdownText = SpannableStringBuilder()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+
+ binding = ActivityDiagnoseBinding.inflate(layoutInflater)
+ setupActionBar()
+ setContentView(binding.root)
+ setupSystemColors()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ supportActionBar?.show()
+
+ isGooglePlayServicesAvailable = ClosedInterfaceImpl().isGooglePlayServicesAvailable
+
+ markdownText.clear()
+ setupMetaValues()
+ setupPhoneValues()
+ setupAppValues()
+ setupAccountValues()
+
+ createLayoutFromMarkdown()
+ }
+
+ private fun setupActionBar() {
+ setSupportActionBar(binding.settingsToolbar)
+ binding.settingsToolbar.setNavigationOnClickListener {
+ onBackPressedDispatcher.onBackPressed()
+ }
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.setDisplayShowHomeEnabled(true)
+ supportActionBar?.setIcon(ColorDrawable(resources!!.getColor(android.R.color.transparent, null)))
+ supportActionBar?.title = context.getString(R.string.nc_settings_diagnose_title)
+ viewThemeUtils.material.themeToolbar(binding.settingsToolbar)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.menu_diagnose, menu)
+ return true
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ super.onPrepareOptionsMenu(menu)
+ menu.findItem(R.id.create_issue).isVisible =
+ applicationContext.packageName.equals(ORIGINAL_NEXTCLOUD_TALK_APPLICATION_ID)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressedDispatcher.onBackPressed()
+ true
+ }
+
+ R.id.copy -> {
+ copyToClipboard(markdownText.toString())
+ true
+ }
+
+ R.id.share -> {
+ shareToOtherApps(markdownText.toString())
+ true
+ }
+
+ R.id.send_mail -> {
+ composeEmail(markdownText.toString())
+ true
+ }
+
+ R.id.create_issue -> {
+ createGithubIssue(markdownText.toString())
+ true
+ }
+
+ else -> {
+ super.onOptionsItemSelected(item)
+ }
+ }
+ }
+
+ private fun shareToOtherApps(message: String) {
+ val sendIntent: Intent = Intent().apply {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_TEXT, message)
+ type = "text/plain"
+ }
+ val shareIntent = Intent.createChooser(sendIntent, getString(R.string.share))
+ startActivity(shareIntent)
+ }
+
+ private fun composeEmail(text: String) {
+ val intent = Intent(Intent.ACTION_SENDTO).apply {
+ val appName = context.resources.getString(R.string.nc_app_product_name)
+
+ data = Uri.parse("mailto:")
+ putExtra(Intent.EXTRA_SUBJECT, appName)
+ putExtra(Intent.EXTRA_TEXT, text)
+ }
+ if (intent.resolveActivity(packageManager) != null) {
+ startActivity(intent)
+ }
+ }
+
+ private fun createGithubIssue(text: String) {
+ copyToClipboard(text)
+
+ startActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(resources!!.getString(R.string.nc_talk_android_issues_url))
+ )
+ )
+ }
+
+ private fun copyToClipboard(text: String) {
+ val clipboardManager =
+ getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clipData = ClipData.newPlainText(
+ resources?.getString(R.string.nc_app_product_name),
+ text
+ )
+ clipboardManager.setPrimaryClip(clipData)
+
+ Toast.makeText(
+ context,
+ context.resources.getString(R.string.nc_common_copy_success),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+
+ private fun setupMetaValues() {
+ addHeadline(context.resources.getString(R.string.nc_diagnose_meta_category_title))
+ addKey(context.resources.getString(R.string.nc_diagnose_meta_system_report_date))
+ addValue(DisplayUtils.unixTimeToHumanReadable(System.currentTimeMillis()))
+ }
+
+ private fun setupPhoneValues() {
+ addHeadline(context.resources.getString(R.string.nc_diagnose_phone_category_title))
+
+ addKey(context.resources.getString(R.string.nc_diagnose_device_name_title))
+ addValue(getDeviceName())
+
+ addKey(context.resources.getString(R.string.nc_diagnose_android_version_title))
+ addValue(Build.VERSION.SDK_INT.toString())
+
+ if (isGooglePlayServicesAvailable) {
+ addKey(context.resources.getString(R.string.nc_diagnose_gplay_available_title))
+ addValue(context.resources.getString(R.string.nc_diagnose_gplay_available_yes))
+ } else {
+ addKey(context.resources.getString(R.string.nc_diagnose_gplay_available_title))
+ addValue(context.resources.getString(R.string.nc_diagnose_gplay_available_no))
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ @Suppress("MagicNumber")
+ private fun setupAppValues() {
+ addHeadline(context.resources.getString(R.string.nc_diagnose_app_category_title))
+
+ addKey(context.resources.getString(R.string.nc_diagnose_app_name_title))
+ addValue(context.resources.getString(R.string.nc_app_product_name))
+
+ addKey(context.resources.getString(R.string.nc_diagnose_app_version_title))
+ addValue(String.format("v" + BuildConfig.VERSION_NAME))
+
+ addKey(context.resources.getString(R.string.nc_diagnose_build_flavor))
+ addValue(BuildConfig.FLAVOR)
+
+ if (isGooglePlayServicesAvailable) {
+ addKey(context.resources.getString(R.string.nc_diagnose_battery_optimization_title))
+
+ if (PowerManagerUtils().isIgnoringBatteryOptimizations()) {
+ addValue(context.resources.getString(R.string.nc_diagnose_battery_optimization_ignored))
+ } else {
+ addValue(context.resources.getString(R.string.nc_diagnose_battery_optimization_not_ignored))
+ }
+
+ // handle notification permission on API level >= 33
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ addKey(context.resources.getString(R.string.nc_diagnose_notification_permission))
+ if (platformPermissionUtil.isPostNotificationsPermissionGranted()) {
+ addValue(context.resources.getString(R.string.nc_settings_notifications_granted))
+ } else {
+ addValue(context.resources.getString(R.string.nc_settings_notifications_declined))
+ }
+ }
+
+ addKey(context.resources.getString(R.string.nc_diagnose_notification_calls_channel_permission))
+ addValue(NotificationUtils.isCallsNotificationChannelEnabled(this).toString())
+
+ addKey(context.resources.getString(R.string.nc_diagnose_notification_messages_channel_permission))
+ addValue(NotificationUtils.isMessagesNotificationChannelEnabled(this).toString())
+
+ addKey(context.resources.getString(R.string.nc_diagnose_firebase_push_token_title))
+ if (appPreferences.pushToken.isNullOrEmpty()) {
+ addValue(context.resources.getString(R.string.nc_diagnose_firebase_push_token_missing))
+ } else {
+ addValue("${appPreferences.pushToken.substring(0, 5)}...")
+ }
+
+ addKey(context.resources.getString(R.string.nc_diagnose_firebase_push_token_latest_generated))
+ if (appPreferences.pushTokenLatestGeneration != null && appPreferences.pushTokenLatestGeneration != 0L) {
+ addValue(
+ DisplayUtils.unixTimeToHumanReadable(
+ appPreferences
+ .pushTokenLatestGeneration
+ )
+ )
+ } else {
+ addValue(context.resources.getString(R.string.nc_common_unknown))
+ }
+
+ addKey(context.resources.getString(R.string.nc_diagnose_firebase_push_token_latest_fetch))
+ if (appPreferences.pushTokenLatestFetch != null && appPreferences.pushTokenLatestFetch != 0L) {
+ addValue(DisplayUtils.unixTimeToHumanReadable(appPreferences.pushTokenLatestFetch))
+ } else {
+ addValue(context.resources.getString(R.string.nc_common_unknown))
+ }
+ }
+
+ addKey(context.resources.getString(R.string.nc_diagnose_app_users_amount))
+ addValue(userManager.users.blockingGet().size.toString())
+ }
+
+ private fun setupAccountValues() {
+ addHeadline(context.resources.getString(R.string.nc_diagnose_account_category_title))
+
+ addKey(context.resources.getString(R.string.nc_diagnose_account_server))
+ addValue(userManager.currentUser.blockingGet().baseUrl!!)
+
+ addKey(context.resources.getString(R.string.nc_diagnose_account_user_name))
+ addValue(userManager.currentUser.blockingGet().displayName!!)
+
+ addKey(context.resources.getString(R.string.nc_diagnose_account_user_status_enabled))
+ addValue(
+ translateBoolean(
+ (userManager.currentUser.blockingGet().capabilities?.userStatusCapability?.enabled)
+ )
+ )
+
+ addKey(context.resources.getString(R.string.nc_diagnose_account_server_notification_app))
+ addValue(
+ translateBoolean(
+ userManager.currentUser.blockingGet().capabilities?.notificationsCapability?.features?.isNotEmpty()
+ )
+ )
+
+ if (isGooglePlayServicesAvailable) {
+ setupPushRegistrationDiagnose()
+ }
+
+ addKey(context.resources.getString(R.string.nc_diagnose_server_version))
+ addValue(userManager.currentUser.blockingGet().serverVersion?.versionString!!)
+
+ addKey(context.resources.getString(R.string.nc_diagnose_server_talk_version))
+ addValue(userManager.currentUser.blockingGet().capabilities?.spreedCapability?.version!!)
+
+ addKey(context.resources.getString(R.string.nc_diagnose_signaling_mode_title))
+
+ if (userManager.currentUser.blockingGet().externalSignalingServer?.externalSignalingServer?.isNotEmpty()
+ == true
+ ) {
+ addValue(context.resources.getString(R.string.nc_diagnose_signaling_mode_extern))
+ } else {
+ addValue(context.resources.getString(R.string.nc_diagnose_signaling_mode_intern))
+ }
+ }
+
+ private fun setupPushRegistrationDiagnose() {
+ val accountId = UserIdUtils.getIdForUser(userManager.currentUser.blockingGet())
+
+ val latestPushRegistrationAtServer = arbitraryStorageManager.getStorageSetting(
+ accountId,
+ LATEST_PUSH_REGISTRATION_AT_SERVER,
+ ""
+ ).blockingGet()?.value
+
+ addKey(context.resources.getString(R.string.nc_diagnose_latest_push_registration_at_server))
+ if (latestPushRegistrationAtServer.isNullOrEmpty()) {
+ addValue(context.resources.getString(R.string.nc_diagnose_latest_push_registration_at_server_fail))
+ } else {
+ addValue(DisplayUtils.unixTimeToHumanReadable(latestPushRegistrationAtServer.toLong()))
+ }
+
+ val latestPushRegistrationAtPushProxy = arbitraryStorageManager.getStorageSetting(
+ accountId,
+ LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY,
+ ""
+ ).blockingGet()?.value
+
+ addKey(context.resources.getString(R.string.nc_diagnose_latest_push_registration_at_push_proxy))
+ if (latestPushRegistrationAtPushProxy.isNullOrEmpty()) {
+ addValue(context.resources.getString(R.string.nc_diagnose_latest_push_registration_at_push_proxy_fail))
+ } else {
+ addValue(DisplayUtils.unixTimeToHumanReadable(latestPushRegistrationAtPushProxy.toLong()))
+ }
+ }
+
+ private fun getDeviceName(): String =
+ if (MODEL.startsWith(MANUFACTURER, ignoreCase = true)) {
+ MODEL
+ } else {
+ "$MANUFACTURER $MODEL"
+ }
+
+ private fun translateBoolean(answer: Boolean?): String {
+ return when (answer) {
+ null -> context.resources.getString(R.string.nc_common_unknown)
+ true -> context.resources.getString(R.string.nc_yes)
+ else -> context.resources.getString(R.string.nc_no)
+ }
+ }
+
+ @Suppress("MagicNumber")
+ private fun createLayoutFromMarkdown() {
+ val standardMargin = 16
+ val halfMargin = 8
+ val standardPadding = 16
+
+ binding.diagnoseContentWrapper.removeAllViews()
+
+ markdownText.lines().forEach {
+ if (it.startsWith(MARKDOWN_HEADLINE)) {
+ val headline = TextView(context, null, 0)
+ headline.textSize = 2.0f
+ headline.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ context.resources.getDimension(R.dimen.headline_text_size)
+ )
+ headline.setTypeface(null, Typeface.BOLD)
+ headline.text = it.removeRange(0, 4)
+
+ binding.diagnoseContentWrapper.addView(headline)
+
+ headline.updateLayoutParams {
+ setMargins(0, standardMargin, 0, standardMargin)
+ }
+ headline.setPadding(0, standardPadding, 0, standardPadding)
+
+ viewThemeUtils.platform.colorTextView(headline)
+ } else if (it.startsWith(MARKDOWN_BOLD)) {
+ val key = TextView(context, null, 0)
+ key.setTextColor(resources.getColor(R.color.high_emphasis_text, null))
+ key.setTypeface(null, Typeface.BOLD)
+ key.text = it.replace(MARKDOWN_BOLD, "")
+
+ binding.diagnoseContentWrapper.addView(key)
+
+ key.updateLayoutParams {
+ setMargins(0, 0, 0, halfMargin)
+ }
+ } else if (it.isNotEmpty()) {
+ val value = TextView(context, null, 0)
+ value.setTextColor(resources.getColor(R.color.high_emphasis_text, null))
+ value.text = it
+
+ binding.diagnoseContentWrapper.addView(value)
+
+ value.updateLayoutParams {
+ setMargins(0, 0, 0, standardMargin)
+ }
+ value.setPadding(0, 0, 0, standardPadding)
+ }
+ }
+ }
+
+ private fun addHeadline(text: String) {
+ markdownText.append("$MARKDOWN_HEADLINE $text")
+ markdownText.append("\n\n")
+ }
+
+ private fun addKey(text: String) {
+ markdownText.bold { append("$MARKDOWN_BOLD$text$MARKDOWN_BOLD") }
+ markdownText.append("\n\n")
+ }
+
+ private fun addValue(text: String) {
+ markdownText.append(text)
+ markdownText.append("\n\n")
+ }
+
+ companion object {
+ val TAG = DiagnoseActivity::class.java.simpleName
+ private const val MARKDOWN_HEADLINE = "###"
+ private const val MARKDOWN_BOLD = "**"
+ private const val ORIGINAL_NEXTCLOUD_TALK_APPLICATION_ID = "com.nextcloud.talk2"
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesWorker.java
index 4fe5c4f322..fc2c80402d 100644
--- a/app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesWorker.java
+++ b/app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesWorker.java
@@ -80,6 +80,7 @@ private void updateUser(CapabilitiesOverall capabilitiesOverall, User user) {
capabilitiesOverall.getOcs().getData().getCapabilities() != null) {
user.setCapabilities(capabilitiesOverall.getOcs().getData().getCapabilities());
+ user.setServerVersion(capabilitiesOverall.getOcs().getData().getServerVersion());
try {
int rowsCount = userManager.updateOrCreateUser(user).blockingGet();
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt
index c15d65546a..23026a8ad9 100644
--- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt
+++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt
@@ -3,6 +3,8 @@
*
* @author Mario Danic
* @author Tim Krüger
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe
* Copyright (C) 2022 Tim Krüger
* Copyright (C) 2017-2018 Mario Danic
*
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.kt
index ff15869db5..1b0560afa6 100644
--- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.kt
+++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.kt
@@ -29,9 +29,11 @@ import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class CapabilitiesList(
+ @JsonField(name = ["version"])
+ var serverVersion: ServerVersion?,
@JsonField(name = ["capabilities"])
var capabilities: Capabilities?
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
- constructor() : this(null)
+ constructor() : this(null, null)
}
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ServerVersion.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ServerVersion.kt
new file mode 100644
index 0000000000..0685e06900
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ServerVersion.kt
@@ -0,0 +1,41 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2024 Marcel Hibbe
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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.models.json.capabilities
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+@JsonObject
+data class ServerVersion(
+ @JsonField(name = ["major"])
+ var major: Int = 0,
+ @JsonField(name = ["minor"])
+ var minor: Int = 0,
+ @JsonField(name = ["micro"])
+ var micro: Int = 0,
+ @JsonField(name = ["string"])
+ var versionString: String? = null
+) : Parcelable {
+ // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+ constructor() : this(0, 0, 0, null)
+}
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.kt
index 21078568be..9fa43fcbb0 100644
--- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.kt
+++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.kt
@@ -45,8 +45,10 @@ data class SpreedCapability(
@Contextual
Any
>
- >?
+ >?,
+ @JsonField(name = ["version"])
+ var version: String
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
- constructor() : this(null, null)
+ constructor() : this(null, null, "")
}
diff --git a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt
index 2bf2c3d47d..f70b6e7d04 100644
--- a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt
@@ -3,11 +3,13 @@
*
* @author Andy Scherzinger
* @author Mario Danic
+ * @author Marcel Hibbe
* @author Tim Krüger
* @author Ezhil Shanmugham
* Copyright (C) 2021 Tim Krüger
* Copyright (C) 2021-2022 Andy Scherzinger
* Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
+ * Copyright (C) 2023 Marcel Hibbe
* Copyright (C) 2023 Ezhil Shanmugham
*
* This program is free software: you can redistribute it and/or modify
@@ -25,6 +27,7 @@
*/
package com.nextcloud.talk.settings
+import android.Manifest
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
@@ -56,6 +59,7 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
import androidx.core.view.ViewCompat
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
@@ -72,8 +76,10 @@ import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.setAppTheme
+import com.nextcloud.talk.conversationlist.ConversationsListActivity
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivitySettingsBinding
+import com.nextcloud.talk.diagnose.DiagnoseActivity
import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.jobs.CapabilitiesWorker
import com.nextcloud.talk.jobs.ContactAddressBookWorker
@@ -84,6 +90,7 @@ import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.profile.ProfileActivity
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.LoggingUtils.sendMailWithAttachment
import com.nextcloud.talk.utils.NotificationUtils
@@ -92,6 +99,8 @@ import com.nextcloud.talk.utils.NotificationUtils.getMessageRingtoneUri
import com.nextcloud.talk.utils.SecurityUtils
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
+import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
+import com.nextcloud.talk.utils.power.PowerManagerUtils
import com.nextcloud.talk.utils.preferences.AppPreferencesImpl
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
import io.reactivex.Observer
@@ -124,6 +133,9 @@ class SettingsActivity : BaseActivity() {
@Inject
lateinit var currentUserProvider: CurrentUserProviderNew
+ @Inject
+ lateinit var platformPermissionUtil: PlatformPermissionUtil
+
private var currentUser: User? = null
private var credentials: String? = null
private lateinit var proxyTypeFlow: Flow
@@ -163,12 +175,11 @@ class SettingsActivity : BaseActivity() {
resources!!.getString(R.string.nc_app_product_name)
)
+ setupDiagnose()
setupPrivacyUrl()
setupSourceCodeUrl()
binding.settingsVersionSummary.text = String.format("v" + BuildConfig.VERSION_NAME)
- setupSoundSettings()
-
setupPhoneBookIntegration()
setupClientCertView()
@@ -193,18 +204,7 @@ class SettingsActivity : BaseActivity() {
setupCheckables()
setupScreenLockSetting()
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- binding.settingsNotificationsTitle.text = resources!!.getString(
- R.string.nc_settings_notification_sounds_post_oreo
- )
- }
-
- val callRingtoneUri = getCallRingtoneUri(context, (appPreferences))
- binding.callsRingtone.text = getRingtoneName(context, callRingtoneUri)
- val messageRingtoneUri = getMessageRingtoneUri(context, (appPreferences))
- binding.messagesRingtone.text = getRingtoneName(context, messageRingtoneUri)
-
+ setupNotificationSettings()
setupProxyTypeSettings()
setupProxyCredentialSettings()
registerChangeListeners()
@@ -273,7 +273,112 @@ class SettingsActivity : BaseActivity() {
}
}
- private fun setupSoundSettings() {
+ private fun setupNotificationSettings() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ binding.settingsNotificationsTitle.text = resources!!.getString(
+ R.string.nc_settings_notification_sounds_post_oreo
+ )
+ }
+ setupNotificationSoundsSettings()
+ setupNotificationPermissionSettings()
+ }
+
+ @Suppress("LongMethod")
+ private fun setupNotificationPermissionSettings() {
+ if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) {
+ binding.settingsGplayOnlyWrapper.visibility = View.VISIBLE
+
+ setTroubleshootingClickListenersIfNecessary()
+
+ if (PowerManagerUtils().isIgnoringBatteryOptimizations()) {
+ binding.batteryOptimizationIgnored.text =
+ resources!!.getString(R.string.nc_diagnose_battery_optimization_ignored)
+ binding.batteryOptimizationIgnored.setTextColor(
+ resources.getColor(R.color.high_emphasis_text, null)
+ )
+ } else {
+ binding.batteryOptimizationIgnored.text =
+ resources!!.getString(R.string.nc_diagnose_battery_optimization_not_ignored)
+ binding.batteryOptimizationIgnored.setTextColor(resources.getColor(R.color.nc_darkRed, null))
+
+ binding.settingsBatteryOptimizationWrapper.setOnClickListener {
+ val dialogText = String.format(
+ context.resources.getString(R.string.nc_ignore_battery_optimization_dialog_text),
+ context.resources.getString(R.string.nc_app_name)
+ )
+
+ val dialogBuilder = MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.nc_ignore_battery_optimization_dialog_title)
+ .setMessage(dialogText)
+ .setPositiveButton(R.string.nc_ok) { _, _ ->
+ startActivity(
+ Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
+ )
+ }
+ .setNegativeButton(R.string.nc_common_dismiss, null)
+ viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder)
+ val dialog = dialogBuilder.show()
+ viewThemeUtils.platform.colorTextButtons(
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE),
+ dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
+ )
+ }
+ }
+
+ // handle notification permission on API level >= 33
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (platformPermissionUtil.isPostNotificationsPermissionGranted()) {
+ binding.ncDiagnoseNotificationPermissionSubtitle.text =
+ resources.getString(R.string.nc_settings_notifications_granted)
+ binding.ncDiagnoseNotificationPermissionSubtitle.setTextColor(
+ resources.getColor(R.color.high_emphasis_text, null)
+ )
+ } else {
+ binding.ncDiagnoseNotificationPermissionSubtitle.text =
+ resources.getString(R.string.nc_settings_notifications_declined)
+ binding.ncDiagnoseNotificationPermissionSubtitle.setTextColor(
+ resources.getColor(R.color.nc_darkRed, null)
+ )
+ binding.settingsNotificationsPermissionWrapper.setOnClickListener {
+ requestPermissions(
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ ConversationsListActivity.REQUEST_POST_NOTIFICATIONS_PERMISSION
+ )
+ }
+ }
+ } else {
+ binding.settingsNotificationsPermissionWrapper.visibility = View.GONE
+ }
+ } else {
+ binding.settingsGplayOnlyWrapper.visibility = View.GONE
+ binding.settingsGplayNotAvailable.visibility = View.VISIBLE
+ }
+ }
+
+ private fun setupNotificationSoundsSettings() {
+ if (NotificationUtils.isCallsNotificationChannelEnabled(this)) {
+ val callRingtoneUri = getCallRingtoneUri(context, (appPreferences))
+
+ binding.callsRingtone.setTextColor(resources.getColor(R.color.high_emphasis_text, null))
+ binding.callsRingtone.text = getRingtoneName(context, callRingtoneUri)
+ } else {
+ binding.callsRingtone.setTextColor(
+ ResourcesCompat.getColor(context.resources, R.color.nc_darkRed, null)
+ )
+ binding.callsRingtone.text = resources!!.getString(R.string.nc_common_disabled)
+ }
+
+ if (NotificationUtils.isMessagesNotificationChannelEnabled(this)) {
+ val messageRingtoneUri = getMessageRingtoneUri(context, (appPreferences))
+ binding.messagesRingtone.setTextColor(resources.getColor(R.color.high_emphasis_text, null))
+ binding.messagesRingtone.text = getRingtoneName(context, messageRingtoneUri)
+ } else {
+ binding.messagesRingtone.setTextColor(
+ ResourcesCompat.getColor(context.resources, R.color.nc_darkRed, null)
+ )
+ binding.messagesRingtone.text = resources!!.getString(R.string.nc_common_disabled)
+ }
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
binding.settingsCallSound.setOnClickListener {
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
@@ -295,7 +400,53 @@ class SettingsActivity : BaseActivity() {
startActivity(intent)
}
} else {
- Log.e(TAG, "setupSoundSettings currently not supported for versions < Build.VERSION_CODES.O")
+ Log.w(TAG, "setupSoundSettings currently not supported for versions < Build.VERSION_CODES.O")
+ }
+ }
+
+ private fun setTroubleshootingClickListenersIfNecessary() {
+ fun click() {
+ val dialogBuilder = MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.nc_notifications_troubleshooting_dialog_title)
+ .setMessage(R.string.nc_notifications_troubleshooting_dialog_text)
+ .setNegativeButton(R.string.nc_diagnose_dialog_open_checklist) { _, _ ->
+ startActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(resources.getString(R.string.notification_checklist_url))
+ )
+ )
+ }
+ .setPositiveButton(R.string.nc_diagnose_dialog_open_dontkillmyapp_website) { _, _ ->
+ startActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(resources.getString(R.string.dontkillmyapp_url))
+ )
+ )
+ }
+ .setNeutralButton(R.string.nc_diagnose_dialog_open_diagnose) { _, _ ->
+ val intent = Intent(context, DiagnoseActivity::class.java)
+ startActivity(intent)
+ }
+ viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder)
+ val dialog = dialogBuilder.show()
+ viewThemeUtils.platform.colorTextButtons(
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE),
+ dialog.getButton(AlertDialog.BUTTON_NEGATIVE),
+ dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
+ )
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (platformPermissionUtil.isPostNotificationsPermissionGranted() &&
+ PowerManagerUtils().isIgnoringBatteryOptimizations()
+ ) {
+ binding.settingsNotificationsPermissionWrapper.setOnClickListener { click() }
+ binding.settingsBatteryOptimizationWrapper.setOnClickListener { click() }
+ }
+ } else if (PowerManagerUtils().isIgnoringBatteryOptimizations()) {
+ binding.settingsBatteryOptimizationWrapper.setOnClickListener { click() }
}
}
@@ -314,6 +465,13 @@ class SettingsActivity : BaseActivity() {
}
}
+ private fun setupDiagnose() {
+ binding.diagnoseWrapper.setOnClickListener {
+ val intent = Intent(context, DiagnoseActivity::class.java)
+ startActivity(intent)
+ }
+ }
+
private fun setupPrivacyUrl() {
if (!TextUtils.isEmpty(resources!!.getString(R.string.nc_privacy_url))) {
binding.settingsPrivacy.setOnClickListener {
@@ -490,6 +648,7 @@ class SettingsActivity : BaseActivity() {
).show()
restartApp()
}
+
WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> {
Toast.makeText(
context,
@@ -915,22 +1074,39 @@ class SettingsActivity : BaseActivity() {
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- if (requestCode == ContactAddressBookWorker.REQUEST_PERMISSION &&
- grantResults.isNotEmpty() &&
- grantResults[0] == PackageManager.PERMISSION_GRANTED
- ) {
- WorkManager
- .getInstance(this)
- .enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java).build())
- checkForPhoneNumber()
- } else {
- appPreferences.setPhoneBookIntegration(false)
- binding.settingsPhoneBookIntegrationSwitch.isChecked = appPreferences.isPhoneBookIntegrationEnabled
- Snackbar.make(
- binding.root,
- context.resources.getString(R.string.no_phone_book_integration_due_to_permissions),
- Snackbar.LENGTH_LONG
- ).show()
+
+ when (requestCode) {
+ ContactAddressBookWorker.REQUEST_PERMISSION -> {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ WorkManager
+ .getInstance(this)
+ .enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java).build())
+ checkForPhoneNumber()
+ } else {
+ appPreferences.setPhoneBookIntegration(false)
+ binding.settingsPhoneBookIntegrationSwitch.isChecked = appPreferences.isPhoneBookIntegrationEnabled
+ Snackbar.make(
+ binding.root,
+ context.resources.getString(R.string.no_phone_book_integration_due_to_permissions),
+ Snackbar.LENGTH_LONG
+ ).show()
+ }
+ }
+
+ ConversationsListActivity.REQUEST_POST_NOTIFICATIONS_PERMISSION -> {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_DENIED) {
+ Snackbar.make(
+ binding.root,
+ context.resources.getString(R.string.nc_settings_notifications_declined_hint),
+ Snackbar.LENGTH_LONG
+ ).show()
+ Log.d(
+ TAG,
+ "Notification permission is denied. Either because user denied it when being asked. " +
+ "Or permission is already denied and android decided to not offer the dialog."
+ )
+ }
+ }
}
}
@@ -1140,6 +1316,7 @@ class SettingsActivity : BaseActivity() {
Snackbar.LENGTH_LONG
).show()
}
+
else -> {
textInputLayout.helperText = context.resources.getString(
R.string.nc_settings_phone_book_integration_phone_number_dialog_invalid
diff --git a/app/src/main/java/com/nextcloud/talk/users/UserManager.kt b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt
index 378cb81129..0af086cb66 100644
--- a/app/src/main/java/com/nextcloud/talk/users/UserManager.kt
+++ b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt
@@ -27,6 +27,7 @@ import com.nextcloud.talk.data.user.UsersRepository
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.ExternalSignalingServer
import com.nextcloud.talk.models.json.capabilities.Capabilities
+import com.nextcloud.talk.models.json.capabilities.ServerVersion
import com.nextcloud.talk.models.json.push.PushConfigurationState
import io.reactivex.Maybe
import io.reactivex.Observable
@@ -194,6 +195,10 @@ class UserManager internal constructor(private val userRepository: UsersReposito
user.capabilities = LoganSquare
.parse(userAttributes.capabilities, Capabilities::class.java)
}
+ if (userAttributes.serverVersion != null) {
+ user.serverVersion = LoganSquare
+ .parse(userAttributes.serverVersion, ServerVersion::class.java)
+ }
user.clientCertificate = userAttributes.certificateAlias
if (userAttributes.externalSignalingServer != null) {
user.externalSignalingServer = LoganSquare
@@ -220,6 +225,9 @@ class UserManager internal constructor(private val userRepository: UsersReposito
if (!TextUtils.isEmpty(userAttributes.capabilities)) {
user.capabilities = LoganSquare.parse(userAttributes.capabilities, Capabilities::class.java)
}
+ if (!TextUtils.isEmpty(userAttributes.serverVersion)) {
+ user.serverVersion = LoganSquare.parse(userAttributes.serverVersion, ServerVersion::class.java)
+ }
if (!TextUtils.isEmpty(userAttributes.certificateAlias)) {
user.clientCertificate = userAttributes.certificateAlias
}
@@ -248,6 +256,7 @@ class UserManager internal constructor(private val userRepository: UsersReposito
val displayName: String?,
val pushConfigurationState: String?,
val capabilities: String?,
+ val serverVersion: String?,
val certificateAlias: String?,
val externalSignalingServer: String?
)
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 68ff86ca43..ef9d0400aa 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
@@ -34,6 +34,7 @@ import android.os.Build
import android.service.notification.StatusBarNotification
import android.text.TextUtils
import android.util.Log
+import androidx.core.app.NotificationManagerCompat
import androidx.core.graphics.drawable.IconCompat
import coil.executeBlocking
import coil.imageLoader
@@ -275,6 +276,30 @@ object NotificationUtils {
return isVisible
}
+ fun isCallsNotificationChannelEnabled(context: Context): Boolean {
+ val channel = getNotificationChannel(context, NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name)
+ if (channel != null) {
+ return isNotificationChannelEnabled(context, channel)
+ }
+ return false
+ }
+
+ fun isMessagesNotificationChannelEnabled(context: Context): Boolean {
+ val channel = getNotificationChannel(context, NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name)
+ if (channel != null) {
+ return isNotificationChannelEnabled(context, channel)
+ }
+ return false
+ }
+
+ private fun isNotificationChannelEnabled(context: Context, channel: NotificationChannel): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ channel.importance != NotificationManager.IMPORTANCE_NONE
+ } else {
+ NotificationManagerCompat.from(context).areNotificationsEnabled()
+ }
+ }
+
private fun getRingtoneUri(
context: Context,
ringtonePreferencesString: String?,
diff --git a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt
index 5e7dbe4e1a..0cfac75b7e 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt
@@ -31,6 +31,7 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.events.EventStatus
import com.nextcloud.talk.models.SignatureVerification
@@ -73,6 +74,9 @@ class PushUtils {
@Inject
lateinit var appPreferences: AppPreferences
+ @Inject
+ lateinit var arbitraryStorageManager: ArbitraryStorageManager
+
@JvmField
@Inject
var eventBus: EventBus? = null
@@ -243,6 +247,13 @@ class PushUtils {
}
override fun onNext(pushRegistrationOverall: PushRegistrationOverall) {
+ arbitraryStorageManager.storeStorageSetting(
+ getIdForUser(user),
+ LATEST_PUSH_REGISTRATION_AT_SERVER,
+ System.currentTimeMillis().toString(),
+ ""
+ )
+
Log.d(TAG, "pushTokenHash successfully registered at nextcloud server.")
val proxyMap: MutableMap = HashMap()
proxyMap["pushToken"] = token
@@ -273,6 +284,13 @@ class PushUtils {
override fun onNext(t: Unit) {
try {
+ arbitraryStorageManager.storeStorageSetting(
+ getIdForUser(user),
+ LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY,
+ System.currentTimeMillis().toString(),
+ ""
+ )
+
Log.d(TAG, "pushToken successfully registered at pushproxy.")
updatePushStateForUser(proxyMap, user)
} catch (e: IOException) {
@@ -396,5 +414,7 @@ class PushUtils {
companion object {
private const val TAG = "PushUtils"
+ const val LATEST_PUSH_REGISTRATION_AT_SERVER: String = "LATEST_PUSH_REGISTRATION_AT_SERVER"
+ const val LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY: String = "LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY"
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.java b/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.java
deleted file mode 100644
index 97e952fef6..0000000000
--- a/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * 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 .
- *
- * This class is in part based on the code from the great people that wrote Signal
- * https://github.com/signalapp/Signal-Android/raw/f9adb4e4554a44fd65b77320e34bf4bccf7924ce/src/org/thoughtcrime/securesms/webrtc/locks/LockManager.java
- */
-
-package com.nextcloud.talk.utils.power;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.net.wifi.WifiManager;
-import android.os.PowerManager;
-import android.provider.Settings;
-
-import com.nextcloud.talk.application.NextcloudTalkApplication;
-
-import javax.inject.Inject;
-
-import autodagger.AutoInjector;
-
-@AutoInjector(NextcloudTalkApplication.class)
-
-public class PowerManagerUtils {
- private static final String TAG = "PowerManagerUtils";
- private final PowerManager.WakeLock fullLock;
- private final PowerManager.WakeLock partialLock;
- private final WifiManager.WifiLock wifiLock;
- private final boolean wifiLockEnforced;
- @Inject
- Context context;
- private ProximityLock proximityLock;
- private boolean proximityDisabled = false;
-
- private int orientation;
-
- public PowerManagerUtils() {
- NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
-
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- fullLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "nctalk:fullwakelock");
- partialLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "nctalk:partialwakelock");
- proximityLock = new ProximityLock(pm);
-
- // we suppress a possible leak because this is indeed application context
- @SuppressLint("WifiManagerPotentialLeak") WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
- wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "nctalk:wifiwakelock");
-
- fullLock.setReferenceCounted(false);
- partialLock.setReferenceCounted(false);
- wifiLock.setReferenceCounted(false);
-
- wifiLockEnforced = isWifiPowerActiveModeEnabled(context);
- orientation = context.getResources().getConfiguration().orientation;
- }
-
- public void setOrientation(int newOrientation) {
- orientation = newOrientation;
- updateInCallWakeLockState();
- }
-
- public void updatePhoneState(PhoneState state) {
- switch (state) {
- case IDLE:
- setWakeLockState(WakeLockState.SLEEP);
- break;
- case PROCESSING:
- setWakeLockState(WakeLockState.PARTIAL);
- break;
- case INTERACTIVE:
- setWakeLockState(WakeLockState.FULL);
- break;
- case WITH_PROXIMITY_SENSOR_LOCK:
- proximityDisabled = false;
- updateInCallWakeLockState();
- break;
- case WITHOUT_PROXIMITY_SENSOR_LOCK:
- proximityDisabled = true;
- updateInCallWakeLockState();
- break;
- }
- }
-
- private void updateInCallWakeLockState() {
- if (orientation != Configuration.ORIENTATION_LANDSCAPE && wifiLockEnforced && !proximityDisabled) {
- setWakeLockState(WakeLockState.PROXIMITY);
- } else {
- setWakeLockState(WakeLockState.FULL);
- }
- }
-
- private boolean isWifiPowerActiveModeEnabled(Context context) {
- int wifi_pwr_active_mode = Settings.Secure.getInt(context.getContentResolver(), "wifi_pwr_active_mode", -1);
- return (wifi_pwr_active_mode != 0);
- }
-
- @SuppressLint("WakelockTimeout")
- private synchronized void setWakeLockState(WakeLockState newState) {
- switch (newState) {
- case FULL:
- if (!fullLock.isHeld()) {
- fullLock.acquire();
- }
-
- if (!partialLock.isHeld()) {
- partialLock.acquire();
- }
-
- if (!wifiLock.isHeld()) {
- wifiLock.acquire();
- }
- proximityLock.release();
- break;
- case PARTIAL:
- if (!partialLock.isHeld()) {
- partialLock.acquire();
- }
-
- if (!wifiLock.isHeld()) {
- wifiLock.acquire();
- }
-
- fullLock.release();
- proximityLock.release();
- break;
- case SLEEP:
- fullLock.release();
- partialLock.release();
- wifiLock.release();
- proximityLock.release();
- break;
- case PROXIMITY:
- if (!partialLock.isHeld()) {
- partialLock.acquire();
- }
-
- if (!wifiLock.isHeld()) {
- wifiLock.acquire();
- }
-
- fullLock.release(
-
- );
- proximityLock.acquire();
- break;
- default:
- // something went very very wrong
- }
- }
-
- public enum PhoneState {
- IDLE,
- PROCESSING, //used when the phone is active but before the user should be alerted.
- INTERACTIVE,
- WITHOUT_PROXIMITY_SENSOR_LOCK,
- WITH_PROXIMITY_SENSOR_LOCK
- }
-
- public enum WakeLockState {
- FULL,
- PARTIAL,
- SLEEP,
- PROXIMITY
- }
-}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.kt
new file mode 100644
index 0000000000..6ae1b67380
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.kt
@@ -0,0 +1,189 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * Copyright (C) 2023 Marcel Hibbe
+ * 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 .
+ *
+ * This class is in part based on the code from the great people that wrote Signal
+ * https://github.com/signalapp/Signal-Android/raw/f9adb4e4554a44fd65b77320e34bf4bccf7924ce/src/org/thoughtcrime/securesms/webrtc/locks/LockManager.java
+ */
+package com.nextcloud.talk.utils.power
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Context.POWER_SERVICE
+import android.content.res.Configuration
+import android.net.wifi.WifiManager
+import android.net.wifi.WifiManager.WifiLock
+import android.os.PowerManager
+import android.provider.Settings
+import autodagger.AutoInjector
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class PowerManagerUtils {
+ private val fullLock: PowerManager.WakeLock
+ private val partialLock: PowerManager.WakeLock
+ private val wifiLock: WifiLock
+ private val wifiLockEnforced: Boolean
+
+ @JvmField
+ @Inject
+ var context: Context? = null
+ private val proximityLock: ProximityLock
+ private var proximityDisabled = false
+ private var orientation: Int
+
+ init {
+ sharedApplication!!.componentApplication.inject(this)
+ val pm = context!!.getSystemService(Context.POWER_SERVICE) as PowerManager
+ fullLock = pm.newWakeLock(
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
+ "nctalk:fullwakelock"
+ )
+ partialLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "nctalk:partialwakelock")
+ proximityLock = ProximityLock(pm)
+
+ // we suppress a possible leak because this is indeed application context
+ @SuppressLint("WifiManagerPotentialLeak")
+ val wm =
+ context!!.getSystemService(Context.WIFI_SERVICE) as WifiManager
+ wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "nctalk:wifiwakelock")
+ fullLock.setReferenceCounted(false)
+ partialLock.setReferenceCounted(false)
+ wifiLock.setReferenceCounted(false)
+ wifiLockEnforced = isWifiPowerActiveModeEnabled(context)
+ orientation = context!!.resources.configuration.orientation
+ }
+
+ fun isIgnoringBatteryOptimizations(): Boolean {
+ val packageName = context!!.packageName
+ val pm = context!!.getSystemService(POWER_SERVICE) as PowerManager
+ return pm.isIgnoringBatteryOptimizations(packageName)
+ }
+
+ fun setOrientation(newOrientation: Int) {
+ orientation = newOrientation
+ updateInCallWakeLockState()
+ }
+
+ fun updatePhoneState(state: PhoneState?) {
+ when (state) {
+ PhoneState.IDLE -> setWakeLockState(WakeLockState.SLEEP)
+ PhoneState.PROCESSING -> setWakeLockState(WakeLockState.PARTIAL)
+ PhoneState.INTERACTIVE -> setWakeLockState(WakeLockState.FULL)
+ PhoneState.WITH_PROXIMITY_SENSOR_LOCK -> {
+ proximityDisabled = false
+ updateInCallWakeLockState()
+ }
+
+ PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK -> {
+ proximityDisabled = true
+ updateInCallWakeLockState()
+ }
+
+ else -> {}
+ }
+ }
+
+ private fun updateInCallWakeLockState() {
+ if (orientation != Configuration.ORIENTATION_LANDSCAPE && wifiLockEnforced && !proximityDisabled) {
+ setWakeLockState(WakeLockState.PROXIMITY)
+ } else {
+ setWakeLockState(WakeLockState.FULL)
+ }
+ }
+
+ private fun isWifiPowerActiveModeEnabled(context: Context?): Boolean {
+ val wifiPowerActiveMode = Settings.Secure.getInt(context!!.contentResolver, "wifi_pwr_active_mode", -1)
+ return wifiPowerActiveMode != 0
+ }
+
+ @SuppressLint("WakelockTimeout")
+ @Synchronized
+ private fun setWakeLockState(newState: WakeLockState) {
+ when (newState) {
+ WakeLockState.FULL -> {
+ if (!fullLock.isHeld) {
+ fullLock.acquire()
+ }
+ if (!partialLock.isHeld) {
+ partialLock.acquire()
+ }
+ if (!wifiLock.isHeld) {
+ wifiLock.acquire()
+ }
+ proximityLock.release()
+ }
+
+ WakeLockState.PARTIAL -> {
+ if (!partialLock.isHeld) {
+ partialLock.acquire()
+ }
+ if (!wifiLock.isHeld) {
+ wifiLock.acquire()
+ }
+ fullLock.release()
+ proximityLock.release()
+ }
+
+ WakeLockState.SLEEP -> {
+ fullLock.release()
+ partialLock.release()
+ wifiLock.release()
+ proximityLock.release()
+ }
+
+ WakeLockState.PROXIMITY -> {
+ if (!partialLock.isHeld) {
+ partialLock.acquire()
+ }
+ if (!wifiLock.isHeld) {
+ wifiLock.acquire()
+ }
+ fullLock.release()
+ proximityLock.acquire()
+ }
+
+ else -> {}
+ }
+ }
+
+ enum class PhoneState {
+ IDLE,
+ PROCESSING,
+
+ // used when the phone is active but before the user should be alerted.
+ INTERACTIVE,
+ WITHOUT_PROXIMITY_SENSOR_LOCK,
+ WITH_PROXIMITY_SENSOR_LOCK
+ }
+
+ enum class WakeLockState {
+ FULL,
+ PARTIAL,
+ SLEEP,
+ PROXIMITY
+ }
+
+ companion object {
+ private val TAG = PowerManagerUtils::class.java.simpleName
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java
index 3097b3037a..c3ed53fea3 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java
@@ -69,6 +69,14 @@ public interface AppPreferences {
void setPushToken(String pushToken);
+ Long getPushTokenLatestGeneration();
+
+ void setPushTokenLatestGeneration(Long date);
+
+ Long getPushTokenLatestFetch();
+
+ void setPushTokenLatestFetch(Long date);
+
void removePushToken();
String getTemporaryClientCertAlias();
diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt
index c096044ebd..084d284974 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt
@@ -159,6 +159,28 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
pushToken = ""
}
+ override fun getPushTokenLatestGeneration(): Long {
+ return runBlocking { async { readLong(PUSH_TOKEN_LATEST_GENERATION).first() } }.getCompleted()
+ }
+
+ override fun setPushTokenLatestGeneration(date: Long) =
+ runBlocking {
+ async {
+ writeLong(PUSH_TOKEN_LATEST_GENERATION, date)
+ }
+ }
+
+ override fun getPushTokenLatestFetch(): Long {
+ return runBlocking { async { readLong(PUSH_TOKEN_LATEST_FETCH).first() } }.getCompleted()
+ }
+
+ override fun setPushTokenLatestFetch(date: Long) =
+ runBlocking {
+ async {
+ writeLong(PUSH_TOKEN_LATEST_FETCH, date)
+ }
+ }
+
override fun getTemporaryClientCertAlias(): String {
return runBlocking { async { readString(TEMP_CLIENT_CERT_ALIAS).first() } }.getCompleted()
}
@@ -512,6 +534,8 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
const val PROXY_USERNAME = "proxy_username"
const val PROXY_PASSWORD = "proxy_password"
const val PUSH_TOKEN = "push_token"
+ const val PUSH_TOKEN_LATEST_GENERATION = "push_token_latest_generation"
+ const val PUSH_TOKEN_LATEST_FETCH = "push_token_latest_fetch"
const val TEMP_CLIENT_CERT_ALIAS = "tempClientCertAlias"
const val PUSH_TO_TALK_INTRO_SHOWN = "pushToTalk_intro_shown"
const val CALL_RINGTONE = "call_ringtone"
diff --git a/app/src/main/res/layout/activity_diagnose.xml b/app/src/main/res/layout/activity_diagnose.xml
new file mode 100644
index 0000000000..8cd04e69a2
--- /dev/null
+++ b/app/src/main/res/layout/activity_diagnose.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index 16aad93cc3..5ea27fe57e 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -7,7 +7,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/setup.xml b/app/src/main/res/values/setup.xml
index 7d3e5bd5a3..34ca74546e 100644
--- a/app/src/main/res/values/setup.xml
+++ b/app/src/main/res/values/setup.xml
@@ -5,7 +5,7 @@
~ @author Mario Danic
~ @author Marcel Hibbe
~ Copyright (C) 2017-2019 Mario Danic
- ~ Copyright (C) 2022 Marcel Hibbe
+ ~ Copyright (C) 2022-2023 Marcel Hibbe
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
@@ -43,6 +43,7 @@
https://nextcloud.com/privacy/
https://www.gnu.org/licenses/gpl-3.0.en.html
https://github.com/nextcloud/talk-android
+ https://github.com/nextcloud/talk-android/issues
https://nextcloud.com/providers
@@ -61,6 +62,9 @@
AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s
nextcloud-a7dea.appspot.com
+ https://github.com/nextcloud/talk-android/blob/master/docs/notifications.md
+ https://dontkillmyapp.com/
+
https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
OpenStreetMap contributors
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f60b4e5bda..586c14d0fb 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -6,7 +6,7 @@
~ @author Marcel Hibbe
~ @author Tim Krüger
~ Copyright (C) 2022 Tim Krüger
- ~ Copyright (C) 2022-2023 Marcel Hibbe
+ ~ Copyright (C) 2022-2024 Marcel Hibbe
~ Copyright (C) 2021 Andy Scherzinger
~ Copyright (C) 2017-2018 Mario Danic
~
@@ -50,6 +50,10 @@ How to translate with transifex:
Dismiss
Sorry, something went wrong!
Create
+ Unknown
+ Disabled
+ Copy
+ Copied to clipboard
Settings
@@ -180,6 +184,63 @@ How to translate with transifex:
Personal Info
+ Diagnose
+ Open Diagnose screen to check settings or create bug report
+
+ Notifications are granted
+ Notifications are declined
+ Notifications are declined. Please allow notifications in android settings
+
+
+ Notification troubleshooting
+ Notification permission and battery settings are correctly set up to receive notifications. If you have problems to receive notifications anyway, please check if the notification channels for calls and messages are enabled. Further help can be found at DontKillMyApp.com or at the troubleshooting checklist. If this does not help, please go to diagnose screen and send a bug report.
+ Open troubleshooting checklist
+ Open dontkillmyapp.com
+ Open diagnose screen
+
+ Ignore battery optimization
+ Battery optimization is not ignored. This should be changed to make sure that notifications work in the background! Please click OK and select \"All apps\" -> %1$s -> Don\'t optimize
+
+ Meta information
+ Generation of system report
+ Phone
+ Device
+ Android version
+ App
+ App name
+ App version
+ Registered users
+ Google play services
+ Build flavor
+ Google play services are available
+ Google play services are not available. Notifications are not supported
+ Battery settings
+ Battery optimization is not ignored. This should be changed!
+ Battery optimization is ignored, all fine
+ Notification permissions
+ Calls notification channel enabled?
+ Messages notification channel enabled?
+ Firebase push token
+ Latest firebase push token generation
+ Latest firebase push token fetch
+ No firebase push token set. Please create a bug report.
+ Current account
+ Server
+ User
+ Server notification app installed?
+ User status enabled?
+ Latest push registration at server
+ Not yet registered at server
+ Latest push registration at push proxy
+ Not yet registered at push proxy
+ Server version
+ Server Talk version
+ Signaling Mode
+ Internal
+ External
+ Send email
+ Create issue
+
Leave conversation
Delete all messages