diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt index 08cc2f469..834c575ee 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -17,6 +17,7 @@ import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.participants.AddParticipantOverall import com.nextcloud.talk.models.json.participants.TalkBan import com.nextcloud.talk.models.json.participants.TalkBanOverall +import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall import okhttp3.MultipartBody import okhttp3.RequestBody @@ -238,6 +239,12 @@ interface NcApiCoroutines { @Url url: String ): UserAbsenceOverall + @POST + suspend fun testPushNotifications( + @Header("Authorization") authorization: String, + @Url url: String + ): TestNotificationOverall + @GET suspend fun getContextOfChatMessage( @Header("Authorization") authorization: String, diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt index a5626faf2..a514d5bbc 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -16,6 +16,7 @@ import com.nextcloud.talk.conversationcreation.ConversationCreationViewModel import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel +import com.nextcloud.talk.diagnose.DiagnoseViewModel import com.nextcloud.talk.invitation.viewmodels.InvitationsViewModel import com.nextcloud.talk.messagesearch.MessageSearchViewModel import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel @@ -148,4 +149,9 @@ abstract class ViewModelModule { @IntoMap @ViewModelKey(ConversationCreationViewModel::class) abstract fun conversationCreationViewModel(viewModel: ConversationCreationViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(DiagnoseViewModel::class) + abstract fun diagnoseViewModel(viewModel: DiagnoseViewModel): ViewModel } diff --git a/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseActivity.kt b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseActivity.kt index 191507ac1..c2dcf88fa 100644 --- a/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseActivity.kt @@ -23,10 +23,12 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource +import androidx.lifecycle.ViewModelProvider import androidx.core.net.toUri import autodagger.AutoInjector import com.nextcloud.talk.BuildConfig @@ -35,8 +37,8 @@ import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager -import com.nextcloud.talk.components.SetupSystemBars import com.nextcloud.talk.components.StandardAppBar +import com.nextcloud.talk.components.SetupSystemBars import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.BrandingUtils import com.nextcloud.talk.utils.ClosedInterfaceImpl @@ -56,6 +58,9 @@ class DiagnoseActivity : BaseActivity() { @Inject lateinit var arbitraryStorageManager: ArbitraryStorageManager + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + @Inject lateinit var ncApi: NcApi @@ -78,8 +83,13 @@ class DiagnoseActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + val diagnoseViewModel = ViewModelProvider( + this, + viewModelFactory + )[DiagnoseViewModel::class.java] val colorScheme = viewThemeUtils.getColorScheme(this) + isGooglePlayServicesAvailable = ClosedInterfaceImpl().isGooglePlayServicesAvailable setContent { val backgroundColor = colorResource(id = R.color.bg_default) @@ -107,13 +117,22 @@ class DiagnoseActivity : BaseActivity() { ) }, content = { + val viewState = diagnoseViewModel.notificationViewState.collectAsState().value Column( Modifier .padding(it) .background(backgroundColor) .fillMaxSize() ) { - DiagnoseContentComposable(diagnoseDataState) + DiagnoseContentComposable( + diagnoseDataState, + isLoading = diagnoseViewModel.isLoading.value, + showDialog = diagnoseViewModel.showDialog.value, + viewState = viewState, + onTestPushClick = { diagnoseViewModel.fetchTestPushResult() }, + onDismissDialog = { diagnoseViewModel.dismissDialog() }, + isGooglePlayServicesAvailable = isGooglePlayServicesAvailable + ) } } ) @@ -126,8 +145,6 @@ class DiagnoseActivity : BaseActivity() { super.onResume() supportActionBar?.show() - isGooglePlayServicesAvailable = ClosedInterfaceImpl().isGooglePlayServicesAvailable - diagnoseData.clear() setupMetaValues() setupPhoneValues() diff --git a/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseContentComposable.kt b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseContentComposable.kt index 7a3737af5..f6cfba19e 100644 --- a/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseContentComposable.kt +++ b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseContentComposable.kt @@ -7,28 +7,61 @@ package com.nextcloud.talk.diagnose +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Build +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer 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.width +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource +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.window.Dialog +import androidx.compose.ui.window.DialogProperties import com.nextcloud.talk.R +@Suppress("LongParameterList") @Composable -fun DiagnoseContentComposable(data: State>) { +fun DiagnoseContentComposable( + data: State>, + isLoading: Boolean, + showDialog: Boolean, + viewState: NotificationUiState, + onTestPushClick: () -> Unit, + onDismissDialog: () -> Unit, + isGooglePlayServicesAvailable: Boolean +) { + val context = LocalContext.current Column( modifier = Modifier .fillMaxSize() @@ -37,13 +70,17 @@ fun DiagnoseContentComposable(data: State ) { data.value.forEach { element -> when (element) { - is DiagnoseActivity.DiagnoseElement.DiagnoseHeadline -> Text( - modifier = Modifier.padding(vertical = 16.dp), - text = element.headline, - color = MaterialTheme.colorScheme.primary, - fontSize = LocalDensity.current.run { dimensionResource(R.dimen.headline_text_size).toPx().toSp() }, - fontWeight = FontWeight.Bold - ) + is DiagnoseActivity.DiagnoseElement.DiagnoseHeadline -> { + Text( + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + text = element.headline, + color = MaterialTheme.colorScheme.primary, + fontSize = LocalDensity.current.run { + dimensionResource(R.dimen.headline_text_size).toPx().toSp() + }, + fontWeight = FontWeight.Bold + ) + } is DiagnoseActivity.DiagnoseElement.DiagnoseEntry -> { Text( @@ -59,6 +96,143 @@ fun DiagnoseContentComposable(data: State } } } + if (isGooglePlayServicesAvailable) { + ShowTestPushButton(onTestPushClick) + } + ShowNotificationData(isLoading, showDialog, context, viewState, onDismissDialog) + } +} + +@Composable +fun ShowTestPushButton(onTestPushClick: () -> Unit) { + Button( + modifier = Modifier + .wrapContentSize() + .padding(vertical = 8.dp), + onClick = { + onTestPushClick() + } + ) { + Text( + text = stringResource(R.string.nc_test_push_button), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + fontSize = LocalDensity.current.run { + dimensionResource(R.dimen.headline_text_size).toPx().toSp() + }, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } +} + +@Composable +fun ShowOptions(onDismissDialog: () -> Unit, message: String, context: Context) { + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier.fillMaxWidth() + ) { + TextButton(onClick = { onDismissDialog() }) { + Text(text = stringResource(R.string.nc_cancel)) + } + Spacer(modifier = Modifier.width(8.dp)) + TextButton(onClick = { + val clipboard = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Push Message", message) + clipboard.setPrimaryClip(clip) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(context, R.string.message_copied, Toast.LENGTH_SHORT).show() + } + onDismissDialog() + }) { + Text(text = stringResource(R.string.nc_common_copy)) + } + } +} + +@Composable +fun LoadingIndicator() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} + +@Composable +fun ShowNotificationData( + isLoading: Boolean, + showDialog: Boolean, + context: Context, + viewState: NotificationUiState, + onDismissDialog: () -> Unit +) { + val message = getMessage(context, viewState) + + if (isLoading) { + LoadingIndicator() + } + if (showDialog) { + Dialog( + onDismissRequest = { onDismissDialog() }, + properties = DialogProperties( + dismissOnClickOutside = true, + usePlatformDefaultWidth = false + ) + ) { + Surface( + shape = MaterialTheme.shapes.medium, + tonalElevation = 8.dp, + modifier = Modifier + .wrapContentSize() + .padding(16.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(R.string.nc_test_results), + style = MaterialTheme.typography + .titleMedium + ) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = false) + .verticalScroll(rememberScrollState()) + ) { + Column(modifier = Modifier.padding(top = 12.dp)) { + if (viewState is NotificationUiState.Success) { + Text( + text = stringResource(R.string.nc_push_notification_message), + color = colorResource(R.color.colorPrimary) + ) + } + Text( + modifier = Modifier.padding(top = 12.dp), + text = message + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + ShowOptions(onDismissDialog, message, context) + } + } + } + } +} + +fun getMessage(context: Context, viewState: NotificationUiState): String { + return when (viewState) { + is NotificationUiState.Success -> + viewState.testNotification ?: context.getString(R.string.nc_push_notification_fetch_error) + + is NotificationUiState.Error -> + context.getString(R.string.nc_push_notification_error, viewState.message) + + else -> + context.getString(R.string.nc_common_error_sorry) } } @@ -73,5 +247,13 @@ fun DiagnoseContentPreview() { ) ) } - DiagnoseContentComposable(state) + DiagnoseContentComposable( + state, + false, + true, + NotificationUiState.Success("Test notification successful"), + {}, + {}, + true + ) } diff --git a/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseViewModel.kt b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseViewModel.kt new file mode 100644 index 000000000..6fca3b8da --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseViewModel.kt @@ -0,0 +1,69 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.diagnose + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("TooGenericExceptionCaught") +class DiagnoseViewModel @Inject constructor( + private val ncApiCoroutines: NcApiCoroutines, + private val currentUserProvider: CurrentUserProviderNew +) : ViewModel() { + private val _currentUser = currentUserProvider.currentUser.blockingGet() + val currentUser: User = _currentUser + val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: "" + + private val _notificationViewState = MutableStateFlow(NotificationUiState.None) + val notificationViewState: StateFlow = _notificationViewState + + private val _isLoading = mutableStateOf(false) + val isLoading = _isLoading + + private val _showDialog = mutableStateOf(false) + val showDialog = _showDialog + + fun fetchTestPushResult() { + viewModelScope.launch { + try { + _isLoading.value = true + val response = ncApiCoroutines.testPushNotifications( + credentials, + ApiUtils + .getUrlForTestPushNotifications(_currentUser.baseUrl ?: "") + ) + val notificationMessage = response.ocs?.data?.message + _notificationViewState.value = NotificationUiState.Success(notificationMessage) + } catch (e: Exception) { + _notificationViewState.value = NotificationUiState.Error(e.message ?: "") + } finally { + _isLoading.value = false + _showDialog.value = true + } + } + } + + fun dismissDialog() { + _showDialog.value = false + } +} + +sealed class NotificationUiState { + data object None : NotificationUiState() + data class Success(val testNotification: String?) : NotificationUiState() + data class Error(val message: String) : NotificationUiState() +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationData.kt b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationData.kt new file mode 100644 index 000000000..451b9a2af --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationData.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.testNotification + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class TestNotificationData( + @JsonField(name = ["message"]) + var message: String +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : + this("") +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOCS.kt new file mode 100644 index 000000000..b43fe1f52 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.testNotification + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class TestNotificationOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: TestNotificationData? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOverall.kt new file mode 100644 index 000000000..140d4b940 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.testNotification + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class TestNotificationOverall( + @JsonField(name = ["ocs"]) + var ocs: TestNotificationOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt index 8dc117490..3e0bfd173 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt @@ -273,6 +273,10 @@ object ApiUtils { return getUrlForApi(version, baseUrl) + "/signaling" } + fun getUrlForTestPushNotifications(baseUrl: String): String { + return "$baseUrl$OCS_API_VERSION/apps/notifications/api/v3/test/self" + } + @JvmStatic fun getUrlForSignalingBackend(version: Int, baseUrl: String?): String { return getUrlForSignaling(version, baseUrl) + "/backend" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4742ce754..80c3a9e01 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -67,6 +67,8 @@ How to translate with transifex: Display name couldn\'t be fetched, aborting %1$s not available (not installed or restricted by admin) Could not store display name, aborting + Sorry something went wrong, error is %1$s + Sorry something went wrong, cannot fetch test push message Search @@ -228,6 +230,11 @@ How to translate with transifex: Send email Create issue Build flavor + "Test push notifications + Test results + Message copied + Push notification is sent successfully. You should now receive + a notification on this device with the title \'Testing push notifications\' Leave conversation