add diagnose screen and permission warnings in settings

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2023-12-07 16:38:01 +01:00
parent bd58cd8a65
commit c7a72aaf4a
No known key found for this signature in database
GPG key ID: C793F8B59F43CE7B
31 changed files with 1533 additions and 237 deletions

View file

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

View file

@ -29,6 +29,7 @@ class MainActivityTest {
displayName = "Test Name", displayName = "Test Name",
pushConfigurationState = null, pushConfigurationState = null,
capabilities = null, capabilities = null,
serverVersion = null,
certificateAlias = null, certificateAlias = null,
externalSignalingServer = null externalSignalingServer = null
) )

View file

@ -57,6 +57,7 @@ class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerP
Log.d(TAG, "Fetched firebase push token is: $pushToken") Log.d(TAG, "Fetched firebase push token is: $pushToken")
appPreferences.pushToken = pushToken appPreferences.pushToken = pushToken
appPreferences.pushTokenLatestFetch = System.currentTimeMillis()
val data: Data = val data: Data =
Data.Builder().putString(PushRegistrationWorker.ORIGIN, "GetFirebasePushTokenWorker").build() Data.Builder().putString(PushRegistrationWorker.ORIGIN, "GetFirebasePushTokenWorker").build()

View file

@ -78,6 +78,7 @@ class NCFirebaseMessagingService : FirebaseMessagingService() {
Log.d(TAG, "onNewToken. token = $token") Log.d(TAG, "onNewToken. token = $token")
appPreferences.pushToken = token appPreferences.pushToken = token
appPreferences.pushTokenLatestGeneration = System.currentTimeMillis()
val data: Data = val data: Data =
Data.Builder().putString(PushRegistrationWorker.ORIGIN, "NCFirebaseMessagingService#onNewToken").build() Data.Builder().putString(PushRegistrationWorker.ORIGIN, "NCFirebaseMessagingService#onNewToken").build()

View file

@ -241,6 +241,10 @@
android:name=".settings.SettingsActivity" android:name=".settings.SettingsActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity
android:name=".diagnose.DiagnoseActivity"
android:theme="@style/AppTheme" />
<activity <activity
android:name=".conversationinfo.ConversationInfoActivity" android:name=".conversationinfo.ConversationInfoActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />

View file

@ -51,7 +51,6 @@ import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.jobs.CapabilitiesWorker import com.nextcloud.talk.jobs.CapabilitiesWorker
import com.nextcloud.talk.jobs.SignalingSettingsWorker import com.nextcloud.talk.jobs.SignalingSettingsWorker
import com.nextcloud.talk.jobs.WebsocketConnectionsWorker 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.capabilities.CapabilitiesOverall
import com.nextcloud.talk.models.json.generic.Status import com.nextcloud.talk.models.json.generic.Status
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall 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( userManager.storeProfile(
username, username,
UserManager.UserAttributes( UserManager.UserAttributes(
@ -261,7 +260,8 @@ class AccountVerificationActivity : BaseActivity() {
token = token, token = token,
displayName = displayName, displayName = displayName,
pushConfigurationState = null, pushConfigurationState = null,
capabilities = LoganSquare.serialize(capabilities), capabilities = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.capabilities),
serverVersion = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.serverVersion),
certificateAlias = appPreferences.temporaryClientCertAlias, certificateAlias = appPreferences.temporaryClientCertAlias,
externalSignalingServer = null 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( ncApi.getUserProfile(
credentials, credentials,
ApiUtils.getUrlForUserProfile(baseUrl) ApiUtils.getUrlForUserProfile(baseUrl)
@ -325,7 +325,7 @@ class AccountVerificationActivity : BaseActivity() {
storeProfile( storeProfile(
displayName, displayName,
userProfileOverall.ocs!!.data!!.userId!!, userProfileOverall.ocs!!.data!!.userId!!,
capabilities.ocs!!.data!!.capabilities!! capabilitiesOverall
) )
} else { } else {
runOnUiThread { runOnUiThread {

View file

@ -41,6 +41,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.provider.Settings
import android.text.InputType import android.text.InputType
import android.text.TextUtils import android.text.TextUtils
import android.util.Log 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.ui.dialog.FilterConversationFragment
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.FileUtils import com.nextcloud.talk.utils.FileUtils
import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.Mimetype
import com.nextcloud.talk.utils.ParticipantPermissions 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.isUnifiedSearchAvailable
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isUserStatusAvailable import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isUserStatusAvailable
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil 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.rx.SearchViewObservable.Companion.observeSearchView
import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@ -234,7 +237,8 @@ class ConversationsListActivity :
// handle notification permission on API level >= 33 // handle notification permission on API level >= 33
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
!platformPermissionUtil.isPostNotificationsPermissionGranted() !platformPermissionUtil.isPostNotificationsPermissionGranted() &&
ClosedInterfaceImpl().isGooglePlayServicesAvailable
) { ) {
requestPermissions( requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS), arrayOf(Manifest.permission.POST_NOTIFICATIONS),
@ -1269,7 +1273,9 @@ class ConversationsListActivity :
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION) {
when (requestCode) {
UploadAndShareFilesWorker.REQUEST_PERMISSION -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "upload starting after permissions were granted") Log.d(TAG, "upload starting after permissions were granted")
showSendFilesConfirmDialog() showSendFilesConfirmDialog()
@ -1281,6 +1287,43 @@ class ConversationsListActivity :
).show() ).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."
)
}
}
}
} }
private fun openConversation(textToPaste: String? = "") { private fun openConversation(textToPaste: String? = "") {

View file

@ -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.ExternalSignalingServerConverter
import com.nextcloud.talk.data.source.local.converters.HashMapHashMapConverter 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.PushConfigurationConverter
import com.nextcloud.talk.data.source.local.converters.ServerVersionConverter
import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter
import com.nextcloud.talk.data.storage.ArbitraryStoragesDao import com.nextcloud.talk.data.storage.ArbitraryStoragesDao
import com.nextcloud.talk.data.storage.model.ArbitraryStorageEntity 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.SQLiteDatabaseHook
import net.sqlcipher.database.SupportFactory import net.sqlcipher.database.SupportFactory
import java.util.Locale import java.util.Locale
import androidx.room.AutoMigration
@Database( @Database(
entities = [UserEntity::class, ArbitraryStorageEntity::class], entities = [UserEntity::class, ArbitraryStorageEntity::class],
version = 9, version = 10,
autoMigrations = [
AutoMigration(from = 9, to = 10)
],
exportSchema = true exportSchema = true
) )
@TypeConverters( @TypeConverters(
PushConfigurationConverter::class, PushConfigurationConverter::class,
CapabilitiesConverter::class, CapabilitiesConverter::class,
ServerVersionConverter::class,
ExternalSignalingServerConverter::class, ExternalSignalingServerConverter::class,
SignalingSettingsConverter::class, SignalingSettingsConverter::class,
HashMapHashMapConverter::class HashMapHashMapConverter::class

View file

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

View file

@ -41,6 +41,7 @@ object UserMapper {
entity.displayName, entity.displayName,
entity.pushConfigurationState, entity.pushConfigurationState,
entity.capabilities, entity.capabilities,
entity.serverVersion,
entity.clientCertificate, entity.clientCertificate,
entity.externalSignalingServer, entity.externalSignalingServer,
entity.current, entity.current,
@ -59,6 +60,7 @@ object UserMapper {
displayName = model.displayName displayName = model.displayName
pushConfigurationState = model.pushConfigurationState pushConfigurationState = model.pushConfigurationState
capabilities = model.capabilities capabilities = model.capabilities
serverVersion = model.serverVersion
clientCertificate = model.clientCertificate clientCertificate = model.clientCertificate
externalSignalingServer = model.externalSignalingServer externalSignalingServer = model.externalSignalingServer
current = model.current current = model.current

View file

@ -22,6 +22,7 @@ package com.nextcloud.talk.data.user.model
import android.os.Parcelable import android.os.Parcelable
import com.nextcloud.talk.models.ExternalSignalingServer import com.nextcloud.talk.models.ExternalSignalingServer
import com.nextcloud.talk.models.json.capabilities.Capabilities 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.models.json.push.PushConfigurationState
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -37,6 +38,7 @@ data class User(
var displayName: String? = null, var displayName: String? = null,
var pushConfigurationState: PushConfigurationState? = null, var pushConfigurationState: PushConfigurationState? = null,
var capabilities: Capabilities? = null, var capabilities: Capabilities? = null,
var serverVersion: ServerVersion? = null,
var clientCertificate: String? = null, var clientCertificate: String? = null,
var externalSignalingServer: ExternalSignalingServer? = null, var externalSignalingServer: ExternalSignalingServer? = null,
var current: Boolean = FALSE, var current: Boolean = FALSE,

View file

@ -28,6 +28,7 @@ import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.nextcloud.talk.models.ExternalSignalingServer import com.nextcloud.talk.models.ExternalSignalingServer
import com.nextcloud.talk.models.json.capabilities.Capabilities 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.models.json.push.PushConfigurationState
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.lang.Boolean.FALSE import java.lang.Boolean.FALSE
@ -60,6 +61,9 @@ data class UserEntity(
@ColumnInfo(name = "capabilities") @ColumnInfo(name = "capabilities")
var capabilities: Capabilities? = null, var capabilities: Capabilities? = null,
@ColumnInfo(name = "serverVersion", defaultValue = "")
var serverVersion: ServerVersion? = null,
@ColumnInfo(name = "clientCertificate") @ColumnInfo(name = "clientCertificate")
var clientCertificate: String? = null, var clientCertificate: String? = null,

View file

@ -0,0 +1,476 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.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<ViewGroup.MarginLayoutParams> {
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<ViewGroup.MarginLayoutParams> {
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<ViewGroup.MarginLayoutParams> {
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"
}
}

View file

@ -80,6 +80,7 @@ public class CapabilitiesWorker extends Worker {
capabilitiesOverall.getOcs().getData().getCapabilities() != null) { capabilitiesOverall.getOcs().getData().getCapabilities() != null) {
user.setCapabilities(capabilitiesOverall.getOcs().getData().getCapabilities()); user.setCapabilities(capabilitiesOverall.getOcs().getData().getCapabilities());
user.setServerVersion(capabilitiesOverall.getOcs().getData().getServerVersion());
try { try {
int rowsCount = userManager.updateOrCreateUser(user).blockingGet(); int rowsCount = userManager.updateOrCreateUser(user).blockingGet();

View file

@ -3,6 +3,8 @@
* *
* @author Mario Danic * @author Mario Danic
* @author Tim Krüger * @author Tim Krüger
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me> * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com> * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
* *

View file

@ -29,9 +29,11 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@JsonObject @JsonObject
data class CapabilitiesList( data class CapabilitiesList(
@JsonField(name = ["version"])
var serverVersion: ServerVersion?,
@JsonField(name = ["capabilities"]) @JsonField(name = ["capabilities"])
var capabilities: Capabilities? var capabilities: Capabilities?
) : Parcelable { ) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null) constructor() : this(null, null)
} }

View file

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

View file

@ -45,8 +45,10 @@ data class SpreedCapability(
@Contextual @Contextual
Any Any
> >
>? >?,
@JsonField(name = ["version"])
var version: String
) : Parcelable { ) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null) constructor() : this(null, null, "")
} }

View file

@ -3,11 +3,13 @@
* *
* @author Andy Scherzinger * @author Andy Scherzinger
* @author Mario Danic * @author Mario Danic
* @author Marcel Hibbe
* @author Tim Krüger * @author Tim Krüger
* @author Ezhil Shanmugham * @author Ezhil Shanmugham
* Copyright (C) 2021 Tim Krüger <t@timkrueger.me> * Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2021-2022 Andy Scherzinger <info@andy-scherzinger.de> * Copyright (C) 2021-2022 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017 Mario Danic (mario@lovelyhq.com) * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2023 Ezhil Shanmugham <ezhil56x.contact@gmail.com> * Copyright (C) 2023 Ezhil Shanmugham <ezhil56x.contact@gmail.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -25,6 +27,7 @@
*/ */
package com.nextcloud.talk.settings package com.nextcloud.talk.settings
import android.Manifest
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
@ -56,6 +59,7 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo import androidx.work.WorkInfo
@ -72,8 +76,10 @@ import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.setAppTheme 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.data.user.model.User
import com.nextcloud.talk.databinding.ActivitySettingsBinding import com.nextcloud.talk.databinding.ActivitySettingsBinding
import com.nextcloud.talk.diagnose.DiagnoseActivity
import com.nextcloud.talk.jobs.AccountRemovalWorker import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.jobs.CapabilitiesWorker import com.nextcloud.talk.jobs.CapabilitiesWorker
import com.nextcloud.talk.jobs.ContactAddressBookWorker 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.profile.ProfileActivity
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.LoggingUtils.sendMailWithAttachment import com.nextcloud.talk.utils.LoggingUtils.sendMailWithAttachment
import com.nextcloud.talk.utils.NotificationUtils 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.SecurityUtils
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew 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.preferences.AppPreferencesImpl
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
import io.reactivex.Observer import io.reactivex.Observer
@ -124,6 +133,9 @@ class SettingsActivity : BaseActivity() {
@Inject @Inject
lateinit var currentUserProvider: CurrentUserProviderNew lateinit var currentUserProvider: CurrentUserProviderNew
@Inject
lateinit var platformPermissionUtil: PlatformPermissionUtil
private var currentUser: User? = null private var currentUser: User? = null
private var credentials: String? = null private var credentials: String? = null
private lateinit var proxyTypeFlow: Flow<String> private lateinit var proxyTypeFlow: Flow<String>
@ -163,12 +175,11 @@ class SettingsActivity : BaseActivity() {
resources!!.getString(R.string.nc_app_product_name) resources!!.getString(R.string.nc_app_product_name)
) )
setupDiagnose()
setupPrivacyUrl() setupPrivacyUrl()
setupSourceCodeUrl() setupSourceCodeUrl()
binding.settingsVersionSummary.text = String.format("v" + BuildConfig.VERSION_NAME) binding.settingsVersionSummary.text = String.format("v" + BuildConfig.VERSION_NAME)
setupSoundSettings()
setupPhoneBookIntegration() setupPhoneBookIntegration()
setupClientCertView() setupClientCertView()
@ -193,18 +204,7 @@ class SettingsActivity : BaseActivity() {
setupCheckables() setupCheckables()
setupScreenLockSetting() setupScreenLockSetting()
setupNotificationSettings()
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)
setupProxyTypeSettings() setupProxyTypeSettings()
setupProxyCredentialSettings() setupProxyCredentialSettings()
registerChangeListeners() 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
binding.settingsCallSound.setOnClickListener { binding.settingsCallSound.setOnClickListener {
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
@ -295,7 +400,53 @@ class SettingsActivity : BaseActivity() {
startActivity(intent) startActivity(intent)
} }
} else { } 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() { private fun setupPrivacyUrl() {
if (!TextUtils.isEmpty(resources!!.getString(R.string.nc_privacy_url))) { if (!TextUtils.isEmpty(resources!!.getString(R.string.nc_privacy_url))) {
binding.settingsPrivacy.setOnClickListener { binding.settingsPrivacy.setOnClickListener {
@ -490,6 +648,7 @@ class SettingsActivity : BaseActivity() {
).show() ).show()
restartApp() restartApp()
} }
WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> {
Toast.makeText( Toast.makeText(
context, context,
@ -915,10 +1074,10 @@ class SettingsActivity : BaseActivity() {
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == ContactAddressBookWorker.REQUEST_PERMISSION &&
grantResults.isNotEmpty() && when (requestCode) {
grantResults[0] == PackageManager.PERMISSION_GRANTED ContactAddressBookWorker.REQUEST_PERMISSION -> {
) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
WorkManager WorkManager
.getInstance(this) .getInstance(this)
.enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java).build()) .enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java).build())
@ -934,6 +1093,23 @@ class SettingsActivity : BaseActivity() {
} }
} }
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."
)
}
}
}
}
private fun observeScreenLock() { private fun observeScreenLock() {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
var state = appPreferences.isScreenLocked var state = appPreferences.isScreenLocked
@ -1140,6 +1316,7 @@ class SettingsActivity : BaseActivity() {
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
).show() ).show()
} }
else -> { else -> {
textInputLayout.helperText = context.resources.getString( textInputLayout.helperText = context.resources.getString(
R.string.nc_settings_phone_book_integration_phone_number_dialog_invalid R.string.nc_settings_phone_book_integration_phone_number_dialog_invalid

View file

@ -27,6 +27,7 @@ import com.nextcloud.talk.data.user.UsersRepository
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.ExternalSignalingServer import com.nextcloud.talk.models.ExternalSignalingServer
import com.nextcloud.talk.models.json.capabilities.Capabilities 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.models.json.push.PushConfigurationState
import io.reactivex.Maybe import io.reactivex.Maybe
import io.reactivex.Observable import io.reactivex.Observable
@ -194,6 +195,10 @@ class UserManager internal constructor(private val userRepository: UsersReposito
user.capabilities = LoganSquare user.capabilities = LoganSquare
.parse(userAttributes.capabilities, Capabilities::class.java) .parse(userAttributes.capabilities, Capabilities::class.java)
} }
if (userAttributes.serverVersion != null) {
user.serverVersion = LoganSquare
.parse(userAttributes.serverVersion, ServerVersion::class.java)
}
user.clientCertificate = userAttributes.certificateAlias user.clientCertificate = userAttributes.certificateAlias
if (userAttributes.externalSignalingServer != null) { if (userAttributes.externalSignalingServer != null) {
user.externalSignalingServer = LoganSquare user.externalSignalingServer = LoganSquare
@ -220,6 +225,9 @@ class UserManager internal constructor(private val userRepository: UsersReposito
if (!TextUtils.isEmpty(userAttributes.capabilities)) { if (!TextUtils.isEmpty(userAttributes.capabilities)) {
user.capabilities = LoganSquare.parse(userAttributes.capabilities, Capabilities::class.java) 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)) { if (!TextUtils.isEmpty(userAttributes.certificateAlias)) {
user.clientCertificate = userAttributes.certificateAlias user.clientCertificate = userAttributes.certificateAlias
} }
@ -248,6 +256,7 @@ class UserManager internal constructor(private val userRepository: UsersReposito
val displayName: String?, val displayName: String?,
val pushConfigurationState: String?, val pushConfigurationState: String?,
val capabilities: String?, val capabilities: String?,
val serverVersion: String?,
val certificateAlias: String?, val certificateAlias: String?,
val externalSignalingServer: String? val externalSignalingServer: String?
) )

View file

@ -34,6 +34,7 @@ import android.os.Build
import android.service.notification.StatusBarNotification import android.service.notification.StatusBarNotification
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import androidx.core.app.NotificationManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import coil.executeBlocking import coil.executeBlocking
import coil.imageLoader import coil.imageLoader
@ -275,6 +276,30 @@ object NotificationUtils {
return isVisible 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( private fun getRingtoneUri(
context: Context, context: Context,
ringtonePreferencesString: String?, ringtonePreferencesString: String?,

View file

@ -31,6 +31,7 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication 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.data.user.model.User
import com.nextcloud.talk.events.EventStatus import com.nextcloud.talk.events.EventStatus
import com.nextcloud.talk.models.SignatureVerification import com.nextcloud.talk.models.SignatureVerification
@ -73,6 +74,9 @@ class PushUtils {
@Inject @Inject
lateinit var appPreferences: AppPreferences lateinit var appPreferences: AppPreferences
@Inject
lateinit var arbitraryStorageManager: ArbitraryStorageManager
@JvmField @JvmField
@Inject @Inject
var eventBus: EventBus? = null var eventBus: EventBus? = null
@ -243,6 +247,13 @@ class PushUtils {
} }
override fun onNext(pushRegistrationOverall: PushRegistrationOverall) { 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.") Log.d(TAG, "pushTokenHash successfully registered at nextcloud server.")
val proxyMap: MutableMap<String, String?> = HashMap() val proxyMap: MutableMap<String, String?> = HashMap()
proxyMap["pushToken"] = token proxyMap["pushToken"] = token
@ -273,6 +284,13 @@ class PushUtils {
override fun onNext(t: Unit) { override fun onNext(t: Unit) {
try { try {
arbitraryStorageManager.storeStorageSetting(
getIdForUser(user),
LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY,
System.currentTimeMillis().toString(),
""
)
Log.d(TAG, "pushToken successfully registered at pushproxy.") Log.d(TAG, "pushToken successfully registered at pushproxy.")
updatePushStateForUser(proxyMap, user) updatePushStateForUser(proxyMap, user)
} catch (e: IOException) { } catch (e: IOException) {
@ -396,5 +414,7 @@ class PushUtils {
companion object { companion object {
private const val TAG = "PushUtils" 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"
} }
} }

View file

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

View file

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

View file

@ -69,6 +69,14 @@ public interface AppPreferences {
void setPushToken(String pushToken); void setPushToken(String pushToken);
Long getPushTokenLatestGeneration();
void setPushTokenLatestGeneration(Long date);
Long getPushTokenLatestFetch();
void setPushTokenLatestFetch(Long date);
void removePushToken(); void removePushToken();
String getTemporaryClientCertAlias(); String getTemporaryClientCertAlias();

View file

@ -159,6 +159,28 @@ class AppPreferencesImpl(val context: Context) : AppPreferences {
pushToken = "" pushToken = ""
} }
override fun getPushTokenLatestGeneration(): Long {
return runBlocking { async { readLong(PUSH_TOKEN_LATEST_GENERATION).first() } }.getCompleted()
}
override fun setPushTokenLatestGeneration(date: Long) =
runBlocking<Unit> {
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<Unit> {
async {
writeLong(PUSH_TOKEN_LATEST_FETCH, date)
}
}
override fun getTemporaryClientCertAlias(): String { override fun getTemporaryClientCertAlias(): String {
return runBlocking { async { readString(TEMP_CLIENT_CERT_ALIAS).first() } }.getCompleted() 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_USERNAME = "proxy_username"
const val PROXY_PASSWORD = "proxy_password" const val PROXY_PASSWORD = "proxy_password"
const val PUSH_TOKEN = "push_token" 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 TEMP_CLIENT_CERT_ALIAS = "tempClientCertAlias"
const val PUSH_TO_TALK_INTRO_SHOWN = "pushToTalk_intro_shown" const val PUSH_TO_TALK_INTRO_SHOWN = "pushToTalk_intro_shown"
const val CALL_RINGTONE = "call_ringtone" const val CALL_RINGTONE = "call_ringtone"

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Marcel Hibbe
~ Copyright (C) 2024 Marcel Hibbe <dev@mhibbe.de>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/parent_container"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/settings_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/settings_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/appbar"
android:theme="?attr/actionBarPopupTheme"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationIconTint="@color/fontAppbar"
app:popupTheme="@style/appActionBarPopupMenu"
app:titleTextColor="@color/fontAppbar"
tools:title="@string/nc_app_product_name" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/diagnose_content_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/standard_padding">
</LinearLayout>
</ScrollView>
</LinearLayout>

View file

@ -7,7 +7,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/settings_appbar" android:id="@+id/settings_appbar"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -195,6 +194,70 @@
android:textSize="@dimen/headline_text_size" android:textSize="@dimen/headline_text_size"
android:textStyle="bold"/> android:textStyle="bold"/>
<LinearLayout
android:id="@+id/settings_gplay_only_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/settings_notifications_permission_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/standard_padding"
android:background="?android:attr/selectableItemBackground">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/headline_text_size"
android:text="@string/nc_diagnose_notification_permission" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/nc_diagnose_notification_permission_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="@+id/settings_battery_optimization_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/standard_padding"
android:background="?android:attr/selectableItemBackground">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/headline_text_size"
android:text="@string/nc_diagnose_battery_optimization_title" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/battery_optimization_ignored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/nc_diagnose_battery_optimization_ignored"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/settings_gplay_not_available"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/standard_padding"
android:visibility="gone"
android:orientation="vertical"
tools:visibility="visible">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nc_diagnose_gplay_available_no"/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/settings_call_sound" android:id="@+id/settings_call_sound"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -242,6 +305,7 @@
android:textSize="@dimen/supporting_text_text_size"/> android:textSize="@dimen/supporting_text_text_size"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@ -515,6 +579,28 @@
android:textSize="@dimen/headline_text_size" android:textSize="@dimen/headline_text_size"
android:textStyle="bold"/> android:textStyle="bold"/>
<LinearLayout
android:id="@+id/diagnose_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/standard_padding"
android:background="?android:attr/selectableItemBackground">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/diagnose_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/headline_text_size"
android:text="@string/nc_settings_diagnose_title"/>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/diagnose_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nc_settings_diagnose_subtitle"/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/settings_client_cert" android:id="@+id/settings_client_cert"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Marcel Hibbe
~ Copyright (C) 2024 Marcel Hibbe <dev@mhibbe.de>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/copy"
android:title="@string/nc_common_copy" />
<item
android:id="@+id/share"
android:title="@string/share" />
<item
android:id="@+id/send_mail"
android:title="@string/send_email" />
<item
android:id="@+id/create_issue"
android:title="@string/create_issue" />
</menu>

View file

@ -5,7 +5,7 @@
~ @author Mario Danic ~ @author Mario Danic
~ @author Marcel Hibbe ~ @author Marcel Hibbe
~ Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com> ~ Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de> ~ Copyright (C) 2022-2023 Marcel Hibbe <dev@mhibbe.de>
~ ~
~ This program is free software: you can redistribute it and/or modify ~ 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 ~ it under the terms of the GNU General Public License as published by
@ -43,6 +43,7 @@
<string name="nc_privacy_url" translatable="false">https://nextcloud.com/privacy/</string> <string name="nc_privacy_url" translatable="false">https://nextcloud.com/privacy/</string>
<string name="nc_gpl3_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.en.html</string> <string name="nc_gpl3_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.en.html</string>
<string name="nc_source_code_url" translatable="false">https://github.com/nextcloud/talk-android</string> <string name="nc_source_code_url" translatable="false">https://github.com/nextcloud/talk-android</string>
<string name="nc_talk_android_issues_url" translatable="false">https://github.com/nextcloud/talk-android/issues</string>
<string name="nc_providers_url" translatable="false">https://nextcloud.com/providers</string> <string name="nc_providers_url" translatable="false">https://nextcloud.com/providers</string>
<!-- Package name from which to import accounts - if empty, won't ever be shown --> <!-- Package name from which to import accounts - if empty, won't ever be shown -->
@ -61,6 +62,9 @@
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s</string> <string name="google_crash_reporting_api_key" translatable="false">AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s</string>
<string name="google_storage_bucket" translatable="false">nextcloud-a7dea.appspot.com</string> <string name="google_storage_bucket" translatable="false">nextcloud-a7dea.appspot.com</string>
<string name="notification_checklist_url" translatable="false">https://github.com/nextcloud/talk-android/blob/master/docs/notifications.md</string>
<string name="dontkillmyapp_url" translatable="false">https://dontkillmyapp.com/</string>
<!-- Map and Geocoding --> <!-- Map and Geocoding -->
<string name="osm_tile_server_url" translatable="false">https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png</string> <string name="osm_tile_server_url" translatable="false">https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png</string>
<string name="osm_tile_server_attributation" translatable="false">OpenStreetMap contributors</string> <string name="osm_tile_server_attributation" translatable="false">OpenStreetMap contributors</string>

View file

@ -6,7 +6,7 @@
~ @author Marcel Hibbe ~ @author Marcel Hibbe
~ @author Tim Krüger ~ @author Tim Krüger
~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me> ~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
~ Copyright (C) 2022-2023 Marcel Hibbe <dev@mhibbe.de> ~ Copyright (C) 2022-2024 Marcel Hibbe <dev@mhibbe.de>
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de> ~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com> ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
~ ~
@ -50,6 +50,10 @@ How to translate with transifex:
<string name="nc_common_dismiss">Dismiss</string> <string name="nc_common_dismiss">Dismiss</string>
<string name="nc_common_error_sorry">Sorry, something went wrong!</string> <string name="nc_common_error_sorry">Sorry, something went wrong!</string>
<string name="nc_common_create">Create</string> <string name="nc_common_create">Create</string>
<string name="nc_common_unknown">Unknown</string>
<string name="nc_common_disabled">Disabled</string>
<string name="nc_common_copy">Copy</string>
<string name="nc_common_copy_success">Copied to clipboard</string>
<!-- Bottom Navigation --> <!-- Bottom Navigation -->
<string name="nc_settings">Settings</string> <string name="nc_settings">Settings</string>
@ -180,6 +184,63 @@ How to translate with transifex:
<string name="nc_profile_personal_info_title">Personal Info</string> <string name="nc_profile_personal_info_title">Personal Info</string>
<string name="nc_settings_diagnose_title">Diagnose</string>
<string name="nc_settings_diagnose_subtitle">Open Diagnose screen to check settings or create bug report</string>
<string name="nc_settings_notifications_granted">Notifications are granted</string>
<string name="nc_settings_notifications_declined">Notifications are declined</string>
<string name="nc_settings_notifications_declined_hint">Notifications are declined. Please allow notifications in android settings</string>
<!-- Diagnose -->
<string name="nc_notifications_troubleshooting_dialog_title">Notification troubleshooting</string>
<string name="nc_notifications_troubleshooting_dialog_text">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.</string>
<string name="nc_diagnose_dialog_open_checklist">Open troubleshooting checklist</string>
<string name="nc_diagnose_dialog_open_dontkillmyapp_website">Open dontkillmyapp.com</string>
<string name="nc_diagnose_dialog_open_diagnose">Open diagnose screen</string>
<string name="nc_ignore_battery_optimization_dialog_title">Ignore battery optimization</string>
<string name="nc_ignore_battery_optimization_dialog_text">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</string>
<string name="nc_diagnose_meta_category_title">Meta information</string>
<string name="nc_diagnose_meta_system_report_date">Generation of system report</string>
<string name="nc_diagnose_phone_category_title">Phone</string>
<string name="nc_diagnose_device_name_title">Device</string>
<string name="nc_diagnose_android_version_title">Android version</string>
<string name="nc_diagnose_app_category_title">App</string>
<string name="nc_diagnose_app_name_title">App name</string>
<string name="nc_diagnose_app_version_title">App version</string>
<string name="nc_diagnose_app_users_amount">Registered users</string>
<string name="nc_diagnose_gplay_available_title">Google play services</string>
<string name="nc_diagnose_build_flavor">Build flavor</string>
<string name="nc_diagnose_gplay_available_yes">Google play services are available</string>
<string name="nc_diagnose_gplay_available_no">Google play services are not available. Notifications are not supported</string>
<string name="nc_diagnose_battery_optimization_title">Battery settings</string>
<string name="nc_diagnose_battery_optimization_not_ignored">Battery optimization is not ignored. This should be changed!</string>
<string name="nc_diagnose_battery_optimization_ignored">Battery optimization is ignored, all fine</string>
<string name="nc_diagnose_notification_permission">Notification permissions</string>
<string name="nc_diagnose_notification_calls_channel_permission">Calls notification channel enabled?</string>
<string name="nc_diagnose_notification_messages_channel_permission">Messages notification channel enabled?</string>
<string name="nc_diagnose_firebase_push_token_title">Firebase push token</string>
<string name="nc_diagnose_firebase_push_token_latest_generated">Latest firebase push token generation</string>
<string name="nc_diagnose_firebase_push_token_latest_fetch">Latest firebase push token fetch</string>
<string name="nc_diagnose_firebase_push_token_missing">No firebase push token set. Please create a bug report.</string>
<string name="nc_diagnose_account_category_title">Current account</string>
<string name="nc_diagnose_account_server">Server</string>
<string name="nc_diagnose_account_user_name">User</string>
<string name="nc_diagnose_account_server_notification_app">Server notification app installed?</string>
<string name="nc_diagnose_account_user_status_enabled">User status enabled?</string>
<string name="nc_diagnose_latest_push_registration_at_server">Latest push registration at server</string>
<string name="nc_diagnose_latest_push_registration_at_server_fail">Not yet registered at server</string>
<string name="nc_diagnose_latest_push_registration_at_push_proxy">Latest push registration at push proxy</string>
<string name="nc_diagnose_latest_push_registration_at_push_proxy_fail">Not yet registered at push proxy</string>
<string name="nc_diagnose_server_version">Server version</string>
<string name="nc_diagnose_server_talk_version">Server Talk version</string>
<string name="nc_diagnose_signaling_mode_title">Signaling Mode</string>
<string name="nc_diagnose_signaling_mode_intern">Internal</string>
<string name="nc_diagnose_signaling_mode_extern">External</string>
<string name="send_email">Send email</string>
<string name="create_issue">Create issue</string>
<!-- Conversation menu --> <!-- Conversation menu -->
<string name="nc_leave">Leave conversation</string> <string name="nc_leave">Leave conversation</string>
<string name="nc_clear_history">Delete all messages</string> <string name="nc_clear_history">Delete all messages</string>