🛠️ Create PermissionRepository

* Created PermissionRepository that manages all permissions
This commit is contained in:
Acclorite 2025-02-03 11:43:05 +02:00
parent 9866641367
commit 9670470f7e
8 changed files with 271 additions and 219 deletions

View file

@ -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.DataStoreRepositoryImpl
import ua.acclorite.book_story.data.repository.FileSystemRepositoryImpl import ua.acclorite.book_story.data.repository.FileSystemRepositoryImpl
import ua.acclorite.book_story.data.repository.HistoryRepositoryImpl 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.data.repository.RemoteRepositoryImpl
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.repository.ColorPresetRepository import ua.acclorite.book_story.domain.repository.ColorPresetRepository
import ua.acclorite.book_story.domain.repository.DataStoreRepository import ua.acclorite.book_story.domain.repository.DataStoreRepository
import ua.acclorite.book_story.domain.repository.FileSystemRepository import ua.acclorite.book_story.domain.repository.FileSystemRepository
import ua.acclorite.book_story.domain.repository.HistoryRepository 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 ua.acclorite.book_story.domain.repository.RemoteRepository
import javax.inject.Singleton import javax.inject.Singleton
@ -77,6 +79,12 @@ abstract class RepositoryModule {
remoteRepositoryImpl: RemoteRepositoryImpl remoteRepositoryImpl: RemoteRepositoryImpl
): RemoteRepository ): RemoteRepository
@Binds
@Singleton
abstract fun bindPermissionRepository(
permissionRepositoryImpl: PermissionRepositoryImpl
): PermissionRepository
@Binds @Binds
@Singleton @Singleton
abstract fun bindBookMapper( abstract fun bindBookMapper(

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
)
}
}

View file

@ -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
)
}
}

View file

@ -1,15 +1,11 @@
package ua.acclorite.book_story.ui.browse package ua.acclorite.book_story.ui.browse
import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.Settings
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.shouldShowRationale
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job 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.book.InsertBook
import ua.acclorite.book_story.domain.use_case.file_system.GetBookFromFile 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.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.presentation.core.util.showToast
import ua.acclorite.book_story.ui.library.LibraryScreen import ua.acclorite.book_story.ui.library.LibraryScreen
import java.io.File import java.io.File
@ -40,6 +36,7 @@ import kotlin.collections.map
@HiltViewModel @HiltViewModel
class BrowseModel @Inject constructor( class BrowseModel @Inject constructor(
private val grantStoragePermission: GrantStoragePermission,
private val getFilesFromDevice: GetFilesFromDevice, private val getFilesFromDevice: GetFilesFromDevice,
private val getBookFromFile: GetBookFromFile, private val getBookFromFile: GetBookFromFile,
private val insertBook: InsertBook private val insertBook: InsertBook
@ -274,72 +271,13 @@ class BrowseModel @Inject constructor(
} }
is BrowseEvent.OnActionPermissionDialog -> { 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?.cancel()
storagePermissionJob = launch { storagePermissionJob = viewModelScope.launch(Dispatchers.IO) {
while (true) { grantStoragePermission.execute(
val granted = if (legacyStoragePermission) { activity = event.activity,
event.storagePermissionState.status.isGranted storagePermissionState = event.storagePermissionState
} else { ).apply {
Environment.isExternalStorageManager() if (this) {
}
if (!granted) {
delay(1000)
yield()
continue
}
yield()
_state.update { _state.update {
it.copy( it.copy(
dialog = null, dialog = null,
@ -352,7 +290,6 @@ class BrowseModel @Inject constructor(
hideSearch = false hideSearch = false
) )
) )
break
} }
} }
} }
@ -361,19 +298,22 @@ class BrowseModel @Inject constructor(
is BrowseEvent.OnDismissPermissionDialog -> { is BrowseEvent.OnDismissPermissionDialog -> {
viewModelScope.launch { viewModelScope.launch {
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
val isPermissionGranted = if (!legacyPermission) { val permissionGranted = if (!legacyPermission) {
Environment.isExternalStorageManager() Environment.isExternalStorageManager()
} else event.storagePermissionState.status.isGranted } else event.storagePermissionState.status.isGranted
storagePermissionJob?.cancel() storagePermissionJob?.cancel()
_state.update { it.copy(dialog = null) } _state.update {
it.copy(
isError = !permissionGranted,
dialog = null
)
}
if (isPermissionGranted) { if (permissionGranted) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getFilesFromDownloads() getFilesFromDownloads()
} }
} else {
_state.update { it.copy(isError = true) }
} }
} }
} }

View file

@ -1,18 +1,12 @@
package ua.acclorite.book_story.ui.settings package ua.acclorite.book_story.ui.settings
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.accompanist.permissions.ExperimentalPermissionsApi 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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch 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.ReorderColorPresets
import ua.acclorite.book_story.domain.use_case.color_preset.SelectColorPreset 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.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.Constants
import ua.acclorite.book_story.presentation.core.constants.provideDefaultColorPreset 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 ua.acclorite.book_story.presentation.core.util.showToast
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random import kotlin.random.Random
@HiltViewModel @HiltViewModel
class SettingsModel @Inject constructor( class SettingsModel @Inject constructor(
private val grantNotificationsPermission: GrantNotificationsPermission,
private val getColorPresets: GetColorPresets, private val getColorPresets: GetColorPresets,
private val updateColorPreset: UpdateColorPreset, private val updateColorPreset: UpdateColorPreset,
private val selectColorPreset: SelectColorPreset, private val selectColorPreset: SelectColorPreset,
@ -98,45 +93,20 @@ class SettingsModel @Inject constructor(
fun onEvent(event: SettingsEvent) { fun onEvent(event: SettingsEvent) {
when (event) { when (event) {
is SettingsEvent.OnChangeCheckForUpdates -> { is SettingsEvent.OnChangeCheckForUpdates -> {
notificationsPermissionJob?.cancel()
notificationsPermissionJob = viewModelScope.launch(Dispatchers.IO) {
if (!event.enable) { if (!event.enable) {
event.onChangeCheckForUpdates(false) event.onChangeCheckForUpdates(false)
return return@launch
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { grantNotificationsPermission.execute(
activity = event.activity,
notificationsPermissionState = event.notificationsPermissionState
).apply {
if (this) {
event.onChangeCheckForUpdates(true) 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
}
yield()
event.onChangeCheckForUpdates(true)
break
} }
} }
} }

View file

@ -1,31 +1,28 @@
package ua.acclorite.book_story.ui.start package ua.acclorite.book_story.ui.start
import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.Settings
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.shouldShowRationale
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.yield 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 import javax.inject.Inject
@HiltViewModel @HiltViewModel
class StartModel @Inject constructor( class StartModel @Inject constructor(
private val grantStoragePermission: GrantStoragePermission,
private val grantNotificationsPermission: GrantNotificationsPermission
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex() private val mutex = Mutex()
@ -62,113 +59,36 @@ class StartModel @Inject constructor(
} }
is StartEvent.OnStoragePermissionRequest -> { 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?.cancel()
storagePermissionJob = viewModelScope.launch { storagePermissionJob = viewModelScope.launch(Dispatchers.IO) {
while (true) { grantStoragePermission.execute(
val granted = if (legacyStoragePermission) { activity = event.activity,
event.storagePermissionState.status.isGranted storagePermissionState = event.storagePermissionState
} else Environment.isExternalStorageManager() ).apply {
if (this) {
if (!granted) {
delay(1000)
yield()
continue
}
yield()
_state.update { _state.update {
it.copy( it.copy(
storagePermissionGranted = true storagePermissionGranted = true
) )
} }
break
} }
} }
} }
} }
is StartEvent.OnNotificationsPermissionRequest -> { 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?.cancel()
notificationsPermissionJob = viewModelScope.launch { notificationsPermissionJob = viewModelScope.launch(Dispatchers.IO) {
while (true) { grantNotificationsPermission.execute(
if (!event.notificationsPermissionState.status.isGranted) { activity = event.activity,
delay(1000) notificationsPermissionState = event.notificationsPermissionState
yield() ).apply {
continue if (this) {
}
yield()
_state.update { _state.update {
it.copy( it.copy(
notificationsPermissionGranted = true notificationsPermissionGranted = true
) )
} }
break
} }
} }
} }