diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2831a5..b41bc1f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# Build 219 (1.2) +2023-11-02 + +- New troubleshooting activity +- Improved group cloning: it now preserves group admins + +# ~~Build 218 (1.1.1)~~ +2023-10-28 + +- Internal release + # Build 217 (1.1) 2023-10-24 diff --git a/obv_messenger/app/build.gradle b/obv_messenger/app/build.gradle index e430c2d1..6b241e70 100644 --- a/obv_messenger/app/build.gradle +++ b/obv_messenger/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId "io.olvid.messenger" minSdkVersion 21 targetSdk 34 - versionCode 217 - versionName "1.1" + versionCode 219 + versionName "1.2" vectorDrawables.useSupportLibrary true multiDexEnabled true resourceConfigurations += ['en', 'fr'] @@ -139,7 +139,8 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.2' implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' - implementation 'com.google.accompanist:accompanist-themeadapter-appcompat:0.31.1-alpha' + implementation 'com.google.accompanist:accompanist-themeadapter-appcompat:0.33.2-alpha' + implementation 'com.google.accompanist:accompanist-permissions:0.33.2-alpha' implementation 'io.coil-kt:coil-compose:2.3.0' // starting with zxing 3.4.0, API level 24 is required... @@ -172,6 +173,7 @@ dependencies { implementation 'androidx.sharetarget:sharetarget:1.2.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.work:work-runtime:2.8.1' + implementation 'androidx.datastore:datastore-preferences:1.0.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' implementation 'org.jsoup:jsoup:1.16.1' diff --git a/obv_messenger/app/src/main/AndroidManifest.xml b/obv_messenger/app/src/main/AndroidManifest.xml index 841a58fb..d1482e63 100644 --- a/obv_messenger/app/src/main/AndroidManifest.xml +++ b/obv_messenger/app/src/main/AndroidManifest.xml @@ -283,6 +283,9 @@ android:name=".onboarding.flow.OnboardingFlowActivity" android:launchMode="singleTop" android:theme="@style/AppTheme.NoActionBar.LightBlueStatusBar" /> + preselectedGroupMembers) { + public static void openGroupCreationActivityForCloning(Context activityContext, String absolutePhotoUrl, String serializedGroupDetails, String serializedGroupType, List preselectedGroupMembers, List preselectedGroupAdminMembers) { Intent intent = new Intent(getContext(), GroupCreationActivity.class); if (absolutePhotoUrl != null) { intent.putExtra(GroupCreationActivity.ABSOLUTE_PHOTO_URL_INTENT_EXTRA, absolutePhotoUrl); @@ -345,6 +346,13 @@ public static void openGroupCreationActivityForCloning(Context activityContext, } intent.putParcelableArrayListExtra(GroupCreationActivity.PRESELECTED_GROUP_MEMBERS_INTENT_EXTRA, parcelables); } + if (preselectedGroupAdminMembers != null) { + ArrayList parcelables = new ArrayList<>(preselectedGroupAdminMembers.size()); + for (Contact contact : preselectedGroupAdminMembers) { + parcelables.add(new BytesKey(contact.bytesContactIdentity)); + } + intent.putParcelableArrayListExtra(GroupCreationActivity.PRESELECTED_GROUP_ADMIN_MEMBERS_INTENT_EXTRA, parcelables); + } activityContext.startActivity(intent); } @@ -1161,7 +1169,7 @@ public void run() { private static EngineNotificationListener backupKeyListener = null; - private void configureMdmWebDavAutomaticBackups() throws Exception { + static void configureMdmWebDavAutomaticBackups() throws Exception { BackupCloudProviderService.CloudProviderConfiguration mdmAutoBackupConfiguration = MDMConfigurationSingleton.getAutoBackupConfiguration(); if (mdmAutoBackupConfiguration != null) { BackupCloudProviderService.CloudProviderConfiguration currentAutoBackupConfiguration = SettingsActivity.getAutomaticBackupConfiguration(); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/AppSingleton.java b/obv_messenger/app/src/main/java/io/olvid/messenger/AppSingleton.java index b59f1846..81e00c51 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/AppSingleton.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/AppSingleton.java @@ -83,6 +83,7 @@ import io.olvid.messenger.notifications.AndroidNotificationManager; import io.olvid.messenger.openid.KeycloakManager; import io.olvid.messenger.services.BackupCloudProviderService; +import io.olvid.messenger.services.MDMConfigurationSingleton; import io.olvid.messenger.services.PeriodicTasksScheduler; import io.olvid.messenger.settings.SettingsActivity; @@ -135,14 +136,6 @@ private AppSingleton() { SettingsActivity.setContactDisplayNameFormat(JsonIdentityDetails.FORMAT_STRING_FIRST_LAST_POSITION_COMPANY); } - if (lastBuildExecuted != 0 && lastBuildExecuted < 124) { - // clear missing google service dialog hide preference - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(App.getContext()); - SharedPreferences.Editor editor = prefs.edit(); - editor.remove(SettingsActivity.USER_DIALOG_HIDE_GOOGLE_APIS); - editor.apply(); - } - if (lastBuildExecuted != 0 && lastBuildExecuted < 136) { // clear deprecated scaled_turn preference final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(App.getContext()); @@ -165,6 +158,19 @@ private AppSingleton() { App.runThread(new ContactDisplayNameFormatChangedTask()); } + if (lastBuildExecuted != 0 && lastBuildExecuted < 219) { + // clear removed dialog hide preference + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(App.getContext()); + SharedPreferences.Editor editor = prefs.edit(); + editor.remove("user_dialog_hide_battery_optimization"); + editor.remove("user_dialog_hide_background_restricted"); + editor.remove("user_dialog_hide_alarm_scheduling"); + editor.remove("user_dialog_hide_allow_notifications"); + editor.remove("user_dialog_hide_full_screen_notification"); + editor.remove("pref_key_last_backup_reminder_timestamp"); + editor.apply(); + } + // TODO: enable this once location is no longer in beta // if (lastBuildExecuted != 0 && lastBuildExecuted < 171) { // // if the user has customized attach icon order, add the send location icon so they see it @@ -649,6 +655,14 @@ public void generateIdentity(@NonNull final String server, } } + // in case we have an MDM configuration, reload it and reconfigure backups + MDMConfigurationSingleton.reloadMDMConfiguration(); + try { + App.AppStartupTasks.configureMdmWebDavAutomaticBackups(); + } catch (Exception e) { + e.printStackTrace(); + } + App.runThread(new OwnedDevicesSynchronisationWithEngineTask(ownedIdentity.bytesOwnedIdentity)); selectIdentity(ownedIdentity.bytesOwnedIdentity, (OwnedIdentity ignored) -> { diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/GroupCloningTasks.java b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/GroupCloningTasks.java index 5f561844..5782a624 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/GroupCloningTasks.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/GroupCloningTasks.java @@ -123,6 +123,7 @@ public static ClonabilityOutput getClonability(@NonNull Group group) { null, absolutePhotoUrl, clonableGroupContacts, + new ArrayList<>(), nonGroupV2CapableContacts, nonContactOrNonChanelSerializedDetails ); @@ -144,6 +145,7 @@ public static ClonabilityOutput getClonability(@NonNull Group2 group2) { } List clonableGroupContacts = new ArrayList<>(); + List clonableGroupAdminContacts = new ArrayList<>(); List nonContactOrNonChanelSerializedDetails = new ArrayList<>(); @@ -155,7 +157,11 @@ public static ClonabilityOutput getClonability(@NonNull Group2 group2) { if (group2Member.contact == null || !group2Member.contact.capabilityGroupsV2 || group2Member.contact.establishedChannelCount == 0) { nonContactOrNonChanelSerializedDetails.add(group2Member.identityDetails); } else { - clonableGroupContacts.add(group2Member.contact); + if (group2Member.permissionAdmin) { + clonableGroupAdminContacts.add(group2Member.contact); + } else { + clonableGroupContacts.add(group2Member.contact); + } } } @@ -165,6 +171,7 @@ public static ClonabilityOutput getClonability(@NonNull Group2 group2) { serializedGroupType, absolutePhotoUrl, clonableGroupContacts, + clonableGroupAdminContacts, new ArrayList<>(), nonContactOrNonChanelSerializedDetails ); @@ -176,17 +183,19 @@ public static class ClonabilityOutput { public final String serializedGroupDetails; // contains the actual group name (or custom name), never the members names public final String serializedGroupType; public final String absolutePhotoUrl; - public final List clonableGroupContacts; // list of contacts in the group (or pending) with whom I have a channel and that have group v2 capability + public final List clonableGroupContacts; // list of non admin contacts in the group (or pending) with whom I have a channel and that have group v2 capability + public final List clonableGroupAdminContacts; // list of admins in the group (or pending) with whom I have a channel and that have group v2 capability public final List nonGroupV2CapableContacts; // contacts without group v2 capability public final List nonContactOrNonChanelSerializedDetails; // serializedIdentityDetails of group members without a channel or with whom I am not in contact - public ClonabilityOutput(String groupDisplayName, String serializedGroupDetails, String serializedGroupType, String absolutePhotoUrl, List clonableGroupContacts, List nonGroupV2CapableContacts, List nonContactOrNonChanelSerializedDetails) { + public ClonabilityOutput(String groupDisplayName, String serializedGroupDetails, String serializedGroupType, String absolutePhotoUrl, List clonableGroupContacts, List clonableGroupAdminContacts, List nonGroupV2CapableContacts, List nonContactOrNonChanelSerializedDetails) { this.groupDisplayName = groupDisplayName; this.serializedGroupDetails = serializedGroupDetails; this.serializedGroupType = serializedGroupType; this.absolutePhotoUrl = absolutePhotoUrl; this.clonableGroupContacts = clonableGroupContacts; + this.clonableGroupAdminContacts = clonableGroupAdminContacts; this.nonGroupV2CapableContacts = nonGroupV2CapableContacts; this.nonContactOrNonChanelSerializedDetails = nonContactOrNonChanelSerializedDetails; } @@ -228,11 +237,11 @@ public static void initiateGroupCloningOrWarnUser(FragmentActivity activity, Clo .setMessage(activity.getString(R.string.dialog_message_clone_group_warning_missing_members, clonabilityOutput.groupDisplayName, sb.toString())) - .setPositiveButton(R.string.button_label_proceed, ((DialogInterface dialog, int which) -> App.openGroupCreationActivityForCloning(activity, clonabilityOutput.absolutePhotoUrl, clonabilityOutput.serializedGroupDetails, clonabilityOutput.serializedGroupType, clonabilityOutput.clonableGroupContacts))) + .setPositiveButton(R.string.button_label_proceed, ((DialogInterface dialog, int which) -> App.openGroupCreationActivityForCloning(activity, clonabilityOutput.absolutePhotoUrl, clonabilityOutput.serializedGroupDetails, clonabilityOutput.serializedGroupType, clonabilityOutput.clonableGroupContacts, clonabilityOutput.clonableGroupAdminContacts))) .setNegativeButton(R.string.button_label_cancel, null); confirmationBuilder.create().show(); } else { - App.openGroupCreationActivityForCloning(activity, clonabilityOutput.absolutePhotoUrl, clonabilityOutput.serializedGroupDetails, clonabilityOutput.serializedGroupType, clonabilityOutput.clonableGroupContacts); + App.openGroupCreationActivityForCloning(activity, clonabilityOutput.absolutePhotoUrl, clonabilityOutput.serializedGroupDetails, clonabilityOutput.serializedGroupType, clonabilityOutput.clonableGroupContacts, clonabilityOutput.clonableGroupAdminContacts); } } } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/backup/RestoreAppDataFromBackupTask.java b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/backup/RestoreAppDataFromBackupTask.java index 9aa330f9..39d9708a 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/backup/RestoreAppDataFromBackupTask.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/backup/RestoreAppDataFromBackupTask.java @@ -319,7 +319,6 @@ public Boolean call() { appBackupPojo.settings.restore(); } new Handler(Looper.getMainLooper()).post(SettingsActivity::setDefaultNightMode); - SettingsActivity.setLastBackupReminderTimestamp(System.currentTimeMillis()); return true; } catch (Exception e) { diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/settings/DiscussionSettingsActivity.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/settings/DiscussionSettingsActivity.java index 85eee521..2f0f6efe 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/settings/DiscussionSettingsActivity.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/settings/DiscussionSettingsActivity.java @@ -164,9 +164,14 @@ public void onBackPressed() { AlertDialog.Builder builder = new SecureAlertDialogBuilder(this, R.style.CustomAlertDialog) .setTitle(R.string.dialog_title_shared_ephemeral_settings_modified) .setMessage(R.string.dialog_message_shared_ephemeral_settings_modified) - .setNegativeButton(R.string.button_label_discard, (dialog, which) -> discussionSettingsViewModel.discardEphemeralSettings()) - .setPositiveButton(R.string.button_label_update, (dialog, which) -> discussionSettingsViewModel.saveEphemeralSettingsAndNotifyPeers()) - .setOnDismissListener(dialog -> super.onBackPressed()); + .setNegativeButton(R.string.button_label_discard, (dialog, which) -> { + discussionSettingsViewModel.discardEphemeralSettings(); + super.onBackPressed(); + }) + .setPositiveButton(R.string.button_label_update, (dialog, which) -> { + discussionSettingsViewModel.saveEphemeralSettingsAndNotifyPeers(); + super.onBackPressed(); + }); builder.create().show(); } else { super.onBackPressed(); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/gallery/GalleryActivity.java b/obv_messenger/app/src/main/java/io/olvid/messenger/gallery/GalleryActivity.java index 45a76312..b1aec755 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/gallery/GalleryActivity.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/gallery/GalleryActivity.java @@ -360,7 +360,11 @@ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float ve && e1.getY() > 100 // no fling if starting from top of screen (like to show status bar) ) { finish(); - overridePendingTransition(R.anim.none, R.anim.dismiss_from_fling_down); + if (velocityY < 0) { + overridePendingTransition(R.anim.none, R.anim.dismiss_from_fling_up); + } else { + overridePendingTransition(R.anim.none, R.anim.dismiss_from_fling_down); + } return true; } return false; diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupCreationActivity.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupCreationActivity.kt index 7d7240e7..9b26592e 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupCreationActivity.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupCreationActivity.kt @@ -65,6 +65,7 @@ import io.olvid.messenger.databases.entity.OwnedIdentity import io.olvid.messenger.databases.entity.jsons.JsonExpiration import io.olvid.messenger.fragments.FilteredContactListFragment import io.olvid.messenger.group.GroupTypeModel.CustomGroup +import io.olvid.messenger.group.GroupTypeModel.PrivateGroup import io.olvid.messenger.group.GroupTypeModel.SimpleGroup import io.olvid.messenger.settings.SettingsActivity @@ -122,9 +123,15 @@ class GroupCreationActivity : LockableActivity(), OnClickListener { ) ) } - if (intent.hasExtra(PRESELECTED_GROUP_MEMBERS_INTENT_EXTRA)) { - val preselectedContactBytesKeys = intent.getParcelableArrayListExtra(PRESELECTED_GROUP_MEMBERS_INTENT_EXTRA) - if (preselectedContactBytesKeys != null) { + val preselectedContactAdminBytesKeys = intent.getParcelableArrayListExtra( + PRESELECTED_GROUP_ADMIN_MEMBERS_INTENT_EXTRA + ) ?: arrayListOf() + val preselectedContactNonAdminBytesKeys = + intent.getParcelableArrayListExtra(PRESELECTED_GROUP_MEMBERS_INTENT_EXTRA) + ?: arrayListOf() + val preselectedContactBytesKeys = preselectedContactAdminBytesKeys + preselectedContactNonAdminBytesKeys + if (preselectedContactBytesKeys.isNotEmpty()) { + val admins : HashSet = hashSetOf() App.runThread { val preselectedContacts: MutableList = ArrayList() for (bytesKey in preselectedContactBytesKeys) { @@ -132,10 +139,14 @@ class GroupCreationActivity : LockableActivity(), OnClickListener { .contactDao()[AppSingleton.getBytesCurrentIdentity(), bytesKey.bytes] if (contact != null) { preselectedContacts.add(contact) + if (preselectedContactAdminBytesKeys.contains(bytesKey)) { + admins.add(contact) + } } } if (preselectedContacts.isNotEmpty()) { runOnUiThread { + groupCreationViewModel.admins.value = admins groupCreationViewModel.selectedContacts = preselectedContacts contactsSelectionFragment?.setInitiallySelectedContacts( preselectedContacts @@ -143,7 +154,6 @@ class GroupCreationActivity : LockableActivity(), OnClickListener { } } } - } } } setContentView(R.layout.activity_group_creation) @@ -373,32 +383,46 @@ class GroupCreationActivity : LockableActivity(), OnClickListener { val groupDescription = groupDetailsViewModel.groupDescription?.trim() val jsonGroupDetails = JsonGroupDetails(groupName.trim(), groupDescription) - val groupType = groupV2DetailsViewModel.getGroupTypeLiveData().value + val groupType = groupV2DetailsViewModel.getGroupTypeLiveData().value ?: SimpleGroup val otherGroupMembers = HashMap>() for (contact in selectedContacts) { val permissions = groupV2DetailsViewModel.getPermissions( - groupV2DetailsViewModel.getGroupTypeLiveData().value ?: SimpleGroup, - groupCreationViewModel.admins.value?.find { it == contact } != null) + groupType, + if (groupType is PrivateGroup) + false + else + groupCreationViewModel.admins.value?.find { it == contact } != null) otherGroupMembers[ObvBytesKey(contact.bytesContactIdentity)] = permissions } try { val serializedGroupDetails = AppSingleton.getJsonObjectMapper().writeValueAsString(jsonGroupDetails) - val serializedGroupType = AppSingleton.getJsonObjectMapper().writeValueAsString((groupType ?: CustomGroup()).toJsonGroupType() ) + val serializedGroupType = AppSingleton.getJsonObjectMapper() + .writeValueAsString((groupType).toJsonGroupType()) // set tmp ephemeral settings for just created group AppSingleton.setCreatedGroupEphemeralSettings( - JsonExpiration().takeIf { groupV2DetailsViewModel.getGroupTypeLiveData().value is CustomGroup }?.apply { - readOnce = groupCreationViewModel.settingsReadOnce - existenceDuration = - groupCreationViewModel.settingsExistenceDuration - visibilityDuration = - groupCreationViewModel.settingsVisibilityDuration - } + JsonExpiration().takeIf { groupV2DetailsViewModel.getGroupTypeLiveData().value is CustomGroup } + ?.apply { + readOnce = groupCreationViewModel.settingsReadOnce + existenceDuration = + groupCreationViewModel.settingsExistenceDuration + visibilityDuration = + groupCreationViewModel.settingsVisibilityDuration + } ) val ownPermissions = Permission.DEFAULT_ADMIN_PERMISSIONS.toHashSet().apply { - if (groupType is CustomGroup && groupType.remoteDeleteSetting == GroupTypeModel.RemoteDeleteSetting.NOBODY) this.remove(Permission.REMOTE_DELETE_ANYTHING) + if (groupType is CustomGroup && groupType.remoteDeleteSetting == GroupTypeModel.RemoteDeleteSetting.NOBODY) this.remove( + Permission.REMOTE_DELETE_ANYTHING + ) } - AppSingleton.getEngine().startGroupV2CreationProtocol(serializedGroupDetails, groupAbsolutePhotoUrl, bytesOwnedIdentity, ownPermissions, otherGroupMembers, serializedGroupType) + AppSingleton.getEngine().startGroupV2CreationProtocol( + serializedGroupDetails, + groupAbsolutePhotoUrl, + bytesOwnedIdentity, + ownPermissions, + otherGroupMembers, + serializedGroupType + ) AppSingleton.getEngine().addNotificationListener( EngineNotifications.GROUP_V2_CREATED_OR_UPDATED, object : EngineNotificationListener { @@ -670,5 +694,7 @@ class GroupCreationActivity : LockableActivity(), OnClickListener { "serialized_group_type" // String with serialized JsonGroupType const val PRESELECTED_GROUP_MEMBERS_INTENT_EXTRA = "preselected_group_members" // Array of BytesKey + const val PRESELECTED_GROUP_ADMIN_MEMBERS_INTENT_EXTRA = + "preselected_group_admin_members" // Array of BytesKey } } \ No newline at end of file diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/EmptyListCard.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/EmptyListCard.kt deleted file mode 100644 index 2e2ca2b4..00000000 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/EmptyListCard.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Olvid for Android - * Copyright © 2019-2023 Olvid SAS - * - * This file is part of Olvid for Android. - * - * Olvid is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * Olvid 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Olvid. If not, see . - */ - -package io.olvid.messenger.main - -import androidx.annotation.StringRes -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.google.accompanist.themeadapter.appcompat.AppCompatTheme -import io.olvid.messenger.R -import io.olvid.messenger.R.color - -@Composable -fun EmptyListCard(@StringRes stringRes: Int) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - shape = RoundedCornerShape(4.dp), - elevation = 4.dp - ) { - Column( - modifier = Modifier - .background(colorResource(id = color.almostWhite)) - .padding(8.dp) - ) { - Text( - text = stringResource( - id = stringRes - ), - fontSize = 16.sp, - fontStyle = FontStyle.Italic, - color = colorResource( - id = color.greyTint - ) - ) - } - } -} - -@Preview -@Composable -private fun EmptyListCardPreview() { - AppCompatTheme { - EmptyListCard(stringRes = R.string.explanation_empty_discussion_list) - } -} \ No newline at end of file diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/MainActivity.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/MainActivity.kt index 46bdc43f..3c12cbc1 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/MainActivity.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/MainActivity.kt @@ -18,12 +18,14 @@ */ package io.olvid.messenger.main +import android.Manifest import android.annotation.SuppressLint import android.content.DialogInterface import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.text.InputType import android.view.GestureDetector import android.view.GestureDetector.SimpleOnGestureListener @@ -40,6 +42,21 @@ import androidx.activity.result.contract.ActivityResultContracts.RequestPermissi import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.SnackbarDuration +import androidx.compose.material.SnackbarHost +import androidx.compose.material.SnackbarHostState +import androidx.compose.material.SnackbarResult.ActionPerformed +import androidx.compose.material.SnackbarResult.Dismissed +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.unit.dp import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen @@ -49,26 +66,26 @@ import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentOnAttachListener import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.lifecycle.switchMap -import androidx.preference.PreferenceManager import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.MarginPageTransformer import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback -import com.google.android.material.snackbar.BaseTransientBottomBar -import com.google.android.material.snackbar.Snackbar +import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import io.olvid.engine.Logger import io.olvid.engine.datatypes.ObvBase64 import io.olvid.engine.engine.types.EngineNotificationListener import io.olvid.engine.engine.types.EngineNotifications -import io.olvid.engine.engine.types.ObvBackupKeyInformation import io.olvid.messenger.App import io.olvid.messenger.AppSingleton import io.olvid.messenger.BuildConfig import io.olvid.messenger.R +import io.olvid.messenger.R.color +import io.olvid.messenger.R.dimen +import io.olvid.messenger.R.id import io.olvid.messenger.activities.ContactDetailsActivity import io.olvid.messenger.activities.ObvLinkActivity -import io.olvid.messenger.owneddetails.OwnedIdentityDetailsActivity import io.olvid.messenger.activities.storage_manager.StorageManagerActivity import io.olvid.messenger.billing.BillingUtils import io.olvid.messenger.customClasses.ConfigurationPojo @@ -83,7 +100,6 @@ import io.olvid.messenger.fragments.dialog.DiscussionSearchDialogFragment import io.olvid.messenger.fragments.dialog.OtherKnownUsersDialogFragment import io.olvid.messenger.fragments.dialog.OwnedIdentitySelectionDialogFragment import io.olvid.messenger.fragments.dialog.OwnedIdentitySelectionDialogFragment.OnOwnedIdentitySelectedListener -import io.olvid.messenger.google_services.GoogleServicesUtils import io.olvid.messenger.main.calls.CallLogFragment import io.olvid.messenger.main.contacts.ContactListFragment import io.olvid.messenger.main.discussions.DiscussionListFragment @@ -92,6 +108,7 @@ import io.olvid.messenger.notifications.AndroidNotificationManager import io.olvid.messenger.onboarding.OnboardingActivity import io.olvid.messenger.onboarding.flow.OnboardingFlowActivity import io.olvid.messenger.openid.KeycloakManager +import io.olvid.messenger.owneddetails.OwnedIdentityDetailsActivity import io.olvid.messenger.plus_button.PlusButtonActivity import io.olvid.messenger.services.UnifiedForegroundService import io.olvid.messenger.settings.SettingsActivity @@ -100,6 +117,10 @@ import io.olvid.messenger.settings.SettingsActivity.PingConnectivityIndicator.DO import io.olvid.messenger.settings.SettingsActivity.PingConnectivityIndicator.FULL import io.olvid.messenger.settings.SettingsActivity.PingConnectivityIndicator.LINE import io.olvid.messenger.settings.SettingsActivity.PingConnectivityIndicator.NONE +import io.olvid.messenger.troubleshooting.TroubleshootingActivity +import io.olvid.messenger.troubleshooting.TroubleshootingDataStore +import io.olvid.messenger.troubleshooting.shouldShowTroubleshootingSnackbar +import kotlinx.coroutines.launch import java.util.* class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListener { @@ -123,20 +144,11 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen @JvmField var requestNotificationPermission = registerForActivityResult( RequestPermission() - ) { isGranted: Boolean -> - if (!isGranted) { - val prefs = PreferenceManager.getDefaultSharedPreferences(App.getContext()) - val hideDialog = - prefs.getBoolean(SettingsActivity.USER_DIALOG_HIDE_ALLOW_NOTIFICATIONS, false) - if (!hideDialog) { - val builder = Utils.getNotificationDisabledDialogBuilder(this, prefs) - builder.create().show() - } - } - } + ) {} @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { + lifecycleScope.launch { TroubleshootingDataStore(this@MainActivity).load() } App.runThread { val identityCount: Int = try { AppSingleton.getEngine().ownedIdentities.size @@ -145,14 +157,15 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen AppDatabase.getInstance().ownedIdentityDao().countAll() } if (identityCount == 0) { - val onboardingIntent = Intent(applicationContext, OnboardingFlowActivity::class.java) + val onboardingIntent = + Intent(applicationContext, OnboardingFlowActivity::class.java) startActivity(onboardingIntent) finish() } } try { installSplashScreen() - } catch (e: Exception) { + } catch (e: Exception) { setTheme(R.style.AppTheme_NoActionBar) } @@ -163,6 +176,46 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen setSupportActionBar(toolbar) val actionBar = supportActionBar actionBar?.setDisplayShowTitleEnabled(false) + val snackBarContainer = findViewById(R.id.snackbar_container) + snackBarContainer.setContent { + val snackbarState = remember { + SnackbarHostState() + } + LaunchedEffect(Unit) { + if (savedInstanceState == null && shouldShowTroubleshootingSnackbar()) { + snackbarState.showSnackbar( + message = getString(R.string.snackbar_go_to_troubleshooting_message), + actionLabel = getString(R.string.snackbar_go_to_troubleshooting_button), + duration = SnackbarDuration.Long + ).let { snackbarResult -> + when (snackbarResult) { + Dismissed -> {} + ActionPerformed -> { + startActivity( + Intent( + this@MainActivity, + TroubleshootingActivity::class.java + ) + ) + } + } + } + } + } + AppCompatTheme { + Box( + modifier = Modifier + .fillMaxSize() + ) { + SnackbarHost( + hostState = snackbarState, modifier = Modifier + .widthIn(max = 400.dp) + .align(Alignment.BottomCenter) + .padding(vertical = 80.dp) + ) + } + } + } val gestureDetector = GestureDetector(this, object : SimpleOnGestureListener() { var scrolled = false override fun onDown(e: MotionEvent): Boolean { @@ -175,7 +228,12 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen return true } - override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { if (scrolled) { return false } @@ -189,49 +247,50 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen event!! ) } - pingConnectivityDot = findViewById(R.id.ping_indicator_dot) - pingConnectivityLine = findViewById(R.id.ping_indicator_line) - pingConnectivityFull = findViewById(R.id.ping_indicator_full) - pingConnectivityFullTextView = findViewById(R.id.ping_indicator_full_text_view) - pingConnectivityFullPingTextView = findViewById(R.id.ping_indicator_full_ping_text_view) - pingRed = ContextCompat.getColor(this, R.color.red) - pingGolden = ContextCompat.getColor(this, R.color.golden) - pingGreen = ContextCompat.getColor(this, R.color.green) + pingConnectivityDot = findViewById(id.ping_indicator_dot) + pingConnectivityLine = findViewById(id.ping_indicator_line) + pingConnectivityFull = findViewById(id.ping_indicator_full) + pingConnectivityFullTextView = findViewById(id.ping_indicator_full_text_view) + pingConnectivityFullPingTextView = findViewById(id.ping_indicator_full_ping_text_view) + pingRed = ContextCompat.getColor(this, color.red) + pingGolden = ContextCompat.getColor(this, color.golden) + pingGreen = ContextCompat.getColor(this, color.green) AppSingleton.getWebsocketConnectivityStateLiveData().observe(this, pingListener) tabsPagerAdapter = TabsPagerAdapter( this, - findViewById(R.id.tab_discussions_notification_dot), - findViewById(R.id.tab_contacts_notification_dot), - findViewById(R.id.tab_groups_notification_dot), - findViewById(R.id.tab_calls_notification_dot) + findViewById(id.tab_discussions_notification_dot), + findViewById(id.tab_contacts_notification_dot), + findViewById(id.tab_groups_notification_dot), + findViewById(id.tab_calls_notification_dot) ) tabImageViews = arrayOfNulls(4) - tabImageViews[0] = findViewById(R.id.tab_discussions_button) - tabImageViews[1] = findViewById(R.id.tab_contacts_button) - tabImageViews[2] = findViewById(R.id.tab_groups_button) - tabImageViews[3] = findViewById(R.id.tab_calls_button) + tabImageViews[0] = findViewById(id.tab_discussions_button) + tabImageViews[1] = findViewById(id.tab_contacts_button) + tabImageViews[2] = findViewById(id.tab_groups_button) + tabImageViews[3] = findViewById(id.tab_calls_button) for (imageView in tabImageViews) { imageView?.setOnClickListener(this) } mainActivityPageChangeListener = MainActivityPageChangeListener(tabImageViews) viewPager.adapter = tabsPagerAdapter viewPager.registerOnPageChangeCallback(mainActivityPageChangeListener!!) - viewPager.setPageTransformer(MarginPageTransformer(resources.getDimensionPixelSize(R.dimen.main_activity_page_margin))) + viewPager.setPageTransformer(MarginPageTransformer(resources.getDimensionPixelSize(dimen.main_activity_page_margin))) viewPager.offscreenPageLimit = 3 supportFragmentManager.addFragmentOnAttachListener(this) - val addContactButton = findViewById(R.id.tab_plus_button) + val addContactButton = findViewById(id.tab_plus_button) addContactButton.setOnClickListener(this) - val focusHugger = findViewById(R.id.focus_hugger) + val focusHugger = findViewById(id.focus_hugger) focusHugger.requestFocus() // observe owned Identity (for initial view) val ownedIdentityMutedImageView = - findViewById(R.id.owned_identity_muted_marker_image_view) + findViewById(id.owned_identity_muted_marker_image_view) AppSingleton.getCurrentIdentityLiveData().observe(this) { ownedIdentity: OwnedIdentity? -> if (ownedIdentity == null) { App.runThread { if (AppDatabase.getInstance().ownedIdentityDao().countAll() == 0) { - val onboardingIntent = Intent(applicationContext, OnboardingFlowActivity::class.java) + val onboardingIntent = + Intent(applicationContext, OnboardingFlowActivity::class.java) startActivity(onboardingIntent) finish() } @@ -266,7 +325,7 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen tabsPagerAdapter!!.hideNotificationDot(DISCUSSIONS_TAB) } } - val unreadMarker = findViewById(R.id.owned_identity_unread_marker_image_view) + val unreadMarker = findViewById(id.owned_identity_unread_marker_image_view) AppSingleton.getCurrentIdentityLiveData().switchMap { ownedIdentity: OwnedIdentity? -> if (ownedIdentity == null) { return@switchMap null @@ -280,7 +339,6 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen unreadMarker.visibility = View.GONE } } - showReminderSnackBarIfNeeded() if (savedInstanceState != null && savedInstanceState.getBoolean( ALREADY_CREATED_BUNDLE_EXTRA, false @@ -291,99 +349,15 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen UnifiedForegroundService.finishAndRemoveExtraTasks(this) } handleIntent(intent) - } - private fun showReminderSnackBarIfNeeded() { - App.runThread { - var snackBar: Snackbar? = null - var dialogMessageResourceId = -1 - var dialogTitleResourceId = -1 - try { - val lastReminderTimestamp = SettingsActivity.getLastBackupReminderTimestamp() - if (AppDatabase.getInstance().contactDao().countAll() == 0L) { - // no contacts --> do nothing - return@runThread - } - val info: ObvBackupKeyInformation? = try { - AppSingleton.getEngine().backupKeyInformation - } catch (e: Exception) { - // this will be retried the next time MainActivity is started - Logger.e("Unable to retrieve backup info") - return@runThread - } - if (info == null) { - if (lastReminderTimestamp + 7 * 86400000L < System.currentTimeMillis()) { - // no backup key generated - snackBar = Snackbar.make( - root!!, - R.string.snackbar_message_setup_backup, - BaseTransientBottomBar.LENGTH_INDEFINITE - ) - dialogMessageResourceId = R.string.dialog_message_setup_backup_explanation - dialogTitleResourceId = R.string.dialog_title_setup_backup_explanation - } - return@runThread - } - if (lastReminderTimestamp + 7 * 86400000L < System.currentTimeMillis() && - !SettingsActivity.useAutomaticBackup() && info.lastBackupExport + 7 * 86400000L < System.currentTimeMillis() - ) { - // no automatic backups, and no backups since more that a week - snackBar = Snackbar.make( - root!!, - R.string.snackbar_message_remember_to_backup, - BaseTransientBottomBar.LENGTH_INDEFINITE - ) - dialogTitleResourceId = R.string.snackbar_message_remember_to_backup - dialogMessageResourceId = - if (BuildConfig.USE_GOOGLE_LIBS && GoogleServicesUtils.googleServicesAvailable( - this - ) - ) { - R.string.dialog_message_remember_to_backup_explanation - } else { - R.string.dialog_message_remember_to_backup_explanation_no_google - } - return@runThread - } - if (lastReminderTimestamp + 7 * 86400000L < System.currentTimeMillis() && info.keyGenerationTimestamp + 14 * 86400000L < System.currentTimeMillis() && info.lastSuccessfulKeyVerificationTimestamp + 30 * 86400000L < System.currentTimeMillis()) { - // all backup stuff is good, but key not verified since more than 30 days - snackBar = Snackbar.make( - root!!, - R.string.snackbar_message_verify_backup_key, - BaseTransientBottomBar.LENGTH_INDEFINITE - ) - dialogTitleResourceId = R.string.snackbar_message_verify_backup_key - dialogMessageResourceId = R.string.dialog_message_verify_backup_key_explanation - return@runThread - } - } catch (e: Exception) { - e.printStackTrace() - } finally { - if (snackBar != null) { - val finalDialogMessageResourceId = dialogMessageResourceId - val finalDialogTitleResourceId = dialogTitleResourceId - val finalSnackBar: Snackbar = snackBar - snackBar.setAction(R.string.button_label_show_me, OnClickListener { - val builder = SecureAlertDialogBuilder(this, R.style.CustomAlertDialog) - .setTitle(finalDialogTitleResourceId) - .setMessage(finalDialogMessageResourceId) - .setPositiveButton(R.string.button_label_backup_settings) { _, _ -> - val intent = Intent(this, SettingsActivity::class.java) - intent.putExtra( - SettingsActivity.SUB_SETTING_PREF_KEY_TO_OPEN_INTENT_EXTRA, - SettingsActivity.PREF_HEADER_KEY_BACKUP - ) - startActivity(intent) - } - .setNegativeButton(R.string.button_label_remind_me_later) { _: DialogInterface?, _: Int -> - SettingsActivity.setLastBackupReminderTimestamp( - System.currentTimeMillis() - ) - } - builder.create().show() - }) - Handler(Looper.getMainLooper()).post { finalSnackBar.show() } - } + // check notifications permissions + if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS) } } } @@ -499,6 +473,7 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen startActivity(forwardIntent) } } + LINK_ACTION -> { // first detect the type of link to show an appropriate dialog @@ -512,13 +487,14 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen ObvBase64.decode(configurationMatcher.group(2)), ConfigurationPojo::class.java ) - val dialogTitleResourceId: Int = if (configurationPojo.keycloak != null) { - // offer to chose a profile or create a new one for profile binding - R.string.dialog_title_chose_profile_keycloak_configuration - } else { - // offer to chose a profile or create a new one for licence activation or configuration - R.string.dialog_title_chose_profile_configuration - } + val dialogTitleResourceId: Int = + if (configurationPojo.keycloak != null) { + // offer to chose a profile or create a new one for profile binding + R.string.dialog_title_chose_profile_keycloak_configuration + } else { + // offer to chose a profile or create a new one for licence activation or configuration + R.string.dialog_title_chose_profile_configuration + } val ownedIdentitySelectionDialogFragment = OwnedIdentitySelectionDialogFragment.newInstance( this, @@ -540,8 +516,14 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen } override fun onNewProfileCreationSelected() { - val onboardingIntent = Intent(this@MainActivity, OnboardingActivity::class.java) - .putExtra(PlusButtonActivity.LINK_URI_INTENT_EXTRA, uri) + val onboardingIntent = Intent( + this@MainActivity, + OnboardingActivity::class.java + ) + .putExtra( + PlusButtonActivity.LINK_URI_INTENT_EXTRA, + uri + ) startActivity(onboardingIntent) } }) @@ -557,7 +539,8 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen e.printStackTrace() } } else if (ObvLinkActivity.INVITATION_PATTERN.matcher(uri).find() - || ObvLinkActivity.MUTUAL_SCAN_PATTERN.matcher(uri).find()) { + || ObvLinkActivity.MUTUAL_SCAN_PATTERN.matcher(uri).find() + ) { // offer to chose a profile val ownedIdentitySelectionDialogFragment = OwnedIdentitySelectionDialogFragment.newInstance( @@ -580,7 +563,10 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen } override fun onNewProfileCreationSelected() { - val onboardingIntent = Intent(this@MainActivity, OnboardingActivity::class.java) + val onboardingIntent = Intent( + this@MainActivity, + OnboardingActivity::class.java + ) .putExtra(OnboardingActivity.LINK_URI_INTENT_EXTRA, uri) startActivity(onboardingIntent) } @@ -602,14 +588,23 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen override fun onOwnedIdentitySelected(bytesOwnedIdentity: ByteArray) { AppSingleton.getInstance() .selectIdentity(bytesOwnedIdentity) { - val plusIntent = Intent(this@MainActivity, PlusButtonActivity::class.java) - .putExtra(PlusButtonActivity.LINK_URI_INTENT_EXTRA, uri) + val plusIntent = Intent( + this@MainActivity, + PlusButtonActivity::class.java + ) + .putExtra( + PlusButtonActivity.LINK_URI_INTENT_EXTRA, + uri + ) startActivity(plusIntent) } } override fun onNewProfileCreationSelected() { - val onboardingIntent = Intent(this@MainActivity, OnboardingActivity::class.java) + val onboardingIntent = Intent( + this@MainActivity, + OnboardingActivity::class.java + ) .putExtra(OnboardingActivity.PROFILE_CREATION, true) startActivity(onboardingIntent) @@ -658,15 +653,19 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen contactListFragment = ContactListFragment() contactListFragment!! } + GROUPS_TAB -> { GroupListFragment() } + CALLS_TAB -> { CallLogFragment() } + DISCUSSIONS_TAB -> { DiscussionListFragment() } + else -> { DiscussionListFragment() } @@ -704,16 +703,19 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen pingConnectivityLine!!.visibility = View.GONE pingConnectivityFull!!.visibility = View.GONE } + DOT -> { pingConnectivityDot!!.visibility = View.VISIBLE pingConnectivityLine!!.visibility = View.GONE pingConnectivityFull!!.visibility = View.GONE } + LINE -> { pingConnectivityDot!!.visibility = View.GONE pingConnectivityLine!!.visibility = View.VISIBLE pingConnectivityFull!!.visibility = View.GONE } + FULL -> { pingConnectivityDot!!.visibility = View.GONE pingConnectivityLine!!.visibility = View.GONE @@ -723,13 +725,6 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen } } - override fun onWindowFocusChanged(hasFocus: Boolean) { - super.onWindowFocusChanged(hasFocus) - if (hasFocus && AppSingleton.getBytesCurrentIdentity() != null) { - Utils.showDialogs(this) - } - } - override fun onPause() { super.onPause() Utils.stopPinging() @@ -759,7 +754,7 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen } override fun onBackPressed() { - if (!moveTaskToBack(true)) { + if (!moveTaskToBack(true)) { finishAndRemoveTask() } } @@ -788,9 +783,11 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen }) } } + DISCUSSIONS_TAB -> { menuInflater.inflate(R.menu.menu_main_discussion_list, menu) } + CALLS_TAB -> { menuInflater.inflate(R.menu.menu_call_log, menu) return true @@ -808,6 +805,8 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen return true } else if (itemId == R.id.action_storage_management) { startActivity(Intent(this, StorageManagerActivity::class.java)) + } else if (itemId == R.id.action_troubleshooting) { + startActivity(Intent(this, TroubleshootingActivity::class.java)) } else if (itemId == R.id.action_backup_settings) { val intent = Intent(this, SettingsActivity::class.java) intent.putExtra( @@ -861,15 +860,19 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen R.id.tab_plus_button -> { startActivity(Intent(this, PlusButtonActivity::class.java)) } + R.id.tab_discussions_button -> { viewPager.currentItem = DISCUSSIONS_TAB } + R.id.tab_contacts_button -> { viewPager.currentItem = CONTACTS_TAB } + R.id.tab_groups_button -> { viewPager.currentItem = GROUPS_TAB } + R.id.tab_calls_button -> { viewPager.currentItem = CALLS_TAB } @@ -906,6 +909,7 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen 1 -> pingConnectivityFullTextView!!.setText( R.string.label_ping_connectivity_connecting ) + 2 -> pingConnectivityFullTextView!!.setText(R.string.label_ping_connectivity_connected) 0 -> pingConnectivityFullTextView!!.setText(R.string.label_ping_connectivity_none) else -> pingConnectivityFullTextView!!.setText(R.string.label_ping_connectivity_none) @@ -915,9 +919,11 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen pingConnectivityFullPingTextView!!.text = getString(R.string.label_over_max_ping_delay, 5) } + 0L -> { pingConnectivityFullPingTextView!!.text = "-" } + else -> { pingConnectivityFullPingTextView!!.text = getString(R.string.label_ping_delay, lastPing) @@ -936,6 +942,7 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen lastPing = -1 runOnUiThread { refresh() } } + EngineNotifications.PING_RECEIVED -> { (userInfo[EngineNotifications.PING_RECEIVED_DELAY_KEY] as Long?)?.let { lastPing = it @@ -963,7 +970,8 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen private val imm: InputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager private val inactiveColor: Int = ContextCompat.getColor(this@MainActivity, R.color.greyTint) - private val activeColor: Int = ContextCompat.getColor(this@MainActivity, R.color.olvid_gradient_light) + private val activeColor: Int = + ContextCompat.getColor(this@MainActivity, R.color.olvid_gradient_light) private var currentPosition = -1 override fun onPageSelected(position: Int) { @@ -996,6 +1004,7 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen color or ((positionOffset * (inactiveColor and 0xff0000) + (1 - positionOffset) * (activeColor and 0xff0000)).toInt() and 0xff0000) imageViews[i]!!.setColorFilter(color) } + position + 1 -> { var color = -0x1000000 color = @@ -1006,6 +1015,7 @@ class MainActivity : LockableActivity(), OnClickListener, FragmentOnAttachListen color or ((positionOffset * (activeColor and 0xff0000) + (1 - positionOffset) * (inactiveColor and 0xff0000)).toInt() and 0xff0000) imageViews[i]!!.setColorFilter(color) } + else -> { imageViews[i]!!.setColorFilter(inactiveColor) } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/MainScreenEmptyList.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/MainScreenEmptyList.kt new file mode 100644 index 00000000..e21c4edc --- /dev/null +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/MainScreenEmptyList.kt @@ -0,0 +1,91 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.messenger.main + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.accompanist.themeadapter.appcompat.AppCompatTheme +import io.olvid.messenger.R +import io.olvid.messenger.R.color + +@Composable +fun MainScreenEmptyList(@DrawableRes icon: Int, iconPadding: Dp = 0.dp, @StringRes title: Int, @StringRes subtitle: Int?) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = Modifier.size(60.dp).padding(iconPadding).alpha(.8f), + painter = painterResource(id = icon), + contentDescription = "", + colorFilter = ColorFilter.tint(colorResource(id = R.color.greyTint)) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = title), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + color = colorResource(id = color.almostBlack) + ) + subtitle?.let { + Text( + text = stringResource(id = subtitle), + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Center, + color = colorResource(id = color.greyTint) + ) + } + Spacer(modifier = Modifier.height(64.dp)) + } +} + +@Preview +@Composable +private fun MainScreenEmptyListPreview() { + AppCompatTheme { + MainScreenEmptyList(icon = R.drawable.ic_phone_log,title = R.string.explanation_empty_discussion_list, subtitle = R.string.explanation_empty_discussion_list_sub) + } +} \ No newline at end of file diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/Utils.java b/obv_messenger/app/src/main/java/io/olvid/messenger/main/Utils.java index d327f34b..3daf52a2 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/Utils.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/Utils.java @@ -59,306 +59,20 @@ public class Utils { public static boolean dialogsLoaded = false; static boolean dialogShowing = false; - private static final Deque dialogsToShow = new ArrayDeque<>(); - private static final String USER_DIALOG_ALLOW_NOTIFICATIONS = "allow_notifications"; - private static final String USER_DIALOG_GOOGLE_APIS = "google_apis"; - private static final String USER_DIALOG_BACKGROUND_RESTRICTED = "background_restricted"; - private static final String USER_DIALOG_BATTERY_OPTIMIZATION = "battery_optimization"; - private static final String USER_DIALOG_ALARM_SCHEDULING = "alarm_scheduling"; - private static final String USER_DIALOG_FULL_SCREEN_NOTIFICATION = "full_screen_notification"; - - static void showDialogs(MainActivity activity) { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(App.getContext()); - - if (!dialogsLoaded) { - dialogsLoaded = true; - - if ((!BuildConfig.USE_FIREBASE_LIB || !GoogleServicesUtils.googleServicesAvailable(activity)) - && !SettingsActivity.usePermanentWebSocket()) { - boolean hideDialog = prefs.getBoolean(SettingsActivity.USER_DIALOG_HIDE_GOOGLE_APIS, false); - if (!hideDialog) { - dialogsToShow.offerLast(USER_DIALOG_GOOGLE_APIS); - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ActivityManager activityManager = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE); - if (activityManager != null && activityManager.isBackgroundRestricted()) { - boolean hideDialog = prefs.getBoolean(SettingsActivity.USER_DIALOG_HIDE_BACKGROUND_RESTRICTED, false); - if (!hideDialog) { - dialogsToShow.offerLast(USER_DIALOG_BACKGROUND_RESTRICTED); - } - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PowerManager pm = (PowerManager) activity.getSystemService(Context.POWER_SERVICE); - if (pm != null && !pm.isIgnoringBatteryOptimizations(activity.getPackageName())) { - boolean hideDialog = prefs.getBoolean(SettingsActivity.USER_DIALOG_HIDE_BATTERY_OPTIMIZATION, false); - if (!hideDialog) { - dialogsToShow.offerLast(USER_DIALOG_BATTERY_OPTIMIZATION); - } - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - AlarmManager alarmManager = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE); - if (alarmManager != null && !alarmManager.canScheduleExactAlarms()) { - boolean hideDialog = prefs.getBoolean(SettingsActivity.USER_DIALOG_HIDE_ALARM_SCHEDULING, false); - if (!hideDialog) { - dialogsToShow.offerLast(USER_DIALOG_ALARM_SCHEDULING); - } - } - } - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(App.getContext()); - if (!notificationManager.areNotificationsEnabled()) { - boolean hideDialog = prefs.getBoolean(SettingsActivity.USER_DIALOG_HIDE_ALLOW_NOTIFICATIONS, false); - if (!hideDialog) { - dialogsToShow.offerLast(USER_DIALOG_ALLOW_NOTIFICATIONS); - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - NotificationManager notifManager = activity.getSystemService(NotificationManager.class); - if (!notifManager.canUseFullScreenIntent()) { - boolean hideDialog = prefs.getBoolean(SettingsActivity.USER_DIALOG_HIDE_FULL_SCREEN_NOTIFICATION, false); - if (!hideDialog) { - dialogsToShow.offerLast(USER_DIALOG_FULL_SCREEN_NOTIFICATION); - } - } - } - } - - if (dialogShowing) { - return; - } - - String dialogToShow = dialogsToShow.pollFirst(); - if (dialogToShow == null) { - return; - } - - dialogShowing = true; - switch (dialogToShow) { - case USER_DIALOG_GOOGLE_APIS: { - View dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_view_message_and_checkbox, null); - TextView message = dialogView.findViewById(R.id.dialog_message); - message.setText(R.string.dialog_message_google_apis_missing); - CheckBox checkBox = dialogView.findViewById(R.id.checkbox); - checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(SettingsActivity.USER_DIALOG_HIDE_GOOGLE_APIS, isChecked); - editor.apply(); - }); - - AlertDialog.Builder builder = new SecureAlertDialogBuilder(activity, R.style.CustomAlertDialog); - builder.setTitle(R.string.dialog_title_google_apis_missing) - .setView(dialogView) - .setNeutralButton(R.string.button_label_skip, null) - .setPositiveButton(R.string.button_label_enable_permanent_websocket, (DialogInterface dialogInterface, int which) -> { - SettingsActivity.setUsePermanentWebSocket(true); - activity.startService(new Intent(activity, UnifiedForegroundService.class)); - }) - .setOnDismissListener((DialogInterface dialog) -> { - dialogShowing = false; - showDialogs(activity); - }); - builder.create().show(); - break; - } - case USER_DIALOG_BACKGROUND_RESTRICTED: { - View dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_view_message_and_checkbox, null); - TextView message = dialogView.findViewById(R.id.dialog_message); - message.setText(R.string.dialog_message_background_restricted); - CheckBox checkBox = dialogView.findViewById(R.id.checkbox); - checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(SettingsActivity.USER_DIALOG_HIDE_BACKGROUND_RESTRICTED, isChecked); - editor.apply(); - }); - - AlertDialog.Builder builder = new SecureAlertDialogBuilder(activity, R.style.CustomAlertDialog); - builder.setTitle(R.string.dialog_title_background_restricted) - .setView(dialogView) - .setNeutralButton(R.string.button_label_skip, null) - .setPositiveButton(R.string.button_label_app_settings, (DialogInterface dialog, int which) -> { - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Uri uri = Uri.fromParts("package", activity.getPackageName(), null); - intent.setData(uri); - activity.startActivity(intent); - }) - .setOnDismissListener((DialogInterface dialog) -> { - dialogShowing = false; - showDialogs(activity); - }); - - builder.create().show(); - break; - } - case USER_DIALOG_BATTERY_OPTIMIZATION: { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - View dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_view_message_and_checkbox, null); - TextView message = dialogView.findViewById(R.id.dialog_message); - message.setText(R.string.dialog_message_battery_optimization); - CheckBox checkBox = dialogView.findViewById(R.id.checkbox); - checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(SettingsActivity.USER_DIALOG_HIDE_BATTERY_OPTIMIZATION, isChecked); - editor.apply(); - }); - - AlertDialog.Builder builder = new SecureAlertDialogBuilder(activity, R.style.CustomAlertDialog); - builder.setTitle(R.string.dialog_title_battery_optimization) - .setView(dialogView) - .setNeutralButton(R.string.button_label_skip, null) - .setPositiveButton(R.string.button_label_battery_optimization_settings, (DialogInterface dialog, int which) -> { - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Uri uri = Uri.fromParts("package", activity.getPackageName(), null); - intent.setData(uri); - activity.startActivity(intent); - }) - .setOnDismissListener((DialogInterface dialog) -> { - dialogShowing = false; - showDialogs(activity); - }); - - builder.create().show(); - } - break; - } - case USER_DIALOG_ALARM_SCHEDULING: { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - View dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_view_message_and_checkbox, null); - TextView message = dialogView.findViewById(R.id.dialog_message); - message.setText(R.string.dialog_message_alarm_scheduling_forbidden); - CheckBox checkBox = dialogView.findViewById(R.id.checkbox); - checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(SettingsActivity.USER_DIALOG_HIDE_ALARM_SCHEDULING, isChecked); - editor.apply(); - }); - - AlertDialog.Builder builder = new SecureAlertDialogBuilder(activity, R.style.CustomAlertDialog); - builder.setTitle(R.string.dialog_title_alarm_scheduling_forbidden) - .setView(dialogView) - .setNeutralButton(R.string.button_label_skip, null) - .setPositiveButton(R.string.button_label_app_settings, (DialogInterface dialog, int which) -> { - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Uri uri = Uri.fromParts("package", activity.getPackageName(), null); - intent.setData(uri); - activity.startActivity(intent); - }) - .setOnDismissListener((DialogInterface dialog) -> { - dialogShowing = false; - showDialogs(activity); - }); - - builder.create().show(); - } - break; - } - case USER_DIALOG_ALLOW_NOTIFICATIONS: { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (activity.shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) { - AlertDialog.Builder builder = new SecureAlertDialogBuilder(activity, R.style.CustomAlertDialog); - builder.setTitle(R.string.dialog_title_alarm_scheduling_forbidden) - .setTitle(R.string.dialog_title_get_notified) - .setMessage(R.string.dialog_message_get_notified) - .setPositiveButton(R.string.button_label_ok, (DialogInterface dialog, int which) -> activity.requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS)) - .setNeutralButton(R.string.button_label_skip, null) - .setOnDismissListener((DialogInterface dialog) -> { - dialogShowing = false; - showDialogs(activity); - }); - builder.create().show(); - } else { - activity.requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS); - dialogShowing = false; - showDialogs(activity); - } - } else { - AlertDialog.Builder builder = getNotificationDisabledDialogBuilder(activity, prefs); - builder.setOnDismissListener((DialogInterface dialog) -> { - dialogShowing = false; - showDialogs(activity); - }); - builder.create().show(); - } - break; - } - case USER_DIALOG_FULL_SCREEN_NOTIFICATION: { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - View dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_view_message_and_checkbox, null); - TextView message = dialogView.findViewById(R.id.dialog_message); - message.setText(R.string.dialog_message_full_screen_notification); - CheckBox checkBox = dialogView.findViewById(R.id.checkbox); - checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(SettingsActivity.USER_DIALOG_HIDE_FULL_SCREEN_NOTIFICATION, isChecked); - editor.apply(); - }); - - AlertDialog.Builder builder = new SecureAlertDialogBuilder(activity, R.style.CustomAlertDialog); - builder.setTitle(R.string.dialog_title_full_screen_notification) - .setView(dialogView) - .setNeutralButton(R.string.button_label_skip, null) - .setPositiveButton(R.string.button_label_app_settings, (DialogInterface dialog, int which) -> { - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Uri uri = Uri.fromParts("package", activity.getPackageName(), null); - intent.setData(uri); - activity.startActivity(intent); - }) - .setOnDismissListener((DialogInterface dialog) -> { - dialogShowing = false; - showDialogs(activity); - }); - - builder.create().show(); - } - break; - } + public static String getUptime(Context context) { + int uptimeSeconds = (int) ((System.currentTimeMillis() - App.appStartTimestamp) / 1000); + final String uptime; + if (uptimeSeconds > 86400) { + uptime = context.getResources().getQuantityString(R.plurals.text_app_uptime_days, uptimeSeconds / 86400, uptimeSeconds / 86400, (uptimeSeconds % 86400) / 3600, (uptimeSeconds % 3600) / 60, uptimeSeconds % 60); + } else if (uptimeSeconds > 3600) { + uptime = context.getString(R.string.text_app_uptime_hours, uptimeSeconds / 3600, (uptimeSeconds % 3600) / 60, uptimeSeconds % 60); + } else { + uptime = context.getString(R.string.text_app_uptime, uptimeSeconds / 60, uptimeSeconds % 60); } + return uptime; } - static AlertDialog.Builder getNotificationDisabledDialogBuilder(FragmentActivity activity, SharedPreferences prefs) { - View dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_view_message_and_checkbox, null); - TextView message = dialogView.findViewById(R.id.dialog_message); - message.setText(R.string.dialog_message_notifications_disabled); - CheckBox checkBox = dialogView.findViewById(R.id.checkbox); - checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(SettingsActivity.USER_DIALOG_HIDE_ALLOW_NOTIFICATIONS, isChecked); - editor.apply(); - }); - - AlertDialog.Builder builder = new SecureAlertDialogBuilder(activity, R.style.CustomAlertDialog); - builder.setTitle(R.string.dialog_title_notifications_disabled) - .setView(dialogView) - .setNeutralButton(R.string.button_label_skip, null) - .setPositiveButton(R.string.button_label_open_settings, (DialogInterface dialog, int which) -> { - try { - Intent intent = new Intent(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); - intent.putExtra(Settings.EXTRA_APP_PACKAGE, activity.getPackageName()); - } else { - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", activity.getPackageName(), null); - intent.setData(uri); - } - activity.startActivity(intent); - } catch (Exception ignored) { } - }); - return builder; - } - - - // region Websocket latency ping static Timer pingTimer = null; static boolean doPing = false; diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/calls/CallLogScreen.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/calls/CallLogScreen.kt index 5f860a52..6cef2ed0 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/calls/CallLogScreen.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/calls/CallLogScreen.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.unit.dp import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import io.olvid.messenger.R import io.olvid.messenger.databases.dao.CallLogItemDao.CallLogItemAndContacts -import io.olvid.messenger.main.EmptyListCard +import io.olvid.messenger.main.MainScreenEmptyList @Composable fun CallLogScreen( @@ -99,7 +99,15 @@ fun CallLogScreen( } } } else { - EmptyListCard(stringRes = R.string.explanation_empty_call_log) + Box(modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center) { + MainScreenEmptyList( + icon = R.drawable.ic_phone_log, + iconPadding = 6.dp, + title = R.string.explanation_empty_call_log, + subtitle = R.string.explanation_empty_call_log_sub + ) + } } } FloatingActionButton(modifier = Modifier diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/contacts/ContactListItem.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/contacts/ContactListItem.kt index 8333b930..49865254 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/contacts/ContactListItem.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/contacts/ContactListItem.kt @@ -140,6 +140,7 @@ fun ContactListItem( text = body, color = colorResource(id = R.color.greyTint), fontSize = 12.sp, + lineHeight = 15.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/contacts/ContactListScreen.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/contacts/ContactListScreen.kt index 22aec4f0..a47140a8 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/contacts/ContactListScreen.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/contacts/ContactListScreen.kt @@ -58,7 +58,8 @@ import io.olvid.messenger.R.color import io.olvid.messenger.R.drawable import io.olvid.messenger.R.string import io.olvid.messenger.customClasses.StringUtils -import io.olvid.messenger.main.EmptyListCard +import io.olvid.messenger.databases.entity.Contact +import io.olvid.messenger.main.MainScreenEmptyList import io.olvid.messenger.main.RefreshingIndicator import io.olvid.messenger.main.contacts.ContactListViewModel.ContactOrKeycloakDetails import io.olvid.messenger.main.contacts.ContactListViewModel.ContactType.* @@ -136,6 +137,8 @@ fun ContactListScreen( contactListViewModel.filterPatterns ), shouldAnimateChannel = contactOrKeycloakDetails.contact?.shouldShowChannelCreationSpinner() == true && contactOrKeycloakDetails.contact.active, + publishedDetails = contactOrKeycloakDetails.contact?.newPublishedDetails == Contact.PUBLISHED_DETAILS_NEW_SEEN || contactOrKeycloakDetails.contact?.newPublishedDetails == Contact.PUBLISHED_DETAILS_NEW_UNSEEN, + publishedDetailsNotification = contactOrKeycloakDetails.contact?.newPublishedDetails == Contact.PUBLISHED_DETAILS_NEW_UNSEEN, onClick = { onClick(contactOrKeycloakDetails) }, initialViewSetup = { initialView -> when (contactOrKeycloakDetails.contactType) { @@ -213,13 +216,21 @@ fun ContactListScreen( Box( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()) + .verticalScroll(rememberScrollState()), + contentAlignment = Center ) { - EmptyListCard( - stringRes = if (contactListViewModel.isFiltering()) - string.explanation_no_contact_match_filter - else string.explanation_empty_contact_list - ) + if (contactListViewModel.isFiltering()) + MainScreenEmptyList( + icon = drawable.ic_contacts_filter, + title = string.explanation_no_contact_match_filter, + subtitle = null + ) + else + MainScreenEmptyList( + icon = drawable.tab_contacts, + title = string.explanation_empty_contact_list, + subtitle = string.explanation_empty_contact_list_sub + ) } } } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListItem.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListItem.kt index 247ad6c6..cb11779c 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListItem.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListItem.kt @@ -176,6 +176,7 @@ fun DiscussionListItem( text = title, color = colorResource(id = R.color.primary700), fontSize = 16.sp, + lineHeight = 20.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -185,6 +186,7 @@ fun DiscussionListItem( text = body, color = colorResource(id = R.color.greyTint), fontSize = 14.sp, + lineHeight = 18.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -194,6 +196,7 @@ fun DiscussionListItem( text = date, color = colorResource(id = R.color.grey), fontSize = 12.sp, + lineHeight = 15.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -236,6 +239,7 @@ fun DiscussionListItem( .padding(horizontal = 7.dp, vertical = 2.dp), text = "$unreadCount", fontSize = 14.sp, + lineHeight = 17.sp, color = colorResource(id = R.color.alwaysWhite) ) } @@ -257,8 +261,9 @@ fun DiscussionListItem( attachmentCount, attachmentCount ), - Modifier.padding(horizontal = 4.dp, vertical = 2.dp), + modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), fontSize = 10.sp, + lineHeight = 13.sp, fontWeight = FontWeight.Medium, color = colorResource(id = R.color.grey) ) diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListScreen.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListScreen.kt index a9072da3..1e98dd24 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListScreen.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListScreen.kt @@ -54,7 +54,7 @@ import io.olvid.messenger.R import io.olvid.messenger.R.string import io.olvid.messenger.customClasses.ifNull import io.olvid.messenger.databases.entity.Discussion -import io.olvid.messenger.main.EmptyListCard +import io.olvid.messenger.main.MainScreenEmptyList import io.olvid.messenger.main.RefreshingIndicator import io.olvid.messenger.main.invitations.InvitationListViewModel import io.olvid.messenger.main.invitations.getAnnotatedDate @@ -197,8 +197,15 @@ fun DiscussionListScreen( } else { Box(modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState())) { - EmptyListCard(stringRes = string.explanation_empty_discussion_list) + .verticalScroll(rememberScrollState()), + contentAlignment = Alignment.Center + ) { + MainScreenEmptyList( + icon = R.drawable.tab_discussions, + iconPadding = 4.dp, + title = R.string.explanation_empty_discussion_list, + subtitle = R.string.explanation_empty_discussion_list_sub + ) } } } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/groups/GroupListItem.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/groups/GroupListItem.kt index 39d1f9b4..b3ea1370 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/groups/GroupListItem.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/groups/GroupListItem.kt @@ -130,6 +130,7 @@ fun GroupListItem( text = body, color = colorResource(id = R.color.greyTint), fontSize = 12.sp, + lineHeight = 15.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/groups/GroupListScreen.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/groups/GroupListScreen.kt index f42473fd..d4bfc5be 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/groups/GroupListScreen.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/groups/GroupListScreen.kt @@ -24,8 +24,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.requiredSize @@ -54,11 +56,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.google.accompanist.themeadapter.appcompat.AppCompatTheme +import io.olvid.messenger.R import io.olvid.messenger.R.color import io.olvid.messenger.R.drawable import io.olvid.messenger.R.string import io.olvid.messenger.databases.dao.Group2Dao.GroupOrGroup2 -import io.olvid.messenger.main.EmptyListCard +import io.olvid.messenger.main.MainScreenEmptyList import io.olvid.messenger.main.RefreshingIndicator @OptIn(ExperimentalMaterialApi::class) @@ -119,13 +122,22 @@ fun GroupListScreen( } else { Box( modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) + .fillMaxSize(), + contentAlignment = Alignment.TopCenter ) { - Column(modifier = Modifier.fillMaxWidth()) { - NewGroupButton(onNewGroupClick) - EmptyListCard(stringRes = string.explanation_empty_group_list) + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + contentAlignment = Alignment.Center + ) { + MainScreenEmptyList( + icon = R.drawable.tab_groups, + title = R.string.explanation_empty_group_list, + subtitle = R.string.explanation_empty_group_list_sub + ) } + NewGroupButton(onNewGroupClick) } } } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/onboarding/flow/animations/Shimmer.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/onboarding/flow/animations/Shimmer.kt index 8963ecd0..dfd62f08 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/onboarding/flow/animations/Shimmer.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/onboarding/flow/animations/Shimmer.kt @@ -40,7 +40,6 @@ import androidx.compose.ui.unit.dp import io.olvid.messenger.R fun Modifier.shimmer(show:Boolean): Modifier = composed { - if (show) { var size by remember { mutableStateOf(IntSize.Zero) diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/onboarding/flow/screens/transfer/SourceSession.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/onboarding/flow/screens/transfer/SourceSession.kt index f13cc079..627fa4fd 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/onboarding/flow/screens/transfer/SourceSession.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/onboarding/flow/screens/transfer/SourceSession.kt @@ -52,7 +52,6 @@ import io.olvid.messenger.onboarding.flow.OnboardingRoutes import io.olvid.messenger.onboarding.flow.OnboardingScreen import io.olvid.messenger.onboarding.flow.OnboardingStep -@OptIn(ExperimentalComposeUiApi::class) fun NavGraphBuilder.sourceSession( onboardingFlowViewModel: OnboardingFlowViewModel, onSasValidated: () -> Unit, diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/openid/KeycloakManager.java b/obv_messenger/app/src/main/java/io/olvid/messenger/openid/KeycloakManager.java index f56d7fc7..66d2f441 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/openid/KeycloakManager.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/openid/KeycloakManager.java @@ -33,6 +33,7 @@ import org.jose4j.lang.HashUtil; import org.json.JSONException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -53,6 +54,7 @@ import io.olvid.messenger.AppSingleton; import io.olvid.messenger.BuildConfig; import io.olvid.messenger.customClasses.BytesKey; +import io.olvid.messenger.customClasses.ConfigurationKeycloakPojo; import io.olvid.messenger.notifications.AndroidNotificationManager; import io.olvid.messenger.openid.jsons.KeycloakServerRevocationsAndStuff; import io.olvid.messenger.openid.jsons.KeycloakUserDetailsAndStuff; @@ -383,9 +385,8 @@ public void success(Boolean result) { @Override public void failed(int rfc) { - authenticationRequiredOwnedIdentities.add(identityBytesKey); - AndroidNotificationManager.displayKeycloakAuthenticationRequiredNotification(identityBytesKey.bytes); - App.openAppDialogKeycloakAuthenticationRequired(kms.bytesOwnedIdentity, kms.clientId, kms.clientSecret, kms.serverUrl); + // in case of failure, we do nothing --> this is probably only a network error, and it will be tried again + // we do not want to prompt the user to authenticate in case of permanent connection error with the keycloak } }); } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/plus_button/ConfigurationScannedFragment.java b/obv_messenger/app/src/main/java/io/olvid/messenger/plus_button/ConfigurationScannedFragment.java index 15557fab..9126b035 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/plus_button/ConfigurationScannedFragment.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/plus_button/ConfigurationScannedFragment.java @@ -580,7 +580,7 @@ public void callback(String notificationName, HashMap userInfo) FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); transaction.replace(R.id.new_license_status_placeholder, newSubscriptionStatusFragment); transaction.commit(); - activateButton.setEnabled(true); + activateButton.setEnabled(apiKeyStatus != EngineAPI.ApiKeyStatus.LICENSES_EXHAUSTED); }); break; } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/services/AlarmPermissionStateChangeReceiver.java b/obv_messenger/app/src/main/java/io/olvid/messenger/services/AlarmPermissionStateChangeReceiver.java index 84d4197f..5cdb1dfa 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/services/AlarmPermissionStateChangeReceiver.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/services/AlarmPermissionStateChangeReceiver.java @@ -43,12 +43,6 @@ public void onReceive(Context context, Intent intent) { Logger.w("Permission to set exact alarms was granted!"); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); if (alarmManager != null && alarmManager.canScheduleExactAlarms()) { - // reset hidden dialog flag - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - SharedPreferences.Editor editor = prefs.edit(); - editor.remove(SettingsActivity.USER_DIALOG_HIDE_ALARM_SCHEDULING); - editor.apply(); - // restart unified foreground service (in case app was still running) context.startService(new Intent(context, UnifiedForegroundService.class)); UnifiedForegroundService.onAppBackground(context); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/services/AvailableSpaceHelper.java b/obv_messenger/app/src/main/java/io/olvid/messenger/services/AvailableSpaceHelper.java index 7a280d86..925c0ace 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/services/AvailableSpaceHelper.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/services/AvailableSpaceHelper.java @@ -32,7 +32,7 @@ import io.olvid.messenger.settings.SettingsActivity; public class AvailableSpaceHelper { - private final static long AVAILABLE_SPACE_WARNING_THRESHOLD = 50_000_000L; + public final static long AVAILABLE_SPACE_WARNING_THRESHOLD = 50_000_000L; private final static long MIN_REFRESH_INTERVAL_MILLIS = 600_000L; // 10 minutes private final static long MIN_WARNING_INTERVAL_MILLIS = 7_200_000L; // 2 hours diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/services/MDMConfigurationSingleton.java b/obv_messenger/app/src/main/java/io/olvid/messenger/services/MDMConfigurationSingleton.java index 05981e6a..4050a58f 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/services/MDMConfigurationSingleton.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/services/MDMConfigurationSingleton.java @@ -24,21 +24,34 @@ import android.os.Bundle; import android.util.Base64; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.net.URI; import java.security.KeyFactory; import java.security.PublicKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; +import java.util.Objects; +import java.util.regex.Matcher; +import io.olvid.engine.datatypes.ObvBase64; +import io.olvid.engine.engine.types.JsonIdentityDetails; +import io.olvid.engine.engine.types.identities.ObvIdentity; +import io.olvid.engine.engine.types.identities.ObvKeycloakState; import io.olvid.messenger.App; +import io.olvid.messenger.AppSingleton; +import io.olvid.messenger.activities.ObvLinkActivity; +import io.olvid.messenger.customClasses.ConfigurationPojo; +import io.olvid.messenger.customClasses.StringUtils; public class MDMConfigurationSingleton { - public static final String KEYCLOAK_CONFIGURATION_URI = "keycloak_configuration_uri"; - public static final String DISABLE_NEW_VERSION_NOTIFICATION = "disable_new_version_notification"; - public static final String SETTINGS_CONFIGURATION_URI = "settings_configuration_uri"; - public static final String WEBDAV_AUTOMATIC_BACKUP_URI = "webdav_automatic_backup_uri"; - public static final String WEBDAV_AUTOMATIC_BACKUP_WRITE_ONLY = "webdav_automatic_backup_write_only"; - public static final String WEBDAV_KEY_ESCROW_PUBLIC_KEY = "webdav_key_escrow_public_key"; + private static final String KEYCLOAK_CONFIGURATION_URI = "keycloak_configuration_uri"; + private static final String DISABLE_NEW_VERSION_NOTIFICATION = "disable_new_version_notification"; + private static final String SETTINGS_CONFIGURATION_URI = "settings_configuration_uri"; + private static final String WEBDAV_AUTOMATIC_BACKUP_URI = "webdav_automatic_backup_uri"; + private static final String WEBDAV_AUTOMATIC_BACKUP_WRITE_ONLY = "webdav_automatic_backup_write_only"; + private static final String WEBDAV_KEY_ESCROW_PUBLIC_KEY = "webdav_key_escrow_public_key"; private static MDMConfigurationSingleton INSTANCE = null; @@ -81,6 +94,11 @@ public MDMConfigurationSingleton() { if (webdavAutomaticBackupUri != null) { boolean writeOnly = restrictions.containsKey(WEBDAV_AUTOMATIC_BACKUP_WRITE_ONLY) && restrictions.getBoolean(WEBDAV_AUTOMATIC_BACKUP_WRITE_ONLY, false); + // if this identity is configured by keycloak, try to substitute variables + if (keycloakConfigurationUri != null) { + webdavAutomaticBackupUri = replaceWebDavUriVariablesFromKeycloakProfile(webdavAutomaticBackupUri, keycloakConfigurationUri); + } + try { URI mdmUri = new URI(webdavAutomaticBackupUri); String[] userInfo = (mdmUri.getUserInfo() == null) ? new String[0] : mdmUri.getUserInfo().split(":", 2); @@ -136,11 +154,22 @@ public MDMConfigurationSingleton() { public static MDMConfigurationSingleton getInstance() { if (INSTANCE == null) { - INSTANCE = new MDMConfigurationSingleton(); + synchronized (MDMConfigurationSingleton.class) { + if (INSTANCE == null) { + INSTANCE = new MDMConfigurationSingleton(); + } + } } return INSTANCE; } + // called after a profile creation, in case the WEBDAV_AUTOMATIC_BACKUP_URI contains substitution parameters + public static void reloadMDMConfiguration() { + if (INSTANCE != null) { + INSTANCE = null; + } + } + public static String getKeycloakConfigurationUri() { return getInstance().keycloakConfigurationUri; } @@ -163,4 +192,54 @@ public static String getWebdavKeyEscrowPublicKeyString() { public static PublicKey getWebdavKeyEscrowPublicKey() { return getInstance().webdavKeyEscrowPublicKey; } + + + private String replaceWebDavUriVariablesFromKeycloakProfile(String uri, String keycloakConfigurationUri) { + try { + Matcher matcher = ObvLinkActivity.CONFIGURATION_PATTERN.matcher(keycloakConfigurationUri); + if (matcher.find()) { + ConfigurationPojo configurationPojo = AppSingleton.getJsonObjectMapper().readValue(ObvBase64.decode(matcher.group(2)), ConfigurationPojo.class); + if (configurationPojo.keycloak != null) { + JsonIdentityDetails details = null; + + ObvIdentity[] ownedIdentities = AppSingleton.getEngine().getOwnedIdentities(); + for (ObvIdentity ownedIdentity : ownedIdentities) { + if (ownedIdentity.isKeycloakManaged() && ownedIdentity.isActive()) { + ObvKeycloakState keycloakState = AppSingleton.getEngine().getOwnedIdentityKeycloakState(ownedIdentity.getBytesIdentity()); + if (keycloakState != null && Objects.equals(keycloakState.keycloakServer, configurationPojo.keycloak.getServer())) { + if (details == null) { + details = ownedIdentity.getIdentityDetails(); + } else { + throw new Exception("Multiple identities managed by Keycloak"); + } + } + } + } + + if (details != null) { + String first_name = unAccentAndClean(details.getFirstName()); + String last_name = unAccentAndClean(details.getLastName()); + String position = unAccentAndClean(details.getPosition()); + String company = unAccentAndClean(details.getCompany()); + + return uri.replace("{{first_name}}", first_name).replace("{{last_name}}", last_name).replace("{{position}}", position).replace("{{company}}", company); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + if (uri.contains("{{first_name}}") || uri.contains("{{last_name}}") || uri.contains("{{position}}") || uri.contains("{{company}}")) { + return null; + } + return uri; + } + + @NonNull + private String unAccentAndClean(@Nullable String s) { + if (s == null) { + return ""; + } + return StringUtils.unAccent(s).toLowerCase().trim().replaceAll("[^a-z]", "_"); + } } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/settings/OtherPreferenceFragment.java b/obv_messenger/app/src/main/java/io/olvid/messenger/settings/OtherPreferenceFragment.java index 0b0de71a..e869afb8 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/settings/OtherPreferenceFragment.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/settings/OtherPreferenceFragment.java @@ -72,19 +72,12 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences(); if (sharedPreferences != null) { SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.remove(SettingsActivity.USER_DIALOG_HIDE_GOOGLE_APIS); - editor.remove(SettingsActivity.USER_DIALOG_HIDE_BACKGROUND_RESTRICTED); - editor.remove(SettingsActivity.USER_DIALOG_HIDE_BATTERY_OPTIMIZATION); - editor.remove(SettingsActivity.USER_DIALOG_HIDE_ALARM_SCHEDULING); - editor.remove(SettingsActivity.USER_DIALOG_HIDE_ALLOW_NOTIFICATIONS); - editor.remove(SettingsActivity.USER_DIALOG_HIDE_FULL_SCREEN_NOTIFICATION); editor.remove(SettingsActivity.USER_DIALOG_HIDE_OPEN_EXTERNAL_APP); editor.remove(SettingsActivity.USER_DIALOG_HIDE_FORWARD_MESSAGE_EXPLANATION); editor.remove(SettingsActivity.USER_DIALOG_HIDE_OPEN_EXTERNAL_APP_LOCATION); editor.remove(SettingsActivity.USER_DIALOG_HIDE_ADD_DEVICE_EXPLANATION); editor.remove(SettingsActivity.PREF_KEY_FIRST_CALL_AUDIO_PERMISSION_REQUESTED); editor.remove(SettingsActivity.PREF_KEY_FIRST_CALL_BLUETOOTH_PERMISSION_REQUESTED); - editor.remove(SettingsActivity.PREF_KEY_LAST_BACKUP_REMINDER_TIMESTAMP); editor.apply(); App.toast(R.string.toast_message_dialogs_restored, Toast.LENGTH_SHORT); Utils.dialogsLoaded = false; diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/settings/SettingsActivity.java b/obv_messenger/app/src/main/java/io/olvid/messenger/settings/SettingsActivity.java index 0b1e5e2a..75c2da13 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/settings/SettingsActivity.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/settings/SettingsActivity.java @@ -78,6 +78,7 @@ import io.olvid.messenger.databases.AppDatabase; import io.olvid.messenger.firebase.ObvFirebaseMessagingService; import io.olvid.messenger.google_services.GoogleServicesUtils; +import io.olvid.messenger.main.Utils; import io.olvid.messenger.services.BackupCloudProviderService; public class SettingsActivity extends LockableActivity implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { @@ -96,7 +97,6 @@ public class SettingsActivity extends LockableActivity implements PreferenceFrag static final String PREF_KEY_LAST_AVAILABLE_SPACE_WARNING_TIMESTAMP = "pref_key_last_available_space_warning_timestamp"; static final String PREF_KEY_FIRST_CALL_AUDIO_PERMISSION_REQUESTED = "pref_key_first_call_audio_permission_requested"; static final String PREF_KEY_FIRST_CALL_BLUETOOTH_PERMISSION_REQUESTED = "pref_key_first_call_bluetooth_permission_requested"; - static final String PREF_KEY_LAST_BACKUP_REMINDER_TIMESTAMP = "pref_key_last_backup_reminder_timestamp"; static final String PREF_KEY_COMPOSE_MESSAGE_ICON_PREFERRED_ORDER = "pref_key_compose_message_icon_preferred_order"; static final String PREF_KEY_PREFERRED_REACTIONS = "pref_key_preferred_reactions"; @@ -326,13 +326,7 @@ public enum BlockUntrustedCertificate { static final String PREF_KEY_NO_NOTIFY_CERTIFICATE_CHANGE_FOR_PREVIEWS = "pref_key_no_notify_certificate_change_for_previews"; static final boolean PREF_KEY_NO_NOTIFY_CERTIFICATE_CHANGE_FOR_PREVIEWS_DEFAULT = false; - - public static final String USER_DIALOG_HIDE_BATTERY_OPTIMIZATION = "user_dialog_hide_battery_optimization"; - public static final String USER_DIALOG_HIDE_BACKGROUND_RESTRICTED = "user_dialog_hide_background_restricted"; public static final String USER_DIALOG_HIDE_GOOGLE_APIS = "user_dialog_hide_google_apis"; - public static final String USER_DIALOG_HIDE_ALARM_SCHEDULING = "user_dialog_hide_alarm_scheduling"; - public static final String USER_DIALOG_HIDE_ALLOW_NOTIFICATIONS = "user_dialog_hide_allow_notifications"; - public static final String USER_DIALOG_HIDE_FULL_SCREEN_NOTIFICATION = "user_dialog_hide_full_screen_notification"; public static final String USER_DIALOG_HIDE_OPEN_EXTERNAL_APP = "user_dialog_hide_open_external_app"; public static final String USER_DIALOG_HIDE_FORWARD_MESSAGE_EXPLANATION = "user_dialog_hide_forward_message_explanation"; public static final String USER_DIALOG_HIDE_OPEN_EXTERNAL_APP_LOCATION = "user_dialog_hide_open_external_app_location"; @@ -533,14 +527,7 @@ public boolean onOptionsItemSelected(MenuItem item) { extraFeatures.add("beta"); } int uptimeSeconds = (int) ((System.currentTimeMillis() - App.appStartTimestamp) / 1000); - final String uptime; - if (uptimeSeconds > 86400) { - uptime = getResources().getQuantityString(R.plurals.text_app_uptime_days, uptimeSeconds / 86400, uptimeSeconds / 86400, (uptimeSeconds % 86400) / 3600, (uptimeSeconds % 3600) / 60, uptimeSeconds % 60); - } else if (uptimeSeconds > 3600) { - uptime = getString(R.string.text_app_uptime_hours, uptimeSeconds / 3600, (uptimeSeconds % 3600) / 60, uptimeSeconds % 60); - } else { - uptime = getString(R.string.text_app_uptime, uptimeSeconds / 60, uptimeSeconds % 60); - } + final String uptime = Utils.getUptime(this); builder.setTitle(R.string.dialog_title_about_olvid) .setPositiveButton(R.string.button_label_ok, null); StringBuilder sb = new StringBuilder(); @@ -814,17 +801,6 @@ public static void setFirstCallBluetoothPermissionRequested(boolean requested) { editor.apply(); } - public static long getLastBackupReminderTimestamp() { - return PreferenceManager.getDefaultSharedPreferences(App.getContext()).getLong(PREF_KEY_LAST_BACKUP_REMINDER_TIMESTAMP, 0); - } - - public static void setLastBackupReminderTimestamp(long timestamp) { - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(App.getContext()).edit(); - editor.putLong(PREF_KEY_LAST_BACKUP_REMINDER_TIMESTAMP, timestamp); - editor.apply(); - } - - public static boolean useSystemEmojis() { return PreferenceManager.getDefaultSharedPreferences(App.getContext()).getBoolean(PREF_KEY_USE_SYSTEM_EMOJIS, PREF_KEY_USE_SYSTEM_EMOJIS_DEFAULT); } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/CheckState.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/CheckState.kt new file mode 100644 index 00000000..279c6151 --- /dev/null +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/CheckState.kt @@ -0,0 +1,223 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.messenger.troubleshooting + +import android.Manifest +import android.app.ActivityManager +import android.app.AlarmManager +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import android.os.PowerManager +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import io.olvid.engine.Logger +import io.olvid.engine.engine.types.ObvBackupKeyInformation +import io.olvid.messenger.AppSingleton +import io.olvid.messenger.BuildConfig +import io.olvid.messenger.R +import io.olvid.messenger.google_services.GoogleServicesUtils +import io.olvid.messenger.services.AvailableSpaceHelper +import io.olvid.messenger.settings.SettingsActivity +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking + +const val ALARM_CHECK_STATE = "alarm" +const val BACKGROUND_CHECK_STATE = "background" +const val BATTERY_CHECK_STATE = "battery" +const val STORAGE_CHECK_STATE = "storage" +const val SOCKET_CHECK_STATE = "socket" +const val FULL_SCREEN_CHECK_STATE = "full_screen" +const val BACKUP_CHECK_STATE = "backup" + +const val MUTE_KEY_PREFIX = "mute_" + +@Stable +data class CheckState(val name: String, val troubleshootingDataStore: TroubleshootingDataStore, val statusIsOk: (K) -> Boolean = { (it is Boolean) && it }, val getStatus: () -> K) { + var status by mutableStateOf(getStatus()) + var valid by mutableStateOf(statusIsOk(getStatus())) + + fun refreshStatus() { + status = getStatus() + valid = statusIsOk(getStatus()) + } + + val isMute = troubleshootingDataStore.isMute("$MUTE_KEY_PREFIX$name") + suspend fun updateMute(mute: Boolean) { + troubleshootingDataStore.updateMute("$MUTE_KEY_PREFIX$name", mute) + } +} + +@Composable +internal fun LifecycleCheckerEffect( + checks: List>, + lifecycleEvent: Lifecycle.Event = Lifecycle.Event.ON_RESUME +) { + val checkerObserver = remember(checks) { + LifecycleEventObserver { _, event -> + if (event == lifecycleEvent) { + for (check in checks) { + check.refreshStatus() + } + } + } + } + val lifecycle = LocalLifecycleOwner.current.lifecycle + DisposableEffect(lifecycle, checkerObserver) { + lifecycle.addObserver(checkerObserver) + onDispose { lifecycle.removeObserver(checkerObserver) } + } +} + + +fun ComponentActivity.getBatteryOptimizationsState() = + if (VERSION.SDK_INT >= VERSION_CODES.M) { + (getSystemService(ComponentActivity.POWER_SERVICE) as? PowerManager)?.isIgnoringBatteryOptimizations( + packageName + ) == true + } else { + true + } + +fun ComponentActivity.getAlarmState() = + if (VERSION.SDK_INT >= VERSION_CODES.S) { + (getSystemService(ComponentActivity.ALARM_SERVICE) as? AlarmManager)?.canScheduleExactAlarms() == true + } else { + true + } + +fun ComponentActivity.getBackgroundState() = + if (VERSION.SDK_INT >= VERSION_CODES.P) { + (getSystemService(ComponentActivity.ACTIVITY_SERVICE) as? ActivityManager)?.isBackgroundRestricted == false + } else { + true + } + +fun getStorageState() = + (AvailableSpaceHelper.getAvailableSpace() + ?: 0) > AvailableSpaceHelper.AVAILABLE_SPACE_WARNING_THRESHOLD + +fun Context.getPermanentSocketState() = + if (!BuildConfig.USE_FIREBASE_LIB || !GoogleServicesUtils.googleServicesAvailable(this)) { + SettingsActivity.usePermanentWebSocket() + } else { + true + } + +fun Context.getFullScreenIntentState() : Boolean { + if (VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) { + return getSystemService( + NotificationManager::class.java + ).canUseFullScreenIntent() + } + return true +} + +fun getBackupState(): Int { + val info: ObvBackupKeyInformation? = try { + AppSingleton.getEngine().backupKeyInformation + } catch (e: Exception) { + // this will be retried the next time MainActivity is started + Logger.e("Unable to retrieve backup info") + return -1 + } + if (info == null) { + // no backup key generated + return 2 + } else { + if (!SettingsActivity.useAutomaticBackup() && info.lastBackupExport + 7 * 86400000L <= System.currentTimeMillis()) { + return 1 + } + } + return 0 +} + +data class BackupStateInfo(val title: String, val description: String, val critical: Boolean) + +fun Context.getBackupStateInfo(): BackupStateInfo? { + var title: Int? = null + var description: Int = -1 + var critical = false + try { + val info: ObvBackupKeyInformation? = try { + AppSingleton.getEngine().backupKeyInformation + } catch (e: Exception) { + // this will be retried the next time MainActivity is started + Logger.e("Unable to retrieve backup info") + return null + } + if (info == null) { + // no backup key generated + critical = true + title = R.string.snackbar_message_setup_backup + description = R.string.dialog_message_setup_backup_explanation + } else { + if (!SettingsActivity.useAutomaticBackup() && info.lastBackupExport + 7 * 86400000L < System.currentTimeMillis() + ) { + // no automatic backups, and no backups since more that a week + title = R.string.snackbar_message_remember_to_backup + description = + if (BuildConfig.USE_GOOGLE_LIBS && GoogleServicesUtils.googleServicesAvailable(this)) { + R.string.dialog_message_remember_to_backup_explanation + } else { + R.string.dialog_message_remember_to_backup_explanation_no_google + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + + return title?.let { + BackupStateInfo(getString(title), getString(description), critical) + } +} + +fun ComponentActivity.shouldShowTroubleshootingSnackbar(): Boolean { + val troubleshootingDataStore = TroubleshootingDataStore(this) + return listOf( + CheckState(BATTERY_CHECK_STATE, troubleshootingDataStore) { getBatteryOptimizationsState() }, + CheckState(ALARM_CHECK_STATE, troubleshootingDataStore) { getAlarmState() }, + CheckState(BACKGROUND_CHECK_STATE, troubleshootingDataStore) { getBackgroundState() }, + CheckState(STORAGE_CHECK_STATE, troubleshootingDataStore) { getStorageState() }, + CheckState(SOCKET_CHECK_STATE, troubleshootingDataStore) { getPermanentSocketState() }, + CheckState(FULL_SCREEN_CHECK_STATE, troubleshootingDataStore) { getFullScreenIntentState() }, + CheckState(BACKUP_CHECK_STATE, troubleshootingDataStore) { getBackupState() == 0 }, + ).any { checkState -> checkState.valid.not() && runBlocking { checkState.isMute.first() }.not() } + .or( + if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED + } else { + false + } + ) +} \ No newline at end of file diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/PingListener.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/PingListener.kt new file mode 100644 index 00000000..5660fd5d --- /dev/null +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/PingListener.kt @@ -0,0 +1,79 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.messenger.troubleshooting + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import io.olvid.engine.engine.types.EngineNotificationListener +import io.olvid.engine.engine.types.EngineNotifications +import io.olvid.messenger.AppSingleton +import io.olvid.messenger.main.Utils + +@Composable +fun PingListener(connected: Boolean,pingCallback : (lastPing: Long) -> Unit) { + + val pingListener = remember { + object : EngineNotificationListener { + var registrationNumber: Long = 0 + override fun callback(notificationName: String, userInfo: HashMap) { + when (notificationName) { + EngineNotifications.PING_LOST -> { + pingCallback(-1) + } + EngineNotifications.PING_RECEIVED -> { + (userInfo[EngineNotifications.PING_RECEIVED_DELAY_KEY] as Long?)?.let { + pingCallback(it) + } + } + } + } + + override fun setEngineNotificationListenerRegistrationNumber(registrationNumber: Long) { + this.registrationNumber = registrationNumber + } + + override fun getEngineNotificationListenerRegistrationNumber(): Long { + return registrationNumber + } + + override fun hasEngineNotificationListenerRegistrationNumber(): Boolean = + registrationNumber != 0L + } + } + + DisposableEffect(connected, pingListener) { + if (connected) { + Utils.startPinging() + AppSingleton.getEngine() + .addNotificationListener(EngineNotifications.PING_LOST, pingListener) + AppSingleton.getEngine() + .addNotificationListener(EngineNotifications.PING_RECEIVED, pingListener) + } + onDispose { + pingCallback(-1) + Utils.stopPinging() + AppSingleton.getEngine() + .removeNotificationListener(EngineNotifications.PING_LOST, pingListener) + AppSingleton.getEngine() + .removeNotificationListener(EngineNotifications.PING_RECEIVED, pingListener) + } + } +} \ No newline at end of file diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingActivity.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingActivity.kt new file mode 100644 index 00000000..91205d9a --- /dev/null +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingActivity.kt @@ -0,0 +1,540 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.messenger.troubleshooting + +import android.Manifest.permission +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import android.os.Bundle +import android.os.storage.StorageManager +import android.provider.Settings +import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS +import android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS +import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS +import android.text.format.Formatter +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle.State.RESUMED +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale +import com.google.accompanist.themeadapter.appcompat.AppCompatTheme +import io.olvid.messenger.AppSingleton +import io.olvid.messenger.BuildConfig +import io.olvid.messenger.R +import io.olvid.messenger.R.string +import io.olvid.messenger.customClasses.StringUtils +import io.olvid.messenger.firebase.ObvFirebaseMessagingService +import io.olvid.messenger.google_services.GoogleServicesUtils +import io.olvid.messenger.services.AvailableSpaceHelper +import io.olvid.messenger.services.UnifiedForegroundService +import io.olvid.messenger.settings.SettingsActivity +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@OptIn(ExperimentalPermissionsApi::class) +class TroubleshootingActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val troubleshootingDataStore = TroubleshootingDataStore(context) + + val batteryOptimizationState = remember { + CheckState(BATTERY_CHECK_STATE, troubleshootingDataStore) { getBatteryOptimizationsState() } + } + val alarmState = remember { + CheckState(ALARM_CHECK_STATE, troubleshootingDataStore) { getAlarmState() } + } + val backgroundRestrictionState = remember { + CheckState(BACKGROUND_CHECK_STATE, troubleshootingDataStore) { getBackgroundState() } + } + val storageState = remember { + CheckState(STORAGE_CHECK_STATE, troubleshootingDataStore) { getStorageState() } + } + val permanentSocketState = remember { + CheckState(SOCKET_CHECK_STATE, troubleshootingDataStore) { getPermanentSocketState() } + } + val backupState = remember { + CheckState(BACKUP_CHECK_STATE, troubleshootingDataStore, statusIsOk = {a -> a == 0}) { getBackupState()} + } + val fullScreenIntentState = remember { + CheckState(FULL_SCREEN_CHECK_STATE, troubleshootingDataStore) { getFullScreenIntentState() } + } + + val postNotificationsState = rememberPermissionState( permission.POST_NOTIFICATIONS ) + val cameraState = rememberPermissionState(permission.CAMERA) + val microphoneState = rememberPermissionState(permission.RECORD_AUDIO) + + LifecycleCheckerEffect( + checks = listOf>( + batteryOptimizationState, + alarmState, + backgroundRestrictionState, + storageState, + permanentSocketState, + backupState, + fullScreenIntentState + ) + ) + + val troubleshootingItems : MutableState> = remember { + mutableStateOf(mutableListOf()) + } + + LaunchedEffect(Unit) { + val list : ArrayList> = ArrayList() // triple is (valid, critical, TroubleshootItemType) + if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + list.add(Triple(postNotificationsState.status.isGranted, true, TroubleshootingItemType.NOTIFICATIONS)) + } + + if (VERSION.SDK_INT >= VERSION_CODES.M) { + list.add(Triple(cameraState.status.isGranted, false, TroubleshootingItemType.CAMERA)) + } + + if (VERSION.SDK_INT >= VERSION_CODES.M) { + list.add(Triple(microphoneState.status.isGranted, false, TroubleshootingItemType.MICROPHONE)) + } + + if (VERSION.SDK_INT >= VERSION_CODES.M) { + list.add(Triple(batteryOptimizationState.valid, true, TroubleshootingItemType.BATTERY_OPTIMIZATION)) + } + + if (VERSION.SDK_INT >= VERSION_CODES.P) { + list.add(Triple(backgroundRestrictionState.valid, true, TroubleshootingItemType.BACKGROUND_RESTRICTION)) + } + + if (VERSION.SDK_INT >= VERSION_CODES.S) { + list.add(Triple(alarmState.valid, true, TroubleshootingItemType.ALARM)) + } + + if (VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) { + list.add(Triple(fullScreenIntentState.valid, true, TroubleshootingItemType.FULL_SCREEN_INTENT)) + } + + if (!BuildConfig.USE_FIREBASE_LIB || !GoogleServicesUtils.googleServicesAvailable(this@TroubleshootingActivity)) { + list.add(Triple(permanentSocketState.valid, true, TroubleshootingItemType.PERMANENT_WEBSOCKET)) + } + + list.add(Triple(backupState.valid, true, TroubleshootingItemType.BACKUPS)) + + list.add(Triple(AppSingleton.getWebsocketConnectivityStateLiveData().value == 2, true, TroubleshootingItemType.CONNECTIVITY)) + + list.add(Triple(storageState.valid, true, TroubleshootingItemType.STORAGE)) + + list.sortBy { + when { + it.first -> 2 + it.second.not() -> 1 + else -> 0 + } + } + troubleshootingItems.value = list.map { + it.third + } + } + + AppCompatTheme { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + FaqLinkHeader { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://olvid.io/faq/") + ) + ) + } + + troubleshootingItems.value.forEach { troubleshootingItem -> + when(troubleshootingItem) { + + TroubleshootingItemType.NOTIFICATIONS -> { + val notificationsPermissionLauncher = rememberLauncherForActivityResult( + contract = RequestPermission(), + onResult = { granted -> + if (VERSION.SDK_INT >= VERSION_CODES.M) { + if (granted.not() && postNotificationsState.status.shouldShowRationale.not() && shouldShowRequestPermissionRationale( + permission.POST_NOTIFICATIONS + ).not() + ) { + openAppNotificationSettings(context = context) + } + } + } + ) + TroubleShootItem( + title = stringResource(id = string.troubleshooting_notifications_valid_title), + description = stringResource( + id = string.troubleshooting_notifications_valid_description + ) + if (BuildConfig.USE_FIREBASE_LIB && GoogleServicesUtils.googleServicesAvailable( + context + ) && !SettingsActivity.disablePushNotifications() + ) { + "\n\n" + stringResource( + string.dialog_message_about_last_push_notification, + if (ObvFirebaseMessagingService.getLastPushNotificationTimestamp() == null) { + "-" + } else { + StringUtils.getLongNiceDateString( + context, + ObvFirebaseMessagingService.getLastPushNotificationTimestamp() + ) + } + ) + } else "", + titleInvalid = stringResource(id = string.troubleshooting_notifications_invalid_title), + descriptionInvalid = stringResource(id = string.troubleshooting_notifications_invalid_description), + valid = postNotificationsState.status.isGranted + ) { + TextButton( + onClick = + { + notificationsPermissionLauncher.launch(permission.POST_NOTIFICATIONS) + } + ) { + Text(text = stringResource(id = string.troubleshooting_request_permission)) + } + } + } + + TroubleshootingItemType.CAMERA -> { + TroubleShootItem( + title = stringResource(id = string.troubleshooting_camera_valid_title), + description = stringResource(id = string.troubleshooting_camera_valid_description), + titleInvalid = stringResource(id = string.troubleshooting_camera_invalid_title), + valid = cameraState.status.isGranted, + critical = false, + ) { + TextButton( + onClick = + { + cameraState.launchPermissionRequest() + coroutineScope.launch { + delay(100) + if (lifecycle.currentState == RESUMED) { + startSettingsActivity() + } + } + } + ) { + Text(text = stringResource(id = string.troubleshooting_request_permission)) + } + } + } + + TroubleshootingItemType.MICROPHONE -> { + TroubleShootItem( + title = stringResource(id = string.troubleshooting_audio_recording_valid_title), + description = stringResource(id = string.troubleshooting_audio_recording_valid_description), + titleInvalid = stringResource(id = string.troubleshooting_audio_recording_invalid_title), + valid = microphoneState.status.isGranted, + critical = false, + ) { + TextButton( + onClick = + { + microphoneState.launchPermissionRequest() + coroutineScope.launch { + delay(100) + if (lifecycle.currentState == RESUMED) { + startSettingsActivity() + } + } + } + ) { + Text(text = stringResource(id = string.troubleshooting_request_permission)) + } + } + } + + TroubleshootingItemType.BATTERY_OPTIMIZATION -> { + TroubleShootItem( + title = stringResource(id = string.troubleshooting_battery_optimization_valid_title), + description = stringResource(id = string.troubleshooting_battery_optimization_valid_description), + descriptionInvalid = stringResource(id = string.troubleshooting_battery_optimization_invalid_description), + valid = batteryOptimizationState.valid, + checkState = batteryOptimizationState + ) { + TextButton( + onClick = { + startSettingsActivity(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + } + ) { + Text(text = stringResource(id = string.troubleshooting_request_permission)) + } + } + } + + TroubleshootingItemType.BACKGROUND_RESTRICTION -> { + TroubleShootItem( + title = stringResource(id = string.troubleshooting_background_restriction_valid_title), + description = stringResource( + id = string.troubleshooting_background_restriction_valid_description + ), + descriptionInvalid = stringResource(id = string.troubleshooting_background_restriction_invalid_description), + valid = backgroundRestrictionState.valid, + checkState = backgroundRestrictionState + ) { + TextButton( + onClick = { + startSettingsActivity() + } + ) { + Text(text = stringResource(id = string.button_label_app_settings)) + } + } + } + + TroubleshootingItemType.ALARM -> { + TroubleShootItem( + title = stringResource(id = string.troubleshooting_alarm_valid_title), + description = stringResource( + id = string.troubleshooting_alarm_valid_description + ), + descriptionInvalid = stringResource(id = string.troubleshooting_alarm_invalid_description), + valid = alarmState.valid, + checkState = alarmState + ) { + TextButton( + onClick = + { + startSettingsActivity(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM) + } + ) { + Text(text = stringResource(id = string.button_label_app_settings)) + } + } + } + + TroubleshootingItemType.FULL_SCREEN_INTENT -> { + TroubleShootItem( + title = stringResource(id = R.string.troubleshooting_incoming_call_valid_title), + description = stringResource(id = R.string.troubleshooting_incoming_call_valid_description), + descriptionInvalid = stringResource(id = R.string.troubleshooting_incoming_call_invalid_description), + valid = fullScreenIntentState.valid, + checkState = fullScreenIntentState + ) { + TextButton( + onClick = + { + startSettingsActivity(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT) + } + ) { + Text(text = stringResource(id = string.button_label_app_settings)) + } + } + } + + TroubleshootingItemType.PERMANENT_WEBSOCKET -> { + TroubleShootItem( + title = stringResource(id = string.troubleshooting_google_valid_title), + description = stringResource(id = string.troubleshooting_google_valid_description), + descriptionInvalid = stringResource(id = string.troubleshooting_google_invalid_description), + valid = permanentSocketState.valid, + checkState = permanentSocketState + ) { + TextButton( + onClick = + { + SettingsActivity.setUsePermanentWebSocket(true) + startService( + Intent( + this@TroubleshootingActivity, + UnifiedForegroundService::class.java + ) + ) + permanentSocketState.refreshStatus() + } + ) { + Text(text = stringResource(id = string.button_label_enable_websocket)) + } + } + } + + TroubleshootingItemType.BACKUPS -> { + val backupsStateInfo = remember(backupState.getStatus()) { + getBackupStateInfo() + } + TroubleShootItem( + title = stringResource(id = R.string.button_label_backup_settings), + description = stringResource(id = R.string.troubleshooting_backup_valid_description), + titleInvalid = backupsStateInfo?.title, + descriptionInvalid = backupsStateInfo?.description, + valid = backupState.valid, + checkState = backupState + ) { + TextButton( + onClick = + { + val intent = Intent( + this@TroubleshootingActivity, + SettingsActivity::class.java + ) + intent.putExtra( + SettingsActivity.SUB_SETTING_PREF_KEY_TO_OPEN_INTENT_EXTRA, + SettingsActivity.PREF_HEADER_KEY_BACKUP + ) + startActivity(intent) + + } + ) { + Text(text = stringResource(id = string.button_label_backup_settings)) + } + } + } + + TroubleshootingItemType.CONNECTIVITY -> { + var ping by remember { + mutableStateOf("-") + } + val connectivityState by AppSingleton.getWebsocketConnectivityStateLiveData() + .observeAsState() + PingListener(connectivityState == 2) { + ping = when (it) { + -1L -> { + getString(R.string.label_over_max_ping_delay, 5) + } + + 0L -> { + "-" + } + + else -> { + getString(R.string.label_ping_delay, it) + } + } + } + TroubleShootItem( + title = when (connectivityState) { + 2 -> stringResource(id = R.string.label_ping_connectivity_connected) + 1 -> stringResource(id = R.string.label_ping_connectivity_connecting) + else -> stringResource(id = R.string.label_ping_connectivity_none) + }, + description = ping, valid = connectivityState == 2 + ) { + } + } + + + TroubleshootingItemType.STORAGE -> { + TroubleShootItem( + title = stringResource(id = string.troubleshooting_storage_valid_title), + description = stringResource( + id = string.troubleshooting_storage_valid_description, + Formatter.formatShortFileSize( + context, + AvailableSpaceHelper.getAvailableSpace() ?: 0 + ) + ), + titleInvalid = stringResource(id = string.troubleshooting_storage_invalid_title), + descriptionInvalid = stringResource( + id = string.troubleshooting_storage_invalid_description, + Formatter.formatShortFileSize( + context, + AvailableSpaceHelper.getAvailableSpace() ?: 0 + ) + ), + valid = storageState.valid, + checkState = storageState + ) { + TextButton( + onClick = + { + if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) { + startActivity(Intent(StorageManager.ACTION_MANAGE_STORAGE)) + } + } + ) { + Text(text = stringResource(id = string.button_label_manage_storage)) + } + } + } + } + } + + AppVersionHeader(betaEnabled = SettingsActivity.getBetaFeaturesEnabled()) + } + } + } + } +} + +fun Activity.startSettingsActivity(action: String = ACTION_APPLICATION_DETAILS_SETTINGS) = + startActivity( + Intent( + action, + Uri.fromParts("package", packageName, null) + ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + +private fun openAppNotificationSettings(context: Context) { + val intent = Intent().apply { + when { + VERSION.SDK_INT >= VERSION_CODES.O -> { + action = ACTION_APP_NOTIFICATION_SETTINGS + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + + else -> { + action = "android.settings.APP_NOTIFICATION_SETTINGS" + putExtra("app_package", context.packageName) + putExtra("app_uid", context.applicationInfo.uid) + } + } + } + context.startActivity(intent) +} \ No newline at end of file diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingComponents.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingComponents.kt new file mode 100644 index 00000000..3d72fc6c --- /dev/null +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingComponents.kt @@ -0,0 +1,344 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.messenger.troubleshooting + +import android.content.res.Configuration +import android.os.Build +import android.os.Build.VERSION +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.accompanist.themeadapter.appcompat.AppCompatTheme +import io.olvid.messenger.BuildConfig +import io.olvid.messenger.R +import io.olvid.messenger.R.color +import io.olvid.messenger.R.drawable +import io.olvid.messenger.R.string +import io.olvid.messenger.customClasses.formatMarkdown +import io.olvid.messenger.main.Utils +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +@Composable +fun AppVersionHeader(betaEnabled: Boolean) { + val context = LocalContext.current + var uptime by remember { + mutableStateOf(Utils.getUptime(context)) + } + LaunchedEffect(Unit) { + while (true) { + delay(1.seconds) + uptime = Utils.getUptime(context) + } + } + Column { + Text( + modifier = Modifier + .widthIn(max = 400.dp) + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp), + text = AnnotatedString(stringResource( + string.troubleshooting_header, + BuildConfig.VERSION_NAME + if (betaEnabled) " beta" else "", + BuildConfig.VERSION_CODE, + "${Build.BRAND} ${Build.MODEL}", + VERSION.SDK_INT, + uptime + )).formatMarkdown(), + color = colorResource(id = color.almostBlack), + fontSize = 16.sp, + ) + } +} + +@Composable +fun FaqLinkHeader(openFaq: () -> Unit) { + Column { + Text( + modifier = Modifier + .widthIn(max = 400.dp) + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp), + text = stringResource(id = string.troubleshooting_faq_description), + color = colorResource(id = color.almostBlack), + fontSize = 16.sp, + ) + ClickableText( + modifier = Modifier.align(CenterHorizontally) + .padding(8.dp), + text = AnnotatedString( + text = stringResource(id = R.string.troubleshooting_faq_link), + spanStyle = SpanStyle(color = colorResource(id = color.olvid_gradient_light)) + ), + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight(400), + color = Color(0xFF8B8D97), + textAlign = TextAlign.Center + ) + ) { + openFaq() + } + } +} + + +@Composable +fun TroubleShootItem( + title: String, + description: String, + titleInvalid: String? = null, + descriptionInvalid: String? = null, + valid: Boolean, + critical: Boolean = true, + checkState: CheckState? = null, + actions: @Composable RowScope.() -> Unit +) { + var expanded by rememberSaveable { + mutableStateOf(valid.not()) + } + val mute: Boolean by checkState?.let{ it.isMute.collectAsState(true) } ?: remember { mutableStateOf(false) } + val borderWidth: Float by animateFloatAsState(targetValue = if (critical && valid.not() && mute == false) 2f else 1f) + val borderColor: Color by animateColorAsState(targetValue = if (critical && valid.not() && mute == false) colorResource(id = R.color.red) else Color(0x6E111111)) + Column( + modifier = Modifier + .widthIn(max = 400.dp) + .border( + border = BorderStroke(borderWidth.dp, borderColor), + shape = RoundedCornerShape(12.dp) + ) + .clip( + shape = RoundedCornerShape(12.dp) + ) + .background( + color = colorResource(id = color.itemBackground), + shape = RoundedCornerShape(12.dp) + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + expanded = !expanded + } + .padding(top = 8.dp, start = 16.dp, end = 16.dp) + ) { + + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.Top + ) + { + Column( + modifier = Modifier.weight(1f, fill = true), + ) { + Row { + val rotation: Float by animateFloatAsState(targetValue = if (expanded) 90f else 0f) + Image( + modifier = Modifier + .padding(top = 1.dp) + .size(12.dp) + .rotate(degrees = rotation) + .align(CenterVertically), + painter = painterResource(id = R.drawable.ic_chevron_right_compact), + contentDescription = "") + Text( + modifier = Modifier + .padding(start = 8.dp) + .weight(1f, true) + .align(CenterVertically), + text = if (valid) title else titleInvalid ?: title, + color = colorResource(id = color.almostBlack), + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + ) + Image( + modifier = Modifier + .size(32.dp), + painter = painterResource(id = if (valid) drawable.ic_ok_green else drawable.ic_error_outline), + colorFilter = ColorFilter.tint(color = colorResource(id = R.color.golden)) + .takeIf { valid.not() && critical.not() }, + contentDescription = "" + ) + } + AnimatedVisibility(visible = expanded) { + Text( + modifier = Modifier.padding(top = 4.dp, bottom = if (valid) 4.dp else 0.dp), + text = if (valid) description else descriptionInvalid ?: description, + color = colorResource(id = color.greyTint), + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + lineHeight = 18.sp, + ) + } + } + } + AnimatedVisibility(visible = valid.not()) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 8.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + checkState?.let { checkState -> + val coroutineScope = rememberCoroutineScope() + val interactionSource = remember { MutableInteractionSource() } + Image( + modifier = Modifier + .align(CenterVertically) + .clickable( + interactionSource = interactionSource, + indication = rememberRipple(bounded = false) + ) { coroutineScope.launch { checkState.updateMute(mute.not()) } }, + painter = painterResource(id = drawable.ic_notification_muted), + colorFilter = ColorFilter.tint(colorResource(id = R.color.almostBlack)), + contentDescription = "mute", + alpha = if (mute) 1f else 0.3f + ) + AnimatedVisibility(visible = mute) { + Text( + modifier = Modifier + .clickable( + interactionSource = interactionSource, + indication = null + ) { coroutineScope.launch { checkState.updateMute(mute.not()) } } + .padding(start = 4.dp), + text = stringResource(id = R.string.troubleshooting_ignored), + fontSize = 12.sp, + ) + } + } + Spacer(modifier = Modifier.weight(1f, true)) + actions() + } + } + AnimatedVisibility(visible = valid) { + Spacer(modifier = Modifier.height(8.dp)) + } + } +} + +enum class TroubleshootingItemType { + NOTIFICATIONS, + CAMERA, + MICROPHONE, + BATTERY_OPTIMIZATION, + BACKGROUND_RESTRICTION, + ALARM, + FULL_SCREEN_INTENT, + PERMANENT_WEBSOCKET, + BACKUPS, + CONNECTIVITY, + STORAGE, +} + +@Preview +@Composable +fun AppVersionHeaderPreview() { + AppCompatTheme { + AppVersionHeader(true) + } +} + +@Preview(locale = "fr-rFR") +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun TroubleShootItemPreview() { + AppCompatTheme { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(16.dp) + ) { + TroubleShootItem( + title = "Title", + description = "description", + valid = false, + critical = true, + checkState = CheckState( + "test", + TroubleshootingDataStore(LocalContext.current), + ) { true } + ) { + TextButton( + shape = RoundedCornerShape(size = 8.dp), + onClick = {} + ) { + Text(text = stringResource(id = string.troubleshooting_request_permission)) + } + } + } + } +} \ No newline at end of file diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingDataStore.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingDataStore.kt new file mode 100644 index 00000000..1741dcd1 --- /dev/null +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingDataStore.kt @@ -0,0 +1,66 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.messenger.troubleshooting + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.preferencesDataStore +import io.olvid.engine.Logger +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import java.io.IOException + +class TroubleshootingDataStore(private val context: Context) { + companion object { + private val Context.troubleshootingDataStore by preferencesDataStore("troubleshooting") + } + + suspend fun load() = context.troubleshootingDataStore.data.catch { exception -> + if (exception is IOException) { + Logger.e("troubleshootingDatastore", exception) + } else { + Logger.e("unable to load troubleshootingDatastore") + } + emit(emptyPreferences()) + }.first() + + fun isMute(item: String) = context.troubleshootingDataStore.data.catch { exception -> + if (exception is IOException) { + Logger.e("troubleshootingDatastore", exception) + } else { + Logger.e("unable to read troubleshootingDatastore") + } + emit(emptyPreferences()) + }.map { preferences -> + preferences[booleanPreferencesKey(item)] ?: false + } + + suspend fun updateMute(item: String, value: Boolean) { + try { + context.troubleshootingDataStore.edit { preferences -> + preferences[booleanPreferencesKey(item)] = value + } + } catch (exception: Exception) { + } + } +} \ No newline at end of file diff --git a/obv_messenger/app/src/main/res/anim/dismiss_from_fling_up.xml b/obv_messenger/app/src/main/res/anim/dismiss_from_fling_up.xml new file mode 100644 index 00000000..f43db5a6 --- /dev/null +++ b/obv_messenger/app/src/main/res/anim/dismiss_from_fling_up.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/obv_messenger/app/src/main/res/drawable/ic_chevron_right_compact.xml b/obv_messenger/app/src/main/res/drawable/ic_chevron_right_compact.xml new file mode 100644 index 00000000..93f2432d --- /dev/null +++ b/obv_messenger/app/src/main/res/drawable/ic_chevron_right_compact.xml @@ -0,0 +1,6 @@ + + + diff --git a/obv_messenger/app/src/main/res/drawable/ic_contacts_filter.xml b/obv_messenger/app/src/main/res/drawable/ic_contacts_filter.xml new file mode 100644 index 00000000..5ef814a9 --- /dev/null +++ b/obv_messenger/app/src/main/res/drawable/ic_contacts_filter.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/obv_messenger/app/src/main/res/layout/activity_main.xml b/obv_messenger/app/src/main/res/layout/activity_main.xml index 4f52dcee..b50d8627 100644 --- a/obv_messenger/app/src/main/res/layout/activity_main.xml +++ b/obv_messenger/app/src/main/res/layout/activity_main.xml @@ -333,4 +333,8 @@ + \ No newline at end of file diff --git a/obv_messenger/app/src/main/res/menu/menu_main.xml b/obv_messenger/app/src/main/res/menu/menu_main.xml index 4e774918..6c9619c4 100644 --- a/obv_messenger/app/src/main/res/menu/menu_main.xml +++ b/obv_messenger/app/src/main/res/menu/menu_main.xml @@ -17,4 +17,9 @@ android:orderInCategory="200" android:title="@string/menu_action_storage_management" app:showAsAction="never"/> + diff --git a/obv_messenger/app/src/main/res/values-fr/strings.xml b/obv_messenger/app/src/main/res/values-fr/strings.xml index 6021dd22..f1cca7fa 100644 --- a/obv_messenger/app/src/main/res/values-fr/strings.xml +++ b/obv_messenger/app/src/main/res/values-fr/strings.xml @@ -50,7 +50,6 @@ Vous êtes sur le point d\'abandonner une invitation. Abandonner l\'invitation en cours\u00a0? Vous utilisez Olvid pour Android\nVersion %1$s (build %2$d)\n\nAPI serveur\u00a0: %3$d\nSchéma DB moteur\u00a0: %4$d\nSchéma DB application\u00a0: %5$d\nApp démarrée depuis\u00a0: %6$s Vous utilisez Olvid pour Android\nVersion %1$s (build %2$d)\n\nAPI serveur\u00a0: %3$d\nSchéma DB moteur\u00a0: %4$d\nSchéma DB application\u00a0: %5$d\n\nFonctionnalités additionnelles\u00a0: %6$s\nApp démarrée depuis\u00a0: %7$s - L\'exécution en arrière-plan semble avoir été limitée pour Olvid. L\'application ne pourra fonctionner normalement tant qu\'elle fait partie de la liste des applications dont l\'accès est limité.\n\nVous pouvez modifier ce réglage depuis :\nParamètres\u00a0d\'Olvid\u00a0> Options\u00a0avancées\u00a0> Batterie\u00a0> Restriction\u00a0de\u00a0l\'activité\u00a0en\u00a0arrière-plan Souhaitez-vous présenter %1$s à %2$s\u00a0? Souhaitez-vous présenter %1$s aux %2$d contacts suivants\u00a0?\n\n%3$s Souhaitez-vous supprimer définitivement la pièce jointe %1$s de ce message\u00a0?\nCette action est irréversible. @@ -65,7 +64,6 @@ Souhaitez-vous réellement redémarrer le protocole d\'établissement de canal de discussion sécurisé en cours ? Interrompre l\'invitation\u00a0? À propos d\'Olvid - Restriction de l\'activité en arrière plan Présentation de contacts Supprimer la pièce jointe Supprimer la discussion @@ -88,15 +86,17 @@ Partager avec… Créer un raccourci vers… Choisissez un nom qui sera affiché chez vos contacts. Ces informations ne seront jamais envoyées aux serveurs d\'Olvid. - Cet écran affiche la liste des contacts auxquels vous faites confiance.\n\nPour ajouter de nouveaux contacts, appuyez sur le + bleu en bas de cet écran. - Cet écran affiche vos discussions en cours. - Cet écran affiche la liste des groupes auxquels vous appartenez. - Cet écran affiche la liste des invitations reçues ou envoyées. + Aucun contact pour le moment + Apuyez sur le + bleu ci-dessous pour ajouter un contact. + Aucune discussion pour le moment + Les discussions avec vos contacts et groupes s\'afficheront ici. + Aucun groupe pour le moment + Appuyez sur \"Nouveau groupe\" pour en créer un. L\'administrateur du groupe a mis à jour la Group Card. L\'ancienne version et la nouvelle sont affichées ci-dessous.\n\nActualisez les informations du groupe en cliquant «\u00a0mettre à jour\u00a0». Un administrateur a mis à jour les détails du groupe. La nouvelle version est affichée ci-dessous.\n\nUtilisez cette version en appuyant sur «\u00a0mettre à jour\u00a0». Votre contact a mis à jour son Olvid Card. L\'ancienne version et la nouvelle sont affichées ci-dessous.\n\nActualisez les informations de votre contact en cliquant "mettre à jour". Un canal sécurisé est actuellement en cours d\'établissement. Vous ne pouvez pas entamer de discussion avec ce contact tant que cela n\'est pas terminé. Ce processus devrait seulement prendre quelques secondes si vous et votre contact êtes tous deux en ligne.\n\nSi vous pensez que quelque chose s\'est mal passé, vous pouvez redémarrer l\'établissement de canal. - Aucun contact ne correspond à la recherche + Aucun contact trouvé pour cette recherche %1$s : %2$s Société (facultatif) Écrire un message @@ -292,9 +292,6 @@ Rechercher un contact… Recherche Échec de téléchargement des messages - Services Google Play absents - Il semble que les services Google Play ne sont pas installés sur votre appareil. Olvid s\'appuie sur ces services pour recevoir les notifications de messages entrants. Sans eux, vous ne recevrez vos nouveaux messages que quand Olvid est en avant-plan.\n\nVous pouvez à la place activer une connexion WebSocket permanente aux serveurs d\'Olvid qui pourra jouer le même rôle (mais risque de vider votre batterie plus vite). - Fonction d\'économie d\'énergie L\'économie d\'énergie sur Android fonctionne en retardant certains événements d\'arrière-plan pour les exécuter par lots. Cela peut retarder la réception de messages Olvid entrants.\n\nPour être certain de recevoir vos messages au plus vite, vous pouvez autoriser Olvid à «\u00a0toujours fonctionner en arrière-plan\u00a0». Appuyez sur «\u00a0Paramètres\u00a0» puis «\u00a0Autoriser\u00a0». Ne plus afficher ce message Paramètres @@ -504,7 +501,8 @@ Sonnerie Vibreur Notifications d\'appel entrant - Cet écran affiche vos appels émis et reçus. + Aucun appel pour le moment + Vos appels émis et reçus s\'afficheront ici. %1$s (durée %2$02dmin\u00a0%3$02dsec) Effacer le journal (durée %1$02dmin\u00a0%2$02dsec) @@ -1035,8 +1033,6 @@ Serveurs TURN de Singapour Autorisation d\'accés aux appareils bluetooth Si vous souhaitez utiliser des écouteurs bluetooth pour vos appels Olvid, vous devez autoriser Olvid à détecter et se connecter aux appareils à proximité. - Autorisation de définir des alarmes - Olvid utilise des alarmes pour programmer de façon précise le verrouillage de l\'écran et l\'effacement des messages éphémères.\nL\'autorisation de définir de telles alarmes a été révoquée, ce qui peut entrainer le dysfonctionnement de ces fonctions de sécurité.\n\nNous vous encourageons fortement à autoriser la définition d\'alarmes et rappels en appuyant le bouton «\u00a0paramètres d\'Olvid\u00a0» ci-dessous. %1$d contacts %1$d contacts du groupe %2$s Mise à jour requise @@ -1057,16 +1053,12 @@ Olvid a détecté une modification de la clé de signature cryptographique de votre fournisseur d\'identités. Cela ne devrait normalement jamais arriver.\n\nVeuillez contacter votre administrateur et n\'appuyer sur «\u00a0mettre à jour la clé\u00a0» que s\'il peut confirmer que cette modification est intentionnelle. Dans le doute, appuyez sur «\u00a0annuler\u00a0». Fournisseur d\'identités supprimé Il semble que votre compte a été supprimé du fournisseur d\'identités de votre société. Si vous avez quitté votre entreprise, ceci est normal et vous pouvez continuer à utiliser Olvid en tant qu\'utilisateur gratuit.\n\nSi vous pensez que c\'est une erreur, veuillez contacter votre administrateur pour re-enregistrer ce fournisseur d\'identités dans Olvid. - Montrez moi Paramétrer les sauvegardes Il est temps de configurer les sauvegardes\u00a0! - Pourquoi configurer les sauvegardes 🤔\u00a0? - Si vous veniez à égarer votre appareil ou à désinstaller Olvid par erreur, vous perdriez votre ID Olvid, l\'intégralité de vos contacts et tous vos groupes 😱. Fort heureusement, il est possible de les sauvegarder de façon sécurisée 😅. Appuyez sur «\u00a0paramétrer les sauvegardes\u00a0» pour commencer. + Si vous veniez à égarer votre appareil ou à désinstaller Olvid par erreur, vous perdriez votre ID Olvid, l\'intégralité de vos contacts et tous vos groupes 😱. Fort heureusement, il est possible de les sauvegarder de façon sécurisée 😅. Appuyez sur «\u00a0paramétrer les sauvegardes\u00a0» pour commencer. Il est temps de faire une sauvegarde\u00a0! Pour ne perdre aucun contact, nous vous recommandons d\'activer les sauvegardes automatiques vers Google Drive. Rassurez-vous, elles sont chiffrées 🤓\u00a0! Sinon, vous pouvez aussi effectuer des sauvegardes manuelles régulièrement. Appuyez sur «\u00a0paramétrer les sauvegardes\u00a0» pour commencer. - Cela fait plus d\'une semaine que vous n\'avez pas effectué de sauvegarde. C\'est probablement le bon moment pour en faire une nouvelle. Gardez à l\'esprit qu\'une vieille sauvegarde peut ne pas contenir tous vos contacts les plus récents. - Vous souvenez-vous de votre clé de sauvegarde\u00a0? - Avoir une sauvegarde à jour est essentiel, mais il vous faut votre clé de sauvegarde pour la restaurer\u00a0! Appuyez sur «\u00a0paramétrer les sauvegardes\u00a0» pour vérifier votre clé. Si vous avez perdu cette clé, pas d\'inquiétude, vous pourrez en générer une nouvelle. + Cela fait plus d\'une semaine que vous n\'avez pas effectué de sauvegarde. C\'est probablement le bon moment pour en faire une nouvelle. Gardez à l\'esprit qu\'une vieille sauvegarde peut ne pas contenir vos contacts les plus récents. Appuyez sur «\u00a0paramétrer les sauvegardes\u00a0» pour commencer. Ouvrir avec une application externe Vous êtes sur le point d\'ouvrir un fichier avec une application externe. Le contenu de ce fichier sera exposé à l\'extérieur d\'Olvid.\nSouhaitez-vous poursuivre\u00a0? Débloquer la discussion @@ -1221,8 +1213,8 @@ Réseau\u00a0: AUCUN Réseau\u00a0: CONNEXION… Réseau\u00a0: CONNECTÉ - ping\u00a0: %1$dms - ping\u00a0: >%1$ds + Ping\u00a0: %1$dms + Ping\u00a0: >%1$ds Copier le texte du message Copier texte et pièces jointes Message copié depuis Olvid @@ -1275,6 +1267,7 @@ Autres Tout Gestion du stockage + Dépannage Aucune pièce jointe à afficher Ordre de tri Trier par date (récents en premier) @@ -1543,11 +1536,6 @@ En attente Langue de l\'application Langue par défaut de l\'appareil - Notifications désactivées - Il semble que les notifications soient désactivées pour Olvid. Si vous ne les activez pas, vous ne serez pas notifié quand un message de vos contacts vous arrive.\n\nNous vous recommandons d\'«\u00a0ouvrir les paramètres\u00a0» et de réactiver les notifications. - Soyez notifié\u00a0! - Pour être notifié quand un contact vous envoie un message, vous devez autoriser Olvid à vous envoyer des notifications. - Passer Bloquer les connexions utilisant un certificat inconnu Jamais Toujours @@ -1676,8 +1664,6 @@ Changer de service de cartographie Groupe en lecture seule Quand cette option est activée, seuls les administrateurs peuvent poster des messages dans ce groupe - Notifications d\'appel - La réception d\'un appel peut nécessiter l\'affichage d\'une notification d\'appel entrant en plein écran. Olvid n\'a pas pour l\'instnt l\'autorisation d\'afficher une telle notification. Appuyez sur «\u00a0Paramètres d\'Olvid\u00a0» pour ouvrir l\'écran de paramètres correspondant. Ajouter un appareil Bien le bonjour ! Bienvenue parmi nous @@ -1758,6 +1744,46 @@ Séquestre de clé réussi Échec du séquestre de clé + Notifications + Pour être notifié quand un contact vous envoie un message, vous devez autoriser Olvid à afficher des notifications. + Notifications désactivées + Les notifications sont actuellement désactivées pour Olvid. Si vous ne les activez pas, vous ne serez pas notifié quand vous recevez un message de vos contacts. + Accès à l\'appareil photo + Donner accès à l\'appareil photo à Olvid vous permet d\'envoyer des photos prises directement depuis Olvid ou de scanner l\'ID d\'autres utilisateurs. + Accès à l\'appareil photo interdit + Enregistrement audio + Accorder à Olvid l\'accès au micro vous permet d\'enregistrer des messages vocaux et est nécessaire pour l\'émission et la réception d\'appels sécurisés. + Enregistrement audio désactivé + Fonction d\'économie d\'énergie + L\'économie d\'énergie sur Android fonctionne en retardant certains événements d\'arrière-plan pour les exécuter par lots. Cela peut retarder la réception de messages Olvid ou d\'appels.\n\nOlvid est actuellement autorisée à « toujours fonctionner en arrière-plan ». + L\'économie d\'énergie sur Android fonctionne en retardant certains événements d\'arrière-plan pour les exécuter par lots. Cela peut retarder la réception de messages Olvid ou d\'appels.\n\nPour être certain de recevoir vos messages au plus vite, vous devriez autoriser Olvid à «\u00a0toujours fonctionner en arrière-plan\u00a0». + Restriction d\'arrière plan + Olvid doit être autorisée à s\'exécuter en arrière-plan pour bien fonctionner. Activer les restrictions d\'arrière-plan empêcherait, par exemple, de recevoir les messages en arrière plan. + Les « restriction d\'arrière-plan » s\'appliquent actuellement à Olvid. L\'application ne pourra pas fonctionner normalement tant qu\'elle fait partie de la liste des « applications restreintes ».\n\nVous pouvez corriger cela en appuyant sur « Paramètres d\'Olvid » et en allant des la section utilisation de la batterie. + Autorisation de définir des alarmes + Olvid utilise des alarmes pour programmer de façon précise le verrouillage de l\'écran et l\'effacement des messages éphémères. Cette autorisation est réglée correctement. + Olvid utilise des alarmes pour programmer de façon précise le verrouillage de l\'écran et l\'effacement des messages éphémères.\nL\'autorisation de définir de telles alarmes a été révoquée, ce qui peut entrainer le dysfonctionnement de ces fonctions de sécurité.\n\nNous vous encourageons fortement à autoriser la définition d\'alarmes et rappels en appuyant sur «\u00a0paramètres d\'Olvid\u00a0». + Notifications d\'appel + La réception d\'un appel nécessite l\'affichage d\'une notification d\'appel entrant en plein écran. Cette autorisation est réglée correctement. + La réception d\'un appel nécessite l\'affichage d\'une notification d\'appel entrant en plein écran. Olvid n\'a pas pour l\'instnt l\'autorisation d\'afficher une telle notification. Appuyez sur «\u00a0Paramètres d\'Olvid\u00a0» pour autoriser les notifications en plein écran. + Services Google Play absents + Une connexion WebSocket permanente aux serveurs d\'Olvid est correctement configurée. + Les services Google Play ne sont pas disponibles sur votre appareil. Olvid s\'appuie sur ces services pour recevoir les notifications de messages entrants. Sans eux, vous ne recevrez vos nouveaux messages que quand Olvid est en avant-plan.\n\nVous pouvez à la place activer une connexion WebSocket permanente aux serveurs d\'Olvid qui pourra jouer le même rôle (mais risque de vider votre batterie légèrement plus vite). + Activer la WebSocket + Espace de stockage + %1$s d\'espace disponible sur votre appareil. + Espace de stockage presque plein + Seulement %1$s d\'espace libre sur votre appareil.\n\nVotre appareil n\'a presque plus d\'espace de stockage disponible. Attendez-vous à rencontrer des erreurs inattendues lors de l\'envoi ou de la réception de fichiers.\n\nCeci est probablement le bon moment pour libérer de l\'espace sur votre appareil. + **Version d\'Olvid** : %1$s (build %2$d)\n**Modèle d\'appareil** : %3$s\n**API Android** : %4$s\n**App démarrée depuis** : %5$s + Besoin d\'aide avec Olvid ? Consultez notre assistance : + https://olvid.io/faq + Demander l\'autorisation + Les sauvegardes Olvid sont correctement configurées. Assurez-vous que vous savez où est votre clé de sauvegarde et vérifiez que vous avez accès à une sauvegarde récente.\n\nIl est recommandé de vérifier votre clé de sauvegarde de temps en temps, juste pour être certain de savoir quoi faire si vous perdez votre appareil 😅. + Améliorez votre expérience Olvid ? + Allons-y + Ignoré + + %1$d groupe commun diff --git a/obv_messenger/app/src/main/res/values-night/bool.xml b/obv_messenger/app/src/main/res/values-night/bool.xml new file mode 100644 index 00000000..0a075e0e --- /dev/null +++ b/obv_messenger/app/src/main/res/values-night/bool.xml @@ -0,0 +1,4 @@ + + + false + \ No newline at end of file diff --git a/obv_messenger/app/src/main/res/values-night/colors.xml b/obv_messenger/app/src/main/res/values-night/colors.xml index c7db4e9a..bf5a9eea 100644 --- a/obv_messenger/app/src/main/res/values-night/colors.xml +++ b/obv_messenger/app/src/main/res/values-night/colors.xml @@ -34,6 +34,7 @@ #303030 + #171717 #99303030 #c3d9e6 \ No newline at end of file diff --git a/obv_messenger/app/src/main/res/values/bool.xml b/obv_messenger/app/src/main/res/values/bool.xml new file mode 100644 index 00000000..f799eca8 --- /dev/null +++ b/obv_messenger/app/src/main/res/values/bool.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/obv_messenger/app/src/main/res/values/colors.xml b/obv_messenger/app/src/main/res/values/colors.xml index 5703e848..2eeac27e 100644 --- a/obv_messenger/app/src/main/res/values/colors.xml +++ b/obv_messenger/app/src/main/res/values/colors.xml @@ -44,6 +44,7 @@ #ffffff + #ffffff #99ffffff #0C2643 #ffffff diff --git a/obv_messenger/app/src/main/res/values/strings.xml b/obv_messenger/app/src/main/res/values/strings.xml index c2761784..6898cef8 100644 --- a/obv_messenger/app/src/main/res/values/strings.xml +++ b/obv_messenger/app/src/main/res/values/strings.xml @@ -22,9 +22,9 @@ Smaller than 100MB Never download automatically Always download automatically - Abort + Abort Accept - App Settings + App settings Cancel Create Group Discard @@ -51,7 +51,6 @@ You are about to discard an invitation. Proceed? You are using Olvid for Android\nVersion %1$s (build %2$d)\n\nServer API: %3$d\nEngine DB schema: %4$d\nApplication DB schema: %5$d\nApp running for: %6$s You are using Olvid for Android\nVersion %1$s (build %2$d)\n\nServer API: %3$d\nEngine DB schema: %4$d\nApplication DB schema: %5$d\n\nExtra features: %6$s\nApp running for: %7$s - It appears that the Olvid app has Background Restriction enabled. Olvid will not be able to function properly until you remove it from the Restricted Apps list.\n\nYou can access this settings from:\nApp\u00a0Settings\u00a0> Advanced\u00a0> Battery\u00a0> Background\u00a0restriction Do you want to introduce %1$s and %2$s to one another? Do you want to introduce %1$s to the %2$d following contacts?\n\n%3$s Do you really want to delete attachment %1$s from this message?\nThis action cannot be undone. @@ -66,7 +65,6 @@ Do you really wish to restart the ongoing secure discussion channel establishment protocol? Abort Invitation? About Olvid - Olvid is Background Restricted Contact Introduction Delete Attachment Delete Discussion @@ -89,10 +87,12 @@ Share with… Create shortcut to… Please enter a name which will be displayed to your contacts. These details will never be sent to Olvid\'s servers. - This screen displays the list of contacts you trust.\n\nTo add new contacts, press the blue + at the bottom of this screen. - This screen displays your ongoing discussions. - This screen displays the list of groups you belong to. - This screen displays the list of invitations you have received or sent. + No contacts for now + Press the blue + below to add your first contact. + No discussions for now + Contact and group discussions will appear here. + No groups for now + Press \"New group\" to create your first group. The group manager updated the Group Card. Both the old and new versions are shown below.\n\nPress \"update\" to update the group\'s information. One of the group administrators updated the group details. The new version is shown above.\n\nPress \"update\" to use this new version. Your contact updated their Olvid Card. Both the old and new versions are shown below.\n\nPress \"update\" to update your contact\'s information with this new version. @@ -297,9 +297,6 @@ Search Contact Name… Search Failed to get messages - Google Play Services Unavailable - It seems the Google Play Services are not installed on your device. Olvid relies on these services to be notified of incoming messages. Without them, you will only receive new messages when Olvid is in the foreground.\n\nYou may instead enable a permanent WebSocket connection to Olvid\'s servers that can serve the same purpose (but may drain your battery). - Battery Optimization Battery optimization on Android works by delaying certain background events to run them in a batch. This may delay the reception of inbound Olvid messages.\n\nTo ensure that your messages are received promptly, you should allow Olvid to \"always run in background\". Press \"Settings\" below and then \"Allow\". Do not show this message again Settings @@ -510,7 +507,8 @@ Ringtone Vibrator Incoming call notifications - This screen displays the list of calls you made or received. + No calls for now + Calls you make or receive will appear here. %1$s (lasted %2$02dmin\u00a0%3$02dsec) (lasted %1$02dmin\u00a0%2$02dsec) (lasted %1$02dsec) @@ -1042,8 +1040,6 @@ Singapore TURN servers Request permission to access bluetooth devices If you want to use a bluetooth headset for Olvid calls, you need to allow Olvid to \"find and connect to\" your nearby devices. - Permission to set alarms - Olvid relies on alarms to precisely schedule screen lock and ephemeral messages deletion.\nThe permission to set such alarms has been revoked, which may cause these security features to misbehave.\n\nWe strongly encourage you to allow settings alarms and reminders by pressing the \"app settings\" button below. %1$d contacts %1$d contacts in group %2$s Update required @@ -1064,16 +1060,12 @@ Olvid detected a change in the cryptographic signature key of your identity provider. This should normally never happen.\n\nPlease contact your administrator and only press \"update key\" if he can confirm the key change was intentional. If unsure, press \"cancel\". Identity provider removed It seems that your account was removed from your company\'s identity provider. If you left your company, this is normal and you may continue using Olvid as a free user.\n\nIf you believe this is an error, please contact your administrator to re-register this identity provider with Olvid. - Show me Setup backups It\'s time to setup backups! - Why should you setup backups 🤔? - If you were to lose your device, or to uninstall Olvid by mistake, you would lose your Olvid ID, all your contacts, and all your groups 😱. Luckily for you, it is possible to setup secure backups 😅.\n\nPress \"setup backups\" to begin. + If you were to lose your device, or to uninstall Olvid by mistake, you would lose your Olvid ID, all your contacts, and all your groups 😱. Luckily for you, it is possible to setup secure backups 😅.\n\nPress \"Setup backups\" to begin. It\'s backup time! - In order not to lose any contact, we recommend you activate automatic backups to Google Drive. Don\'t worry, these backups are encrypted 🤓!\nOtherwise, you may also perform manual backups on a regular basis.\n\nPress \"setup backups\" to begin. - It\'s been more than a week since your last backup. Now is probably a good time for a new one. Keep in mind that an old backup might not contain all your latest contacts.\n\nPress \"setup backups\" to begin. - Do you remember your backup key? - Having an up to date Olvid backup is essential, but you need your backup key to restore it!\n\nPress \"setup backups\" to verify your key. If you lost it, don\'t worry, you can generate a new one. + In order not to lose any contact, we recommend you activate automatic backups to Google Drive. Don\'t worry, these backups are encrypted 🤓!\nOtherwise, you may also perform manual backups on a regular basis.\n\nPress \"Setup backups\" to begin. + It\'s been more than a week since your last backup. Now is probably a good time for a new one. Keep in mind that an old backup might not contain your latest contacts.\n\nPress \"Setup backups\" to begin. Keycloak configuration Configuration link provided in the keycloak Olvid Management Console. Disable new version notification @@ -1240,8 +1232,8 @@ Connectivity: NONE Connectivity: CONNECTING… Connectivity: CONNECTED - ping: %1$dms - ping: >%1$ds + Ping: %1$dms + Ping: >%1$ds Copy message text Copy message and attachments Message copied from Olvid @@ -1295,6 +1287,7 @@ Other All Storage management + Troubleshooting No attachment to display Sort order Sort by date (most recent first) @@ -1568,11 +1561,6 @@ Default device language English Français - Notifications disabled - It appears that notifications are currently disabled for Olvid. Unless you enable them, you will not be notified when receiving a message from your contacts.\n\nWe recommend you tap \"open settings\" and toggle notifications back on. - Get notified! - To be notified when a contact sends you a message, you need to allow Olvid to send notifications. - Skip Block connections using an untrusted certificate Never Always @@ -1706,8 +1694,6 @@ Change map provider Read only group When activated, only group admins can post messages in this group - Incoming call notifications - Incoming calls sometimes need to display a full screen \"answer call\" notification. Olvid currently does not have permission to open such full screen notifications. Press \"App Settings\" to open the relevant setting screen. Add a device Welcome! Welcome to Olvid @@ -1788,6 +1774,47 @@ Key escrow successful Key escrow failed + Notifactions + To be notified when a contact sends you a message, you need to allow Olvid to display notifications. + Notifications disabled + Notifications are currently disabled for Olvid. Unless you enable them, you will not be notified when receiving a message from your contacts. + Camera access + Granting Olvid access to the camera allows you to send pictures taken directly from within Olvid, or to scan the ID of other users. + Camera access denied + Audio recording + Granting Olvid access to the microphone allows you to record voice messages and is required to start or receive secure calls. + Audio recording disabled + Battery optimization + Battery optimization on Android works by delaying certain background events to run them in a batch. This may delay the reception of inbound Olvid messages and calls.\n\nOlvid is currently allowed to \"always run in background\". + Battery optimization on Android works by delaying certain background events to run them in a batch. This may delay the reception of inbound Olvid messages and calls.\n\nTo ensure that your messages are received promptly, you should allow Olvid to \"always run in background\". + Background restriction + Olvid needs to be allowed to run in background to function properly. Enabling background restriction would, for example, prevent Olvid from receiving messages in the background. + Olvid has \"background restriction\" enabled. Olvid will not be able to function properly until you remove it from the \"restricted apps\" list.\n\nYou can fix this by pressing \"App settings\" and heading to the battery usage section. + Permission to set alarms + Olvid relies on alarms to precisely schedule screen lock and ephemeral messages deletion. This permission is set correctly. + Olvid relies on alarms to precisely schedule screen lock and ephemeral messages deletion.\nThe permission to set such alarms has been revoked, which may cause these security features to misbehave.\n\nWe strongly encourage you to allow setting alarms and reminders by pressing \"App settings\". + Incoming call notifications + Incoming calls need to display a full screen \"answer call\" notification. This permission is set correctly. + Incoming calls need to display a full screen \"answer call\" notification. Olvid currently does not have permission to open such full screen notifications. Press \"App settings\" to allow full scrren notifications. + Google Play Services unavailable + Permanent WebSocket connection to Olvid\'s servers is properly configured. + Google Play Services are not available on your device. Olvid relies on these services to be notified of incoming messages. Without them, you will only receive new messages when Olvid is in the foreground.\n\nYou may instead enable a permanent WebSocket connection to Olvid\'s servers that can serve the same purpose (but may slightly drain your battery). + Enable the WebSocket + Device storage + %1$s storage space available on your device. + Device storage almost full + Only %1$s storage space left on your device.\n\nYour device is running low on available space. Expect to run into unexpected errors when receiving or sending files with Olvid.\n\nThis is probably the right time to free up some space on your device. + **App version**: %1$s (build %2$d)\n**Device model**: %3$s\n**Android API**: %4$s\n**App running for**: %5$s + Need help with Olvid? Please read our FAQ: + https://olvid.io/faq + Request permission + Olvid backups are properly setup. Please make sure you know where your backup key is and check that you have access to up to date backup files.\n\nIt is a good idea to verify your backup key from time to time, just to be sure you\'ll know what to do if you lose your device 😅. + Want to improve your Olvid experience? + Let\'s go + Ignored + + + 1 common group %1$d common groups diff --git a/obv_messenger/app/src/main/res/values/styles.xml b/obv_messenger/app/src/main/res/values/styles.xml index d23aa981..8b9d2359 100644 --- a/obv_messenger/app/src/main/res/values/styles.xml +++ b/obv_messenger/app/src/main/res/values/styles.xml @@ -33,6 +33,12 @@ @color/olvid_gradient_light + +