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