From 11d76ef96fa5fcd839b8e1cc701142966824897e Mon Sep 17 00:00:00 2001 From: Yuichiro Kinoshita Date: Tue, 9 Jan 2024 15:20:31 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=A6=E3=83=BC=E3=82=B6=E3=81=AE?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E3=81=AB=E9=96=A2=E3=81=99=E3=82=8B=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=82=92=E3=82=A2=E3=83=97=E3=83=AA=E5=85=A8?= =?UTF-8?q?=E4=BD=93=E3=81=A7=E5=87=A6=E7=90=86=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...er.kt => UserActionAppGlobalErrorStore.kt} | 6 +- .../error/UserActionAppGlobalErrorListener.kt | 110 ++++++++++++++++++ modules/features/drive/build.gradle | 1 + .../milktea/drive/DriveActivity.kt | 8 ++ .../milktea/drive/viewmodel/DriveViewModel.kt | 44 +++++++ 5 files changed, 167 insertions(+), 2 deletions(-) rename modules/app_store/src/main/java/net/pantasystem/milktea/app_store/handler/{UserActionAppGlobalErrorHandler.kt => UserActionAppGlobalErrorStore.kt} (93%) create mode 100644 modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/error/UserActionAppGlobalErrorListener.kt diff --git a/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/handler/UserActionAppGlobalErrorHandler.kt b/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/handler/UserActionAppGlobalErrorStore.kt similarity index 93% rename from modules/app_store/src/main/java/net/pantasystem/milktea/app_store/handler/UserActionAppGlobalErrorHandler.kt rename to modules/app_store/src/main/java/net/pantasystem/milktea/app_store/handler/UserActionAppGlobalErrorStore.kt index f0ce4d8213..6a52a4dc32 100644 --- a/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/handler/UserActionAppGlobalErrorHandler.kt +++ b/modules/app_store/src/main/java/net/pantasystem/milktea/app_store/handler/UserActionAppGlobalErrorStore.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import net.pantasystem.milktea.common_android.resource.StringSource import javax.inject.Inject +import javax.inject.Singleton /** * アプリ全体で共通としてユーザの操作によって発生したエラーをハンドリングするクラスです。 @@ -13,9 +14,10 @@ import javax.inject.Inject * 複数のレイヤーから呼び出してしまうと、重複して同じエラーを報告してしまう可能性が考えられるため、ViewModelから呼び出すことを前提としています。 * また古い実装の場合このクラスを経由しないケースがあるため、このクラスに必ずエラーが報告されないケースもあるため、エラーの発生元を確認するようにしてください。 */ -class UserActionAppGlobalErrorHandler @Inject constructor() { +@Singleton +class UserActionAppGlobalErrorStore @Inject constructor() { private val _errorFlow = MutableSharedFlow( - extraBufferCapacity = 100, + extraBufferCapacity = 999, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) val errorFlow = _errorFlow.asSharedFlow() diff --git a/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/error/UserActionAppGlobalErrorListener.kt b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/error/UserActionAppGlobalErrorListener.kt new file mode 100644 index 0000000000..ebc366e3f0 --- /dev/null +++ b/modules/common_android_ui/src/main/java/net/pantasystem/milktea/common_android_ui/error/UserActionAppGlobalErrorListener.kt @@ -0,0 +1,110 @@ +package net.pantasystem.milktea.common_android_ui.error + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope +import androidx.lifecycle.flowWithLifecycle +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import net.pantasystem.milktea.app_store.handler.AppGlobalError +import net.pantasystem.milktea.app_store.handler.UserActionAppGlobalErrorStore +import net.pantasystem.milktea.common.APIError +import net.pantasystem.milktea.common_android.resource.getString +import net.pantasystem.milktea.common_android_ui.APIErrorStringConverter +import net.pantasystem.milktea.common_android_ui.R +import net.pantasystem.milktea.model.account.UnauthorizedException +import java.io.IOException +import javax.inject.Inject + +class UserActionAppGlobalErrorListener @Inject constructor( + private val userActionAppGlobalErrorStore: UserActionAppGlobalErrorStore, + @ApplicationContext private val context: Context, +) { + + operator fun invoke( + lifecycle: Lifecycle, + fragmentManager: FragmentManager, + ) { + userActionAppGlobalErrorStore.errorFlow.onEach { appGlobalError -> + when (appGlobalError.level) { + AppGlobalError.ErrorLevel.Info -> {} + AppGlobalError.ErrorLevel.Warning -> { + // toastで表示する + Toast.makeText( + context, + appGlobalError.message.getString(context), + Toast.LENGTH_SHORT + ) + .show() + } + + AppGlobalError.ErrorLevel.Error -> { + val title = when (val error = appGlobalError.throwable) { + is IOException -> { + context.getString(R.string.network_error) + } + + is APIError -> { + APIErrorStringConverter()(error).getString(context) + } + + is UnauthorizedException -> { + context.getString(R.string.unauthorized_error) + } + + else -> "Unknown error" + + } + val dialogInstance = UserActionAppGlobalErrorDialog.newInstance( + title = title, + message = appGlobalError.message.getString(context) + ) + fragmentManager.findFragmentByTag("error_dialog")?.let { + fragmentManager.beginTransaction().remove(it).commit() + } + dialogInstance.show(fragmentManager, "error_dialog") + } + } + }.flowWithLifecycle( + lifecycle, + Lifecycle.State.RESUMED, + ).launchIn(lifecycle.coroutineScope) + } +} + +class UserActionAppGlobalErrorDialog : DialogFragment() { + companion object { + const val EXTRA_TITLE = "title" + const val EXTRA_MESSAGE = "message" + + fun newInstance(title: String, message: String): UserActionAppGlobalErrorDialog { + return UserActionAppGlobalErrorDialog().apply { + arguments = Bundle().apply { + putString(EXTRA_TITLE, title) + putString(EXTRA_MESSAGE, message) + } + } + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext()) + .setTitle( + arguments?.getString(EXTRA_TITLE) ?: "" + ) + .setMessage( + arguments?.getString(EXTRA_MESSAGE) ?: "" + ) + .setPositiveButton(android.R.string.ok) { _, _ -> + dismiss() + } + .create() + } +} \ No newline at end of file diff --git a/modules/features/drive/build.gradle b/modules/features/drive/build.gradle index b4edaa44f0..ab05597b2c 100644 --- a/modules/features/drive/build.gradle +++ b/modules/features/drive/build.gradle @@ -60,6 +60,7 @@ dependencies { implementation project(path: ':modules:common_compose') implementation project(path: ':modules:common_navigation') implementation project(path: ':modules:common_android') + implementation project(path: ':modules:common_android_ui') testImplementation libs.junit androidTestImplementation libs.androidx.test.ext.junit androidTestImplementation libs.androidx.test.espresso.core diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveActivity.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveActivity.kt index 4be43a659e..c54102b11b 100644 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveActivity.kt +++ b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/DriveActivity.kt @@ -17,6 +17,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.ExperimentalCoroutinesApi import net.pantasystem.milktea.common.ui.ApplyTheme import net.pantasystem.milktea.common_android.platform.PermissionUtil +import net.pantasystem.milktea.common_android_ui.error.UserActionAppGlobalErrorListener import net.pantasystem.milktea.common_compose.MilkteaStyleConfigApplyAndTheme import net.pantasystem.milktea.common_navigation.DriveNavigation import net.pantasystem.milktea.common_navigation.DriveNavigationArgs @@ -53,6 +54,9 @@ class DriveActivity : AppCompatActivity() { @Inject internal lateinit var configRepository: LocalConfigRepository + @Inject + internal lateinit var userActionAPpGlobalErrorListener: UserActionAppGlobalErrorListener + @OptIn( ExperimentalPagerApi::class, ExperimentalMaterialApi::class, @@ -62,6 +66,10 @@ class DriveActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setTheme.invoke() + userActionAPpGlobalErrorListener( + lifecycle = lifecycle, + fragmentManager = supportFragmentManager + ) setContent { diff --git a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DriveViewModel.kt b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DriveViewModel.kt index 75af296d28..f8d72111a4 100644 --- a/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DriveViewModel.kt +++ b/modules/features/drive/src/main/java/net/pantasystem/milktea/drive/viewmodel/DriveViewModel.kt @@ -20,11 +20,14 @@ import kotlinx.coroutines.launch import net.pantasystem.milktea.app_store.account.AccountStore import net.pantasystem.milktea.app_store.drive.DriveDirectoryPagingStore import net.pantasystem.milktea.app_store.drive.FilePropertyPagingStore +import net.pantasystem.milktea.app_store.handler.AppGlobalError +import net.pantasystem.milktea.app_store.handler.UserActionAppGlobalErrorStore import net.pantasystem.milktea.common.Logger import net.pantasystem.milktea.common.PageableState import net.pantasystem.milktea.common.convert import net.pantasystem.milktea.common.mapCancellableCatching import net.pantasystem.milktea.common.runCancellableCatching +import net.pantasystem.milktea.common_android.resource.StringSource import net.pantasystem.milktea.common_navigation.EXTRA_ACCOUNT_ID import net.pantasystem.milktea.common_navigation.EXTRA_INT_SELECTABLE_FILE_MAX_SIZE import net.pantasystem.milktea.model.account.Account @@ -53,6 +56,7 @@ class DriveViewModel @Inject constructor( private val filePagingStore: FilePropertyPagingStore, private val filePropertyRepository: DriveFileRepository, private val uriToAppFileUseCase: UriToAppFileUseCase, + private val userActionAppGlobalErrorHandler: UserActionAppGlobalErrorStore, loggerFactory: Logger.Factory, buildDriveUiState: DriveUiStateBuilder, ) : ViewModel() { @@ -242,6 +246,14 @@ class DriveViewModel @Inject constructor( filePropertyRepository.toggleNsfw(id) } catch (e: Exception) { logger.info("nsfwの更新に失敗しました", e = e) + userActionAppGlobalErrorHandler.dispatch( + AppGlobalError( + "DriveViewModel.toggleNsfw", + AppGlobalError.ErrorLevel.Error, + StringSource("Nsfw update failed"), + e, + ) + ) } } } @@ -251,6 +263,14 @@ class DriveViewModel @Inject constructor( viewModelScope.launch { filePropertyRepository.delete(id).onFailure { e -> logger.info("ファイルの削除に失敗しました", e = e) + userActionAppGlobalErrorHandler.dispatch( + AppGlobalError( + "DriveViewModel.deleteFile", + AppGlobalError.ErrorLevel.Error, + StringSource("File delete failed"), + e, + ) + ) } } } @@ -263,6 +283,14 @@ class DriveViewModel @Inject constructor( .update(comment = newCaption) ).onFailure { logger.info("キャプションの更新に失敗しました。", e = it) + userActionAppGlobalErrorHandler.dispatch( + AppGlobalError( + "DriveViewModel.updateCaption", + AppGlobalError.ErrorLevel.Error, + StringSource("Caption update failed"), + it, + ) + ) } } } @@ -274,6 +302,14 @@ class DriveViewModel @Inject constructor( .update(name = name) ).onFailure { logger.error("update file name failed", it) + userActionAppGlobalErrorHandler.dispatch( + AppGlobalError( + "DriveViewModel.updateFileName", + AppGlobalError.ErrorLevel.Error, + StringSource("File name update failed"), + it, + ) + ) } } } @@ -318,6 +354,14 @@ class DriveViewModel @Inject constructor( filePagingStore.onCreated(e.id) } catch (e: Exception) { logger.info("ファイルアップロードに失敗した") + userActionAppGlobalErrorHandler.dispatch( + AppGlobalError( + "DriveViewModel.uploadFile", + AppGlobalError.ErrorLevel.Error, + StringSource("File upload failed"), + e, + ) + ) } } }