Merge pull request #3558 from nextcloud/feature/noid/addNotificationHintsToSettings

Add diagnose screen and add permission checks in settings
This commit is contained in:
Marcel Hibbe 2024-01-12 10:03:45 +01:00 committed by GitHub
commit 52d63fbdb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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",
pushConfigurationState = null,
capabilities = null,
serverVersion = null,
certificateAlias = 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")
appPreferences.pushToken = pushToken
appPreferences.pushTokenLatestFetch = System.currentTimeMillis()
val data: Data =
Data.Builder().putString(PushRegistrationWorker.ORIGIN, "GetFirebasePushTokenWorker").build()

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

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) {
user.setCapabilities(capabilitiesOverall.getOcs().getData().getCapabilities());
user.setServerVersion(capabilitiesOverall.getOcs().getData().getServerVersion());
try {
int rowsCount = userManager.updateOrCreateUser(user).blockingGet();

View file

@ -3,6 +3,8 @@
*
* @author Mario Danic
* @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) 2017-2018 Mario Danic <mario@lovelyhq.com>
*

View file

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

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

View file

@ -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 <t@timkrueger.me>
* Copyright (C) 2021-2022 Andy Scherzinger <info@andy-scherzinger.de>
* 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>
*
* 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<String>
@ -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<String>, 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

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.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?
)

View file

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

View file

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

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);
Long getPushTokenLatestGeneration();
void setPushTokenLatestGeneration(Long date);
Long getPushTokenLatestFetch();
void setPushTokenLatestFetch(Long date);
void removePushToken();
String getTemporaryClientCertAlias();

View file

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

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_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/settings_appbar"
android:layout_width="match_parent"
@ -195,6 +194,70 @@
android:textSize="@dimen/headline_text_size"
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
android:id="@+id/settings_call_sound"
android:layout_width="match_parent"
@ -242,6 +305,7 @@
android:textSize="@dimen/supporting_text_text_size"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
@ -515,6 +579,28 @@
android:textSize="@dimen/headline_text_size"
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
android:id="@+id/settings_client_cert"
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 Marcel Hibbe
~ 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
~ 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_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_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>
<!-- 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_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 -->
<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>

View file

@ -6,7 +6,7 @@
~ @author Marcel Hibbe
~ @author Tim Krüger
~ 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) 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_error_sorry">Sorry, something went wrong!</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 -->
<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_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 -->
<string name="nc_leave">Leave conversation</string>
<string name="nc_clear_history">Delete all messages</string>