diff --git a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt index 8cb4d9c6..4bde0e7b 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt @@ -23,12 +23,14 @@ import ua.acclorite.book_story.data.repository.ColorPresetRepositoryImpl import ua.acclorite.book_story.data.repository.DataStoreRepositoryImpl import ua.acclorite.book_story.data.repository.FileSystemRepositoryImpl import ua.acclorite.book_story.data.repository.HistoryRepositoryImpl +import ua.acclorite.book_story.data.repository.PermissionRepositoryImpl import ua.acclorite.book_story.data.repository.RemoteRepositoryImpl import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.ColorPresetRepository import ua.acclorite.book_story.domain.repository.DataStoreRepository import ua.acclorite.book_story.domain.repository.FileSystemRepository import ua.acclorite.book_story.domain.repository.HistoryRepository +import ua.acclorite.book_story.domain.repository.PermissionRepository import ua.acclorite.book_story.domain.repository.RemoteRepository import javax.inject.Singleton @@ -77,6 +79,12 @@ abstract class RepositoryModule { remoteRepositoryImpl: RemoteRepositoryImpl ): RemoteRepository + @Binds + @Singleton + abstract fun bindPermissionRepository( + permissionRepositoryImpl: PermissionRepositoryImpl + ): PermissionRepository + @Binds @Singleton abstract fun bindBookMapper( diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/PermissionRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/PermissionRepositoryImpl.kt new file mode 100644 index 00000000..c0d9fef7 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/PermissionRepositoryImpl.kt @@ -0,0 +1,148 @@ +package ua.acclorite.book_story.data.repository + +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import android.util.Log +import androidx.activity.ComponentActivity +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.shouldShowRationale +import kotlinx.coroutines.delay +import kotlinx.coroutines.yield +import ua.acclorite.book_story.domain.repository.PermissionRepository +import ua.acclorite.book_story.presentation.core.util.launchActivity +import javax.inject.Inject +import javax.inject.Singleton + +private const val STORAGE_PERMISSION = "STORAGE PERM, REPO" +private const val NOTIFICATIONS_PERMISSION = "NOTIFIC PERM, REPO" + +@Singleton +class PermissionRepositoryImpl @Inject constructor() : PermissionRepository { + + @SuppressLint("InlinedApi") + @OptIn(ExperimentalPermissionsApi::class) + override suspend fun grantStoragePermission( + activity: ComponentActivity, + storagePermissionState: PermissionState + ): Boolean { + Log.i(STORAGE_PERMISSION, "Requested storage permission") + val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R + + val permissionGranted = if (legacyStoragePermission) { + storagePermissionState.status.isGranted + } else Environment.isExternalStorageManager() + + if (permissionGranted) { + Log.i(STORAGE_PERMISSION, "Granted: Storage Permission is already granted") + return true + } + + when (legacyStoragePermission) { + true -> { + if (!storagePermissionState.status.shouldShowRationale) { + storagePermissionState.launchPermissionRequest() + } else { + val uri = Uri.parse("package:${activity.packageName}") + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri) + + intent.launchActivity(activity) { + Log.e( + STORAGE_PERMISSION, + "Could not launch \"ACTION_APPLICATION_DETAILS_SETTINGS\" activity" + ) + return false + } + } + } + + false -> { + val uri = Uri.parse("package:${activity.packageName}") + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, uri) + + intent.launchActivity(activity) { + Log.e( + STORAGE_PERMISSION, + "Could not launch \"ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION\" activity" + ) + return false + } + } + } + + for (i in 1..30) { + val granted = if (legacyStoragePermission) { + storagePermissionState.status.isGranted + } else Environment.isExternalStorageManager() + + if (!granted) { + delay(1000) + yield() + continue + } + + yield() + + Log.i(STORAGE_PERMISSION, "Successfully granted") + return true + break + } + + Log.e(STORAGE_PERMISSION, "Not granted: Timeout") + return false + } + + @OptIn(ExperimentalPermissionsApi::class) + override suspend fun grantNotificationsPermission( + activity: ComponentActivity, + notificationsPermissionState: PermissionState + ): Boolean { + Log.i(NOTIFICATIONS_PERMISSION, "Requested notifications permission") + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Log.w(NOTIFICATIONS_PERMISSION, "Granted: API is below 33") + return true + } + + if (notificationsPermissionState.status.isGranted) { + Log.i(NOTIFICATIONS_PERMISSION, "Granted: Notifications Permission is already granted") + return true + } + + if (!notificationsPermissionState.status.shouldShowRationale) { + notificationsPermissionState.launchPermissionRequest() + } else { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName) + + intent.launchActivity(activity) { + Log.e( + NOTIFICATIONS_PERMISSION, + "Could not launch \"APP_NOTIFICATION_SETTINGS\" activity" + ) + return false + } + } + + for (i in 1..20) { + if (!notificationsPermissionState.status.isGranted) { + delay(1000) + yield() + continue + } + + yield() + + Log.i(NOTIFICATIONS_PERMISSION, "Successfully granted") + return true + break + } + + Log.e(NOTIFICATIONS_PERMISSION, "Not granted: Timeout") + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/PermissionRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/PermissionRepository.kt new file mode 100644 index 00000000..c5c36198 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/PermissionRepository.kt @@ -0,0 +1,20 @@ +package ua.acclorite.book_story.domain.repository + +import androidx.activity.ComponentActivity +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState + +interface PermissionRepository { + + @OptIn(ExperimentalPermissionsApi::class) + suspend fun grantStoragePermission( + activity: ComponentActivity, + storagePermissionState: PermissionState + ): Boolean + + @OptIn(ExperimentalPermissionsApi::class) + suspend fun grantNotificationsPermission( + activity: ComponentActivity, + notificationsPermissionState: PermissionState + ): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantNotificationsPermission.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantNotificationsPermission.kt new file mode 100644 index 00000000..846f7a98 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantNotificationsPermission.kt @@ -0,0 +1,23 @@ +package ua.acclorite.book_story.domain.use_case.permission + +import androidx.activity.ComponentActivity +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import ua.acclorite.book_story.domain.repository.PermissionRepository +import javax.inject.Inject + +class GrantNotificationsPermission @Inject constructor( + private val repository: PermissionRepository +) { + + @OptIn(ExperimentalPermissionsApi::class) + suspend fun execute( + activity: ComponentActivity, + notificationsPermissionState: PermissionState + ): Boolean { + return repository.grantNotificationsPermission( + activity = activity, + notificationsPermissionState = notificationsPermissionState + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantStoragePermission.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantStoragePermission.kt new file mode 100644 index 00000000..aa201273 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantStoragePermission.kt @@ -0,0 +1,23 @@ +package ua.acclorite.book_story.domain.use_case.permission + +import androidx.activity.ComponentActivity +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import ua.acclorite.book_story.domain.repository.PermissionRepository +import javax.inject.Inject + +class GrantStoragePermission @Inject constructor( + private val repository: PermissionRepository +) { + + @OptIn(ExperimentalPermissionsApi::class) + suspend fun execute( + activity: ComponentActivity, + storagePermissionState: PermissionState + ): Boolean { + return repository.grantStoragePermission( + activity = activity, + storagePermissionState = storagePermissionState + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt index e209f4a6..73781349 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt @@ -1,15 +1,11 @@ package ua.acclorite.book_story.ui.browse -import android.content.Intent -import android.net.Uri import android.os.Build import android.os.Environment -import android.provider.Settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.shouldShowRationale import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -31,7 +27,7 @@ import ua.acclorite.book_story.domain.library.book.SelectableNullableBook import ua.acclorite.book_story.domain.use_case.book.InsertBook import ua.acclorite.book_story.domain.use_case.file_system.GetBookFromFile import ua.acclorite.book_story.domain.use_case.file_system.GetFilesFromDevice -import ua.acclorite.book_story.presentation.core.util.launchActivity +import ua.acclorite.book_story.domain.use_case.permission.GrantStoragePermission import ua.acclorite.book_story.presentation.core.util.showToast import ua.acclorite.book_story.ui.library.LibraryScreen import java.io.File @@ -40,6 +36,7 @@ import kotlin.collections.map @HiltViewModel class BrowseModel @Inject constructor( + private val grantStoragePermission: GrantStoragePermission, private val getFilesFromDevice: GetFilesFromDevice, private val getBookFromFile: GetBookFromFile, private val insertBook: InsertBook @@ -274,72 +271,13 @@ class BrowseModel @Inject constructor( } is BrowseEvent.OnActionPermissionDialog -> { - viewModelScope.launch { - val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R - val isPermissionGranted = if (legacyStoragePermission) { - event.storagePermissionState.status.isGranted - } else { - Environment.isExternalStorageManager() - } - - if (isPermissionGranted) { - _state.update { - it.copy( - dialog = null, - isError = false - ) - } - onEvent( - BrowseEvent.OnRefreshList( - showIndicator = true, - hideSearch = false - ) - ) - return@launch - } - - if (legacyStoragePermission) { - if (!event.storagePermissionState.status.shouldShowRationale) { - event.storagePermissionState.launchPermissionRequest() - } else { - val uri = Uri.parse("package:${event.activity.packageName}") - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri) - - intent.launchActivity(event.activity) { - return@launch - } - } - } - - if (!legacyStoragePermission) { - val uri = Uri.parse("package:${event.activity.packageName}") - val intent = Intent( - Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, - uri - ) - - intent.launchActivity(event.activity) { - return@launch - } - } - - storagePermissionJob?.cancel() - storagePermissionJob = launch { - while (true) { - val granted = if (legacyStoragePermission) { - event.storagePermissionState.status.isGranted - } else { - Environment.isExternalStorageManager() - } - - if (!granted) { - delay(1000) - yield() - continue - } - - yield() - + storagePermissionJob?.cancel() + storagePermissionJob = viewModelScope.launch(Dispatchers.IO) { + grantStoragePermission.execute( + activity = event.activity, + storagePermissionState = event.storagePermissionState + ).apply { + if (this) { _state.update { it.copy( dialog = null, @@ -352,7 +290,6 @@ class BrowseModel @Inject constructor( hideSearch = false ) ) - break } } } @@ -361,19 +298,22 @@ class BrowseModel @Inject constructor( is BrowseEvent.OnDismissPermissionDialog -> { viewModelScope.launch { val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R - val isPermissionGranted = if (!legacyPermission) { + val permissionGranted = if (!legacyPermission) { Environment.isExternalStorageManager() } else event.storagePermissionState.status.isGranted storagePermissionJob?.cancel() - _state.update { it.copy(dialog = null) } + _state.update { + it.copy( + isError = !permissionGranted, + dialog = null + ) + } - if (isPermissionGranted) { + if (permissionGranted) { viewModelScope.launch(Dispatchers.IO) { getFilesFromDownloads() } - } else { - _state.update { it.copy(isError = true) } } } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt index e374c925..6af89ad1 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt @@ -1,18 +1,12 @@ package ua.acclorite.book_story.ui.settings -import android.content.Intent -import android.os.Build -import android.provider.Settings import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.shouldShowRationale import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -27,15 +21,16 @@ import ua.acclorite.book_story.domain.use_case.color_preset.GetColorPresets import ua.acclorite.book_story.domain.use_case.color_preset.ReorderColorPresets import ua.acclorite.book_story.domain.use_case.color_preset.SelectColorPreset import ua.acclorite.book_story.domain.use_case.color_preset.UpdateColorPreset +import ua.acclorite.book_story.domain.use_case.permission.GrantNotificationsPermission import ua.acclorite.book_story.presentation.core.constants.Constants import ua.acclorite.book_story.presentation.core.constants.provideDefaultColorPreset -import ua.acclorite.book_story.presentation.core.util.launchActivity import ua.acclorite.book_story.presentation.core.util.showToast import javax.inject.Inject import kotlin.random.Random @HiltViewModel class SettingsModel @Inject constructor( + private val grantNotificationsPermission: GrantNotificationsPermission, private val getColorPresets: GetColorPresets, private val updateColorPreset: UpdateColorPreset, private val selectColorPreset: SelectColorPreset, @@ -98,45 +93,20 @@ class SettingsModel @Inject constructor( fun onEvent(event: SettingsEvent) { when (event) { is SettingsEvent.OnChangeCheckForUpdates -> { - if (!event.enable) { - event.onChangeCheckForUpdates(false) - return - } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - event.onChangeCheckForUpdates(true) - return - } - - if (event.notificationsPermissionState.status.isGranted) { - event.onChangeCheckForUpdates(true) - return - } - - if (!event.notificationsPermissionState.status.shouldShowRationale) { - event.notificationsPermissionState.launchPermissionRequest() - } else { - val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) - intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName) - - intent.launchActivity(event.activity) { - return - } - } - notificationsPermissionJob?.cancel() - notificationsPermissionJob = viewModelScope.launch { - for (i in 1..10) { - if (!event.notificationsPermissionState.status.isGranted) { - delay(1000) - yield() - continue + notificationsPermissionJob = viewModelScope.launch(Dispatchers.IO) { + if (!event.enable) { + event.onChangeCheckForUpdates(false) + return@launch + } + + grantNotificationsPermission.execute( + activity = event.activity, + notificationsPermissionState = event.notificationsPermissionState + ).apply { + if (this) { + event.onChangeCheckForUpdates(true) } - - yield() - - event.onChangeCheckForUpdates(true) - break } } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/start/StartModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/start/StartModel.kt index 72cdd837..038cdd20 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/start/StartModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/start/StartModel.kt @@ -1,31 +1,28 @@ package ua.acclorite.book_story.ui.start -import android.content.Intent -import android.net.Uri import android.os.Build import android.os.Environment -import android.provider.Settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.shouldShowRationale import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.yield -import ua.acclorite.book_story.presentation.core.util.launchActivity +import ua.acclorite.book_story.domain.use_case.permission.GrantNotificationsPermission +import ua.acclorite.book_story.domain.use_case.permission.GrantStoragePermission import javax.inject.Inject @HiltViewModel class StartModel @Inject constructor( - + private val grantStoragePermission: GrantStoragePermission, + private val grantNotificationsPermission: GrantNotificationsPermission ) : ViewModel() { private val mutex = Mutex() @@ -62,113 +59,36 @@ class StartModel @Inject constructor( } is StartEvent.OnStoragePermissionRequest -> { - viewModelScope.launch { - val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R - - val isPermissionGranted = if (legacyStoragePermission) { - event.storagePermissionState.status.isGranted - } else Environment.isExternalStorageManager() - - if (isPermissionGranted) { - _state.update { - it.copy( - storagePermissionGranted = true - ) - } - return@launch - } - - if (legacyStoragePermission) { - if (!event.storagePermissionState.status.shouldShowRationale) { - event.storagePermissionState.launchPermissionRequest() - } else { - val uri = Uri.parse("package:${event.activity.packageName}") - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri) - - intent.launchActivity(event.activity) { - return@launch - } - } - } - - if (!legacyStoragePermission) { - val uri = Uri.parse("package:${event.activity.packageName}") - val intent = Intent( - Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, - uri - ) - - intent.launchActivity(event.activity) { - return@launch - } - } - - storagePermissionJob?.cancel() - storagePermissionJob = viewModelScope.launch { - while (true) { - val granted = if (legacyStoragePermission) { - event.storagePermissionState.status.isGranted - } else Environment.isExternalStorageManager() - - if (!granted) { - delay(1000) - yield() - continue - } - - yield() - + storagePermissionJob?.cancel() + storagePermissionJob = viewModelScope.launch(Dispatchers.IO) { + grantStoragePermission.execute( + activity = event.activity, + storagePermissionState = event.storagePermissionState + ).apply { + if (this) { _state.update { it.copy( storagePermissionGranted = true ) } - break } } } } is StartEvent.OnNotificationsPermissionRequest -> { - viewModelScope.launch { - if (event.notificationsPermissionState.status.isGranted) { - _state.update { - it.copy( - notificationsPermissionGranted = true - ) - } - return@launch - } - - if (!event.notificationsPermissionState.status.shouldShowRationale) { - event.notificationsPermissionState.launchPermissionRequest() - } else { - val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) - intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName) - - intent.launchActivity(event.activity) { - return@launch - } - } - - notificationsPermissionJob?.cancel() - notificationsPermissionJob = viewModelScope.launch { - while (true) { - if (!event.notificationsPermissionState.status.isGranted) { - delay(1000) - yield() - continue - } - - yield() - + notificationsPermissionJob?.cancel() + notificationsPermissionJob = viewModelScope.launch(Dispatchers.IO) { + grantNotificationsPermission.execute( + activity = event.activity, + notificationsPermissionState = event.notificationsPermissionState + ).apply { + if (this) { _state.update { it.copy( notificationsPermissionGranted = true ) } - - break } } }