🛠️ 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.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(

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

View file

@ -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 -> {
notificationsPermissionJob?.cancel()
notificationsPermissionJob = viewModelScope.launch(Dispatchers.IO) {
if (!event.enable) {
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)
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
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 = 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 = viewModelScope.launch(Dispatchers.IO) {
grantNotificationsPermission.execute(
activity = event.activity,
notificationsPermissionState = event.notificationsPermissionState
).apply {
if (this) {
_state.update {
it.copy(
notificationsPermissionGranted = true
)
}
break
}
}
}