()
19 |
20 | @SuppressLint("ComposeUnstableReceiver")
21 | @Composable
22 | @ReadOnlyComposable
23 | fun FileType.stringResource(): String =
24 | when (this) {
25 | is PresetWrappingFileType<*> -> stringResource(presetFileType.labelRes)
26 | is CustomFileType -> name
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/w2sv/filenavigator/ui/screen/home/components/HomeScreenCard.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.screen.home.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.ColumnScope
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.ElevatedCard
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.dp
10 | import com.w2sv.filenavigator.ui.designsystem.AppCardDefaults
11 |
12 | @Composable
13 | fun HomeScreenCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
14 | ElevatedCard(
15 | modifier = modifier,
16 | elevation = AppCardDefaults.elevation
17 | ) {
18 | Column(
19 | modifier = Modifier
20 | .padding(24.dp),
21 | content = content
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/w2sv/filenavigator/ui/state/PostNotificationsPermissionState.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.state
2 |
3 | import android.Manifest
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.Stable
6 | import com.google.accompanist.permissions.PermissionState
7 | import com.google.accompanist.permissions.isGranted
8 | import com.google.accompanist.permissions.rememberPermissionState
9 | import com.w2sv.androidutils.os.postNotificationsPermissionRequired
10 | import com.w2sv.composed.OnChange
11 |
12 | @Stable
13 | @JvmInline
14 | value class PostNotificationsPermissionState(val state: PermissionState?)
15 |
16 | @Composable
17 | fun rememberPostNotificationsPermissionState(
18 | onPermissionResult: (Boolean) -> Unit,
19 | onStatusChanged: (Boolean) -> Unit
20 | ): PostNotificationsPermissionState =
21 | PostNotificationsPermissionState(
22 | state = if (postNotificationsPermissionRequired) {
23 | rememberPermissionState(
24 | permission = Manifest.permission.POST_NOTIFICATIONS,
25 | onPermissionResult = onPermissionResult
26 | )
27 | .also {
28 | OnChange(value = it.status) { status ->
29 | onStatusChanged(status.isGranted)
30 | }
31 | }
32 | } else {
33 | null
34 | }
35 | )
36 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/w2sv/filenavigator/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.theme
2 |
3 | import androidx.compose.material3.ColorScheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.ReadOnlyComposable
6 | import androidx.compose.ui.graphics.Color
7 |
8 | object AppColor {
9 | val success = Color(12, 173, 34, 200)
10 | val error = Color(201, 14, 52, 200)
11 | }
12 |
13 | val ColorScheme.onSurfaceDisabled: Color
14 | @Composable
15 | @ReadOnlyComposable
16 | get() = onSurface.copy(0.38f)
17 |
18 | val ColorScheme.onSurfaceVariantDecreasedAlpha: Color
19 | @Composable
20 | @ReadOnlyComposable
21 | get() = onSurfaceVariant.copy(0.6f)
22 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/w2sv/filenavigator/ui/theme/Dims.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.theme
2 |
3 | const val DEFAULT_ANIMATION_DURATION = 500
4 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/w2sv/filenavigator/ui/util/Color.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.util
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.ReadOnlyComposable
6 | import androidx.compose.ui.graphics.Color
7 | import com.w2sv.filenavigator.ui.theme.onSurfaceDisabled
8 |
9 | @Composable
10 | @ReadOnlyComposable
11 | fun Color.orOnSurfaceDisabledIf(condition: Boolean): Color =
12 | if (condition) MaterialTheme.colorScheme.onSurfaceDisabled else this
13 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/w2sv/filenavigator/ui/util/Easing.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.util
2 |
3 | import android.view.animation.AnticipateInterpolator
4 | import android.view.animation.AnticipateOvershootInterpolator
5 | import android.view.animation.OvershootInterpolator
6 | import com.w2sv.composed.extensions.toEasing
7 | import com.w2sv.kotlinutils.threadUnsafeLazy
8 |
9 | object Easing {
10 | val Anticipate by threadUnsafeLazy { AnticipateInterpolator().toEasing() }
11 | val Overshoot by threadUnsafeLazy { OvershootInterpolator().toEasing() }
12 | val AnticipateOvershoot by threadUnsafeLazy { AnticipateOvershootInterpolator().toEasing() }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/w2sv/filenavigator/ui/util/MovableContent.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.util
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.movableContentOf
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.Modifier
7 |
8 | typealias ModifierReceivingComposable = @Composable (Modifier) -> Unit
9 |
10 | @Composable
11 | fun rememberMovableContentOf(content: @Composable (P) -> Unit): @Composable (P) -> Unit =
12 | remember {
13 | movableContentOf(content)
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/w2sv/filenavigator/ui/util/Saving.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.util
2 |
3 | import androidx.compose.runtime.saveable.listSaver
4 | import androidx.compose.runtime.snapshots.SnapshotStateList
5 | import androidx.compose.runtime.toMutableStateList
6 |
7 | // TODO: Composed
8 | fun snapshotStateListSaver() =
9 | listSaver, T>(
10 | save = { stateList -> stateList.toList() },
11 | restore = { it.toMutableStateList() }
12 | )
13 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/w2sv/filenavigator/ui/util/StateConversion.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.util
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.runtime.Composable
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.lifecycle.LifecycleOwner
7 | import androidx.lifecycle.compose.LocalLifecycleOwner
8 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
9 | import kotlin.coroutines.CoroutineContext
10 | import kotlin.coroutines.EmptyCoroutineContext
11 | import kotlinx.coroutines.flow.StateFlow
12 |
13 | /**
14 | * A shorthand for
15 | * `StateFlow.collectAsStateWithLifecycle().value`
16 | */
17 | @SuppressLint("ComposeUnstableReceiver")
18 | @Composable
19 | fun StateFlow.lifecycleAwareStateValue(
20 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
21 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
22 | context: CoroutineContext = EmptyCoroutineContext
23 | ): T =
24 | collectAsStateWithLifecycle(lifecycleOwner, minActiveState, context).value
25 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/w2sv/filenavigator/ui/util/activityViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.util
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.platform.LocalContext
5 | import androidx.compose.ui.platform.LocalView
6 | import androidx.hilt.navigation.compose.hiltViewModel
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.ViewModelStoreOwner
9 | import androidx.lifecycle.findViewTreeViewModelStoreOwner
10 | import com.w2sv.androidutils.findActivity
11 |
12 | @Composable
13 | inline fun activityViewModel(): VM =
14 | hiltViewModel(
15 | LocalView.current.findViewTreeViewModelStoreOwner()
16 | ?: LocalContext.current.findActivity() as ViewModelStoreOwner
17 | )
18 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/w2sv/filenavigator/ui/viewmodel/MoveHistoryViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.w2sv.domain.model.MovedFile
6 | import com.w2sv.domain.repository.MovedFileRepository
7 | import com.w2sv.domain.usecase.GetMoveHistoryUseCase
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import javax.inject.Inject
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.Job
12 | import kotlinx.coroutines.flow.SharingStarted
13 | import kotlinx.coroutines.flow.stateIn
14 | import kotlinx.coroutines.launch
15 |
16 | @HiltViewModel
17 | class MoveHistoryViewModel @Inject constructor(
18 | private val movedFileRepository: MovedFileRepository,
19 | getMoveHistoryUseCase: GetMoveHistoryUseCase
20 | ) :
21 | ViewModel() {
22 |
23 | val moveHistory = getMoveHistoryUseCase
24 | .invoke()
25 | .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
26 |
27 | fun launchHistoryDeletion(): Job =
28 | viewModelScope.launch(Dispatchers.IO) {
29 | movedFileRepository.deleteAll()
30 | }
31 |
32 | fun launchEntryDeletion(movedFile: MovedFile): Job =
33 | viewModelScope.launch(Dispatchers.IO) {
34 | movedFileRepository.delete(movedFile)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/play/contact-email.txt:
--------------------------------------------------------------------------------
1 | zangenbergjanek@googlemail.com
2 |
--------------------------------------------------------------------------------
/app/src/main/play/default-language.txt:
--------------------------------------------------------------------------------
1 | en-US
2 |
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/full-description.txt:
--------------------------------------------------------------------------------
1 | Welcome to File Navigator - Your Ultimate File Sorting Solution!
2 |
3 | Are you tired of cluttered files on your device? Seeking a streamlined way to organize and manage them? Look no further! File Navigator is here to transform your file management experience.
4 |
5 | Key Features:
6 |
7 | 🔍 File Type Customization: File Navigator puts you in control. Select the specific file types you want to navigate, including Images, Videos, Audio, Text, APKs, PDFs, and Archives.
8 |
9 | 📁 Effortless File Organization: Configure the app to match your preferences. File Navigator allows you to define where different file types should be stored, ensuring they end up in the right place every time.
10 |
11 | 📬 Instant Notifications: When new files of your chosen file types are detected in your system, File Navigator sends you instant notifications. No more surprises - you'll always be up-to-date.
12 |
13 | 🚀 Seamless File Movement: With a simple tap from the notification, you can select the destination folder for the new file. No more manual searching and sorting.
14 |
15 | 🛡️ Precise Permissions: To deliver these exceptional features, File Navigator requests the 'access to manage all files' permission. Rest assured, this permission is solely used to move files you explicitly command to be moved.
16 |
17 | 📱 Simplify Your Digital Life: File Navigator is the ultimate solution to keep your files organized and readily accessible. Enjoy a clutter-free device and manage your files with ease.
18 |
19 | Don't let file chaos overwhelm you. Say goodbye to clutter and embrace the power of precise file management!
20 |
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/graphics/feature-graphic/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/play/listings/en-US/graphics/feature-graphic/1.png
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/graphics/icon/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/play/listings/en-US/graphics/icon/1.png
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/graphics/large-tablet-screenshots/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/play/listings/en-US/graphics/large-tablet-screenshots/1.jpg
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/graphics/large-tablet-screenshots/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/play/listings/en-US/graphics/large-tablet-screenshots/2.jpg
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/graphics/large-tablet-screenshots/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/play/listings/en-US/graphics/large-tablet-screenshots/3.jpg
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.jpg
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.jpg
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.jpg
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/graphics/tablet-screenshots/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/play/listings/en-US/graphics/tablet-screenshots/1.jpg
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/graphics/tablet-screenshots/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/play/listings/en-US/graphics/tablet-screenshots/2.jpg
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/graphics/tablet-screenshots/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/play/listings/en-US/graphics/tablet-screenshots/3.jpg
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/short-description.txt:
--------------------------------------------------------------------------------
1 | The missing link between Android and a well-sorted file system
2 |
--------------------------------------------------------------------------------
/app/src/main/play/listings/en-US/title.txt:
--------------------------------------------------------------------------------
1 | File Navigator
2 |
--------------------------------------------------------------------------------
/app/src/main/play/release-notes/en-US/production.txt:
--------------------------------------------------------------------------------
1 | - Enable creation of custom file types
2 | - Enable configuration of file type colors, as well as the exclusion of file extensions for non-media types
3 | - Add 'html' to Text file type extensions
4 | - Enabling and disabling of file types may now be done via a single, cohesive bottom sheet
5 | - UI polishments
6 | - Performance improvements
7 |
--------------------------------------------------------------------------------
/app/src/main/res/font/raleway_black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/font/raleway_black.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/raleway_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/font/raleway_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/raleway_extrabold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/font/raleway_extrabold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/raleway_extralight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/font/raleway_extralight.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/raleway_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/font/raleway_light.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/raleway_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/font/raleway_medium.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/raleway_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/font/raleway_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/raleway_semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/font/raleway_semibold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/raleway_thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/font/raleway_thin.ttf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/w2sv/filenavigator/ui/screen/navigatorsettings/components/FileTypeCreationDialogKtTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.screen.navigatorsettings.components
2 |
3 | class FileTypeCreationDialogKtTest {
4 |
5 | // @Test
6 | // fun `test get IsMediaFileTypeExtension`() {
7 | // fun test(expectedMediaFileType: PresetFileType.Media?, extension: String) {
8 | // val expectedResult = expectedMediaFileType?.let { FileExtensionInvalidityReason.IsNonExcludableFileTypeExtension(extension, it) }
9 | // assertEquals(expectedResult, IsNonExcludableFileTypeExtension.get(extension))
10 | // }
11 | //
12 | // test(PresetFileType.Image, "jpg")
13 | // test(PresetFileType.Audio, "mp3")
14 | // test(null, "xml")
15 | // }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/w2sv/filenavigator/ui/util/MockInvalidityReason.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.util
2 |
3 | enum class MockInvalidityReason(override val errorMessageRes: Int) : InputInvalidityReason {
4 | ContainsSpecialCharacters(0)
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/w2sv/filenavigator/ui/util/ProxyTextEditorTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.util
2 |
3 | import com.w2sv.common.util.containsSpecialCharacter
4 | import junit.framework.TestCase
5 | import org.junit.Before
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 | import org.robolectric.RobolectricTestRunner
9 |
10 | @RunWith(RobolectricTestRunner::class)
11 | class ProxyTextEditorTest {
12 |
13 | private lateinit var proxyEditor: ProxyTextEditor
14 | private var proxyValue = ""
15 |
16 | @Before
17 | fun setUp() {
18 | proxyEditor = ProxyTextEditor(
19 | getValue = { proxyValue },
20 | setValue = { proxyValue = it },
21 | processInput = { it.trim() },
22 | findInvalidityReason = { if (it.containsSpecialCharacter()) MockInvalidityReason.ContainsSpecialCharacters else null }
23 | )
24 | }
25 |
26 | @Test
27 | fun `proxyEditor updates value correctly`() {
28 | proxyEditor.update(" new proxy input ")
29 | TestCase.assertEquals("new proxy input", proxyValue)
30 | }
31 |
32 | @Test
33 | fun `proxyEditor identifies valid input`() {
34 | proxyEditor.update("valid input")
35 | TestCase.assertTrue(proxyEditor.isValid)
36 | }
37 |
38 | @Test
39 | fun `proxyEditor identifies invalid input`() {
40 | proxyEditor.update("sdaf-x")
41 | TestCase.assertFalse(proxyEditor.isValid)
42 | TestCase.assertEquals(
43 | MockInvalidityReason.ContainsSpecialCharacters,
44 | proxyEditor.invalidityReason
45 | )
46 | }
47 |
48 | @Test
49 | fun `proxyEditor pop resets value`() {
50 | proxyEditor.update("new proxy value")
51 | val popped = proxyEditor.pop()
52 | TestCase.assertEquals("new proxy value", popped)
53 | TestCase.assertEquals("", proxyValue)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/w2sv/filenavigator/ui/util/StatefulTextEditorTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator.ui.util
2 |
3 | import com.w2sv.common.util.containsSpecialCharacter
4 | import junit.framework.TestCase.assertEquals
5 | import junit.framework.TestCase.assertFalse
6 | import junit.framework.TestCase.assertTrue
7 | import org.junit.Before
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import org.robolectric.RobolectricTestRunner
11 |
12 | @RunWith(RobolectricTestRunner::class)
13 | class StatefulTextEditorTest {
14 |
15 | private lateinit var statefulEditor: StatefulTextEditor
16 |
17 | @Before
18 | fun setUp() {
19 | statefulEditor = StatefulTextEditor(
20 | initialText = "hello",
21 | processInput = { it.trim() },
22 | findInvalidityReason = { if (it.containsSpecialCharacter()) MockInvalidityReason.ContainsSpecialCharacters else null }
23 | )
24 | }
25 |
26 | @Test
27 | fun `statefulEditor updates value correctly`() {
28 | statefulEditor.update(" new input ")
29 | assertEquals("new input", statefulEditor.getValue())
30 | }
31 |
32 | @Test
33 | fun `statefulEditor identifies valid input`() {
34 | statefulEditor.update("valid input")
35 | assertTrue(statefulEditor.isValid)
36 | }
37 |
38 | @Test
39 | fun `statefulEditor identifies invalid input`() {
40 | statefulEditor.update(".asdfa")
41 | assertFalse(statefulEditor.isValid)
42 | assertEquals(MockInvalidityReason.ContainsSpecialCharacters, statefulEditor.invalidityReason)
43 | }
44 |
45 | @Test
46 | fun `statefulEditor pop resets value`() {
47 | statefulEditor.update("new value")
48 | val popped = statefulEditor.pop()
49 | assertEquals("new value", popped)
50 | assertEquals("hello", statefulEditor.getValue())
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/benchmarking/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/benchmarking/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.ManagedVirtualDevice
2 |
3 | plugins {
4 | alias(libs.plugins.ktlint)
5 | alias(libs.plugins.android.test)
6 | alias(libs.plugins.kotlin.android)
7 | alias(libs.plugins.baselineprofile)
8 | }
9 |
10 | val mvdName = "Pixel 6 API 33"
11 |
12 | android {
13 | namespace = "com.filenavigator.benchmarking"
14 | compileSdk = libs.versions.compileSdk.get().toInt()
15 |
16 | defaultConfig {
17 | minSdk = 28
18 | targetSdk = libs.versions.compileSdk.get().toInt()
19 |
20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
21 | }
22 |
23 | compileOptions {
24 | sourceCompatibility = JavaVersion.VERSION_17
25 | targetCompatibility = JavaVersion.VERSION_17
26 | }
27 |
28 | testOptions.managedDevices.allDevices {
29 | @Suppress("UnstableApiUsage")
30 | create(mvdName) {
31 | device = "Pixel 6"
32 | apiLevel = 33
33 | systemImageSource = "aosp"
34 | }
35 | }
36 |
37 | targetProjectPath = ":app"
38 | }
39 |
40 | // Baseline profile configuration: https://developer.android.com/topic/performance/baselineprofiles/configure-baselineprofiles
41 | baselineProfile {
42 | @Suppress("UnstableApiUsage")
43 | enableEmulatorDisplay = false
44 | useConnectedDevices = false
45 | managedDevices += mvdName
46 | }
47 |
48 | dependencies {
49 | implementation(libs.androidx.test.ext.junit)
50 | implementation(libs.androidx.benchmark.macro.junit4)
51 | implementation(libs.androidx.test.runner)
52 | }
53 |
--------------------------------------------------------------------------------
/benchmarking/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/benchmarking/src/main/kotlin/com/w2sv/filenavigator/UiDeviceExt.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.filenavigator
2 |
3 | import androidx.test.uiautomator.By
4 | import androidx.test.uiautomator.Direction
5 | import androidx.test.uiautomator.UiDevice
6 | import java.io.ByteArrayOutputStream
7 |
8 | private fun UiDevice.dumpWindowHierarchy(): String {
9 | val outputStream = ByteArrayOutputStream()
10 | dumpWindowHierarchy(outputStream)
11 | return outputStream.toString("UTF-8")
12 | }
13 |
14 | private fun UiDevice.flingListDown(resourceName: String) {
15 | findObject(By.res(resourceName)).fling(Direction.DOWN)
16 | waitForIdle()
17 | }
18 |
--------------------------------------------------------------------------------
/build-logic/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /.gradle
3 | .kotlin
4 | convention/build
5 |
--------------------------------------------------------------------------------
/build-logic/convention/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2 |
3 | plugins {
4 | `kotlin-dsl`
5 | alias(libs.plugins.ktlint)
6 | }
7 |
8 | java {
9 | sourceCompatibility = JavaVersion.VERSION_17
10 | targetCompatibility = JavaVersion.VERSION_17
11 | }
12 |
13 | kotlin {
14 | compilerOptions {
15 | jvmTarget = JvmTarget.JVM_17
16 | }
17 | }
18 |
19 | dependencies {
20 | compileOnly(libs.android.gradlePlugin)
21 | compileOnly(libs.kotlin.gradlePlugin)
22 | compileOnly(libs.ksp.gradlePlugin)
23 | }
24 |
25 | tasks {
26 | validatePlugins {
27 | enableStricterValidation = true
28 | failOnWarning = true
29 | }
30 | }
31 |
32 | gradlePlugin {
33 | plugins {
34 | register("library") {
35 | id = "filenavigator.library"
36 | implementationClass = "LibraryConventionPlugin"
37 | }
38 | register("application") {
39 | id = "filenavigator.application"
40 | implementationClass = "ApplicationConventionPlugin"
41 | }
42 | register("hilt") {
43 | id = "filenavigator.hilt"
44 | implementationClass = "HiltConventionPlugin"
45 | }
46 | register("room") {
47 | id = "filenavigator.room"
48 | implementationClass = "RoomConventionPlugin"
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/ApplicationConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import helpers.Namespace
2 | import helpers.applyBaseConfig
3 | import helpers.applyPlugins
4 | import helpers.catalog
5 | import org.gradle.api.Plugin
6 | import org.gradle.api.Project
7 |
8 | class ApplicationConventionPlugin : Plugin {
9 | override fun apply(target: Project) {
10 | with(target) {
11 | pluginManager.applyPlugins("android-application", "kotlin-android", catalog = catalog)
12 | applyBaseConfig(Namespace.Manual("com.w2sv.filenavigator"))
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import helpers.applyPlugins
2 | import helpers.catalog
3 | import helpers.library
4 | import org.gradle.api.Plugin
5 | import org.gradle.api.Project
6 | import org.gradle.kotlin.dsl.dependencies
7 |
8 | class HiltConventionPlugin : Plugin {
9 | override fun apply(target: Project) {
10 | with(target) {
11 | pluginManager.applyPlugins("ksp", "hilt", catalog = catalog)
12 |
13 | dependencies {
14 | "implementation"(library("google.hilt"))
15 | "ksp"(library("google.hilt.compiler"))
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/LibraryConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import helpers.applyBaseConfig
2 | import helpers.applyPlugins
3 | import helpers.catalog
4 | import org.gradle.api.Plugin
5 | import org.gradle.api.Project
6 |
7 | class LibraryConventionPlugin : Plugin {
8 | override fun apply(target: Project) {
9 | with(target) {
10 | pluginManager.applyPlugins("android-library", "kotlin-android", catalog = catalog)
11 | applyBaseConfig()
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/RoomConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.google.devtools.ksp.gradle.KspExtension
2 | import helpers.applyPlugins
3 | import helpers.catalog
4 | import helpers.library
5 | import org.gradle.api.Plugin
6 | import org.gradle.api.Project
7 | import org.gradle.api.tasks.InputDirectory
8 | import org.gradle.api.tasks.PathSensitive
9 | import org.gradle.api.tasks.PathSensitivity
10 | import org.gradle.kotlin.dsl.configure
11 | import org.gradle.kotlin.dsl.dependencies
12 | import org.gradle.process.CommandLineArgumentProvider
13 | import java.io.File
14 |
15 | class RoomConventionPlugin : Plugin {
16 | override fun apply(target: Project) {
17 | with(target) {
18 | pluginManager.applyPlugins("ksp", catalog = catalog)
19 |
20 | extensions.configure {
21 | // The schemas directory contains a schema file for each version of the Room database.
22 | // This is required to enable Room auto migrations.
23 | // See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration.
24 | arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
25 | }
26 |
27 | dependencies {
28 | "implementation"(library("androidx.room.runtime"))
29 | "implementation"(library("androidx.room.ktx"))
30 | "ksp"(library("androidx.room.compiler"))
31 | }
32 | }
33 | }
34 |
35 | /**
36 | * https://issuetracker.google.com/issues/132245929
37 | * [Export schemas](https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas)
38 | */
39 | class RoomSchemaArgProvider(
40 | @get:InputDirectory
41 | @get:PathSensitive(PathSensitivity.RELATIVE)
42 | val schemaDir: File,
43 | ) : CommandLineArgumentProvider {
44 | override fun asArguments() = listOf("room.schemaLocation=${schemaDir.path}")
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/helpers/Extensions.kt:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.api.artifacts.MinimalExternalModuleDependency
5 | import org.gradle.api.artifacts.VersionCatalog
6 | import org.gradle.api.artifacts.VersionCatalogsExtension
7 | import org.gradle.api.plugins.PluginManager
8 | import org.gradle.api.provider.Provider
9 | import org.gradle.kotlin.dsl.getByType
10 |
11 | /**
12 | * @param alias The version alias that will be passed to [VersionCatalog.findVersion]
13 | */
14 | internal fun VersionCatalog.findVersionInt(alias: String): Int =
15 | findVersion(alias).get().requiredVersion.toInt()
16 |
17 | /**
18 | * @param alias The plugin alias that will be passed to [VersionCatalog.findPlugin]
19 | */
20 | internal fun VersionCatalog.findPluginId(alias: String): String =
21 | findPlugin(alias).get().get().pluginId
22 |
23 | internal val Project.catalog
24 | get(): VersionCatalog = extensions.getByType().named("libs")
25 |
26 | /**
27 | * @param alias The library alias that will be passed to [VersionCatalog.findLibrary]
28 | */
29 | internal fun Project.library(alias: String): Provider =
30 | catalog.findLibrary(alias).get()
31 |
32 | internal fun PluginManager.applyPlugins(vararg pluginId: String, catalog: VersionCatalog) {
33 | pluginId.forEach {
34 | apply(catalog.findPluginId(it))
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/build-logic/gradle.properties:
--------------------------------------------------------------------------------
1 | # Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534
2 | org.gradle.parallel=true
3 | org.gradle.caching=true
4 | org.gradle.configureondemand=true
5 | org.gradle.configuration-cache=true
6 | org.gradle.configuration-cache.parallel=true
7 |
--------------------------------------------------------------------------------
/build-logic/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
4 |
5 | dependencyResolutionManagement {
6 | repositories {
7 | mavenCentral()
8 | google()
9 | gradlePluginPortal()
10 | }
11 | versionCatalogs {
12 | create("libs") {
13 | from(files("../gradle/libs.versions.toml"))
14 | }
15 | }
16 | }
17 |
18 | rootProject.name = "build-logic"
19 | include(":convention")
20 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
2 |
3 | plugins {
4 | alias(libs.plugins.android.application) apply false
5 | alias(libs.plugins.android.library) apply false
6 | alias(libs.plugins.android.test) apply false
7 | alias(libs.plugins.kotlin.android) apply false
8 | alias(libs.plugins.kotlin.compose.compiler) apply false
9 | alias(libs.plugins.kotlin.parcelize) apply false
10 | alias(libs.plugins.hilt) apply false
11 | alias(libs.plugins.ksp) apply false
12 | alias(libs.plugins.play) apply false
13 | alias(libs.plugins.baselineprofile) apply false
14 | alias(libs.plugins.ktlint)
15 | alias(libs.plugins.versions)
16 | alias(libs.plugins.versionCatalogUpdate)
17 | }
18 |
19 | tasks.withType {
20 | checkForGradleUpdate = true
21 | outputFormatter = "json"
22 | outputDir = "build/dependencyUpdates"
23 | reportfileName = "report"
24 |
25 | rejectVersionIf {
26 | isNonStable(candidate.version) && !isNonStable(currentVersion)
27 | }
28 | }
29 |
30 | // Taken from https://github.com/ben-manes/gradle-versions-plugin#rejectversionsif-and-componentselection
31 | private fun isNonStable(version: String): Boolean {
32 | val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) }
33 | val regex = "^[0-9,.v-]+(-r)?$".toRegex()
34 | val isStable = stableKeyword || regex.matches(version)
35 | return isStable.not()
36 | }
37 |
--------------------------------------------------------------------------------
/core/common/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.filenavigator.library)
3 | alias(libs.plugins.filenavigator.hilt)
4 | alias(libs.plugins.kotlin.parcelize)
5 | }
6 |
7 | dependencies {
8 | implementation(libs.w2sv.androidutils.core)
9 | implementation(libs.w2sv.kotlinutils)
10 | implementation(libs.w2sv.simplestorage)
11 | implementation(libs.slimber)
12 |
13 | implementation(libs.androidx.core)
14 |
15 | // Unit tests
16 | testImplementation(projects.core.test)
17 | }
18 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/AppUrl.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common
2 |
3 | object AppUrl {
4 | const val LICENSE = "https://github.com/w2sv/FileNavigator/blob/main/LICENSE"
5 | const val PRIVACY_POLICY = "https://github.com/w2sv/FileNavigator/blob/main/PRIVACY-POLICY.md"
6 | const val GITHUB_REPOSITORY = "https://github.com/w2sv/FileNavigator"
7 | const val CREATE_ISSUE = "https://github.com/w2sv/FileNavigator/issues/new"
8 | const val GOOGLE_PLAY_DEVELOPER_PAGE =
9 | "https://play.google.com/store/apps/dev?id=6884111703871536890"
10 | const val DONATE = "https://buymeacoffee.com/w2sv"
11 | const val PLAYSTORE_LISTING = "https://play.google.com/store/apps/details?id=com.w2sv.filenavigator"
12 | }
13 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/di/AppDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.di
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Qualifier
6 | @Retention(AnnotationRetention.RUNTIME)
7 | annotation class GlobalScope(val appDispatcher: AppDispatcher)
8 |
9 | enum class AppDispatcher {
10 | Default,
11 | IO
12 | }
13 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/di/CommonModule.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.di
2 |
3 | import android.content.Context
4 | import android.os.PowerManager
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import dagger.hilt.components.SingletonComponent
10 | import javax.inject.Singleton
11 | import kotlinx.coroutines.CoroutineScope
12 | import kotlinx.coroutines.Dispatchers
13 |
14 | @InstallIn(SingletonComponent::class)
15 | @Module
16 | object CommonModule {
17 |
18 | @Provides
19 | @GlobalScope(AppDispatcher.Default)
20 | fun defaultScope(): CoroutineScope =
21 | CoroutineScope(Dispatchers.Default)
22 |
23 | @Provides
24 | @GlobalScope(AppDispatcher.IO)
25 | fun ioScope(): CoroutineScope =
26 | CoroutineScope(Dispatchers.IO)
27 |
28 | @Provides
29 | @Singleton
30 | fun powerManager(@ApplicationContext context: Context): PowerManager =
31 | context.getSystemService(PowerManager::class.java)
32 | }
33 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/util/ContentResolver.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import android.content.ContentResolver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.graphics.Bitmap
7 | import android.net.Uri
8 | import com.w2sv.androidutils.graphics.loadBitmap
9 | import com.w2sv.androidutils.hasPermission
10 | import java.io.FileNotFoundException
11 | import slimber.log.e
12 |
13 | fun ContentResolver.loadBitmapWithFileNotFoundHandling(uri: Uri): Bitmap? =
14 | try {
15 | loadBitmap(uri)
16 | } catch (e: FileNotFoundException) {
17 | e(e)
18 | null
19 | }
20 |
21 | /**
22 | * Remedies "Failed query: java.lang.SecurityException: Permission Denial: opening provider com.android.externalstorage.ExternalStorageProvider from ProcessRecord{6fc17ee 8097:com.w2sv.filenavigator.debug/u0a753} (pid=8097, uid=10753) requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs"
23 | */
24 | fun ContentResolver.takePersistableReadAndWriteUriPermission(treeUri: Uri) {
25 | takePersistableUriPermission(
26 | treeUri,
27 | Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
28 | )
29 | }
30 |
31 | fun Uri.hasReadAndWritePermission(context: Context): Boolean =
32 | hasPermission(context, Intent.FLAG_GRANT_READ_URI_PERMISSION) && hasPermission(
33 | context,
34 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
35 | )
36 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/util/DocumentFile.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import android.content.Context
4 | import androidx.documentfile.provider.DocumentFile
5 | import com.anggrayudi.storage.file.child
6 |
7 | fun DocumentFile.hasChild(
8 | context: Context,
9 | path: String,
10 | requiresWriteAccess: Boolean = false
11 | ): Boolean =
12 | child(context, path, requiresWriteAccess) != null
13 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/util/ExternalStorageManager.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Build
7 | import android.os.Environment
8 | import android.provider.Settings
9 | import androidx.annotation.RequiresApi
10 | import com.w2sv.androidutils.os.manageExternalStoragePermissionRequired
11 |
12 | @RequiresApi(Build.VERSION_CODES.R)
13 | fun goToManageExternalStorageSettings(context: Context) {
14 | context.startActivity(
15 | Intent(
16 | Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
17 | Uri.fromParts("package", context.packageName, null)
18 | )
19 | )
20 | }
21 |
22 | val isExternalStorageManger: Boolean
23 | get() = !manageExternalStoragePermissionRequired || Environment.isExternalStorageManager()
24 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/util/Formatting.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import java.text.NumberFormat
4 | import java.util.Locale
5 |
6 | fun String.removeSlashSuffix(): String =
7 | removeSuffix("/")
8 |
9 | fun String.slashPrefixed(): String =
10 | "/$this"
11 |
12 | fun String.lineBreakSuffixed(): String =
13 | "$this\n"
14 |
15 | fun String.colonSuffixed(): String =
16 | "$this:"
17 |
18 | fun formattedFileSize(bytes: Long, locale: Locale = Locale.getDefault()): String {
19 | if (bytes in -999..999) {
20 | return "$bytes B"
21 | }
22 | val dimensionPrefixIterator = "kMGTPE".iterator()
23 | var dimensionPrefix = dimensionPrefixIterator.next()
24 | var byteCount = bytes.toDouble()
25 | while (byteCount <= -999_950 || byteCount >= 999_950) {
26 | byteCount /= 1000
27 | dimensionPrefix = dimensionPrefixIterator.next()
28 | }
29 | val numberFormat = NumberFormat.getNumberInstance(locale).apply {
30 | maximumFractionDigits = 3
31 | }
32 | return "${numberFormat.format(byteCount / 1000)} ${dimensionPrefix}B"
33 | }
34 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/util/Logging.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import slimber.log.i
4 |
5 | inline fun T.log(makeMessage: (T) -> String = { it.toString() }): T =
6 | also { i { makeMessage(this) } }
7 |
8 | val Any.logIdentifier
9 | get() = this::class.java.simpleName
10 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/util/LoggingBroadcastReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import androidx.annotation.CallSuper
7 | import com.w2sv.androidutils.os.logString
8 | import slimber.log.i
9 |
10 | /**
11 | * A [BroadcastReceiver] that logs upon its [onReceive] being called.
12 | */
13 | abstract class LoggingBroadcastReceiver : BroadcastReceiver() {
14 |
15 | @CallSuper
16 | override fun onReceive(context: Context, intent: Intent) {
17 | i { "$logIdentifier.onReceive | ${intent.logString()}" }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/util/LoggingComponentActivity.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import slimber.log.i
6 |
7 | /**
8 | * A [ComponentActivity] that logs upon reaching its most important lifecycle states.
9 | */
10 | abstract class LoggingComponentActivity : ComponentActivity() {
11 |
12 | private val logIdentifier
13 | get() = this::class.simpleName
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | i { "$logIdentifier onCreate" }
18 | }
19 |
20 | override fun onStart() {
21 | super.onStart()
22 | i { "$logIdentifier onStart" }
23 | }
24 |
25 | override fun onResume() {
26 | super.onResume()
27 | i { "$logIdentifier onResume" }
28 | }
29 |
30 | override fun onPause() {
31 | super.onPause()
32 | i { "$logIdentifier onPause" }
33 | }
34 |
35 | override fun onStop() {
36 | super.onStop()
37 | i { "$logIdentifier onStop" }
38 | }
39 |
40 | override fun onDestroy() {
41 | super.onDestroy()
42 | i { "$logIdentifier onDestroy" }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/util/Map.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import com.w2sv.kotlinutils.filterToSet
4 |
5 | // TODO: kotlinutils
6 |
7 | fun Map.filterKeysByValue(predicate: (V) -> Boolean): List =
8 | keys.filter { predicate(getValue(it)) }
9 |
10 | fun Map.filterKeysByValueToSet(predicate: (V) -> Boolean): Set =
11 | keys.filterToSet { predicate(getValue(it)) }
12 |
13 | fun syncMapKeys(
14 | source: Map,
15 | target: MutableMap,
16 | valueOnAddedKeys: V
17 | ) {
18 | val addedKeys = source.keys - target.keys
19 | val removedKeys = target.keys - source.keys
20 |
21 | addedKeys.forEach { target[it] = valueOnAddedKeys }
22 | removedKeys.forEach { target.remove(it) }
23 | }
24 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/util/MediaId.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import android.content.ContentUris
4 | import android.net.Uri
5 | import slimber.log.e
6 |
7 | @JvmInline
8 | value class MediaId(val value: Long) {
9 |
10 | companion object {
11 | fun fromUri(uri: Uri): MediaId? {
12 | return try {
13 | val parsedId = ContentUris.parseId(uri)
14 | if (parsedId != -1L) {
15 | MediaId(parsedId)
16 | } else {
17 | null
18 | }
19 | } catch (e: Exception) {
20 | e { e.stackTraceToString() }
21 | null
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/util/MediaUri.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.os.Parcelable
6 | import android.provider.MediaStore
7 | import androidx.core.net.toUri
8 | import kotlinx.parcelize.Parcelize
9 |
10 | @Parcelize
11 | @JvmInline
12 | value class MediaUri(val uri: Uri) : Parcelable {
13 |
14 | fun documentUri(context: Context): DocumentUri? =
15 | MediaStore.getDocumentUri(context, uri)?.documentUri
16 |
17 | fun id(): MediaId? =
18 | MediaId.fromUri(uri)
19 |
20 | companion object {
21 | fun fromDocumentUri(context: Context, documentUri: DocumentUri): MediaUri? =
22 | MediaStore.getMediaUri(
23 | context,
24 | documentUri.uri
25 | )
26 | ?.mediaUri
27 |
28 | fun parse(uriString: String): MediaUri =
29 | uriString.toUri().mediaUri
30 | }
31 | }
32 |
33 | val Uri.mediaUri: MediaUri
34 | get() = MediaUri(this)
35 |
--------------------------------------------------------------------------------
/core/common/src/main/kotlin/com/w2sv/common/util/String.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | fun String.containsSpecialCharacter(): Boolean =
4 | any { !it.isLetterOrDigit() && it != ' ' }
5 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_apk_file_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_app_foreground_108.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_app_logo_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_app_monochrome_108.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_apps_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_audio_file_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_battery_low_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_book_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_bug_report_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_camera_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_cancel_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_contrast_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_copyright_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_custom_file_type_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
16 |
17 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_delete_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_delete_history_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_donate_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_file_download_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_files_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
14 |
15 |
21 |
22 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_folder_edit_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_folder_open_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_folder_zip_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_github_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_history_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_image_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_info_outline_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_mic_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_more_vert_24.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_nightlight_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_notifications_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_palette_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_pdf_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_policy_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_restart_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_screenshot_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_settings_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_share_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_smartphone_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_star_rate_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_start_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_stop_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_storage_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_subdirectory_arrow_right_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_text_file_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_video_file_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/common/src/main/res/drawable/ic_warning_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/core/common/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #00DFC2
4 | #00696E
5 |
--------------------------------------------------------------------------------
/core/common/src/test/kotlin/com/w2sv/common/util/MediaUriTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import com.w2sv.test.testParceling
4 | import junit.framework.TestCase.assertEquals
5 | import org.junit.Test
6 | import org.junit.runner.RunWith
7 | import org.robolectric.RobolectricTestRunner
8 |
9 | @RunWith(RobolectricTestRunner::class)
10 | internal class MediaUriTest {
11 |
12 | private val mediaUri = MediaUri.parse("content://media/external/images/media/1000012597")
13 |
14 | @Test
15 | fun testParceling() {
16 | mediaUri.testParceling()
17 | }
18 |
19 | @Test
20 | fun testId() {
21 | assertEquals(MediaId(value = 1000012597), mediaUri.id())
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/core/common/src/test/kotlin/com/w2sv/common/util/StringKtTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.common.util
2 |
3 | import junit.framework.TestCase.assertEquals
4 | import org.junit.Test
5 |
6 | class StringKtTest {
7 |
8 | @Test
9 | fun containsSpecialCharacter() {
10 | fun test(expected: Boolean, input: String) {
11 | assertEquals(expected, input.containsSpecialCharacter())
12 | }
13 |
14 | test(false, "")
15 | test(false, "sadfa")
16 | test(false, "sdafa6")
17 | test(false, "sfadfa sdafaxczv")
18 | test(true, ".")
19 | test(true, "sxczv.-cvx")
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/core/database/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/database/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.filenavigator.library)
3 | alias(libs.plugins.filenavigator.hilt)
4 | alias(libs.plugins.filenavigator.room)
5 | alias(libs.plugins.kotlin.parcelize)
6 | }
7 |
8 | android {
9 | sourceSets {
10 | // Adds exported schema location as test app assets.
11 | getByName("androidTest").assets.srcDir("$projectDir/schemas")
12 | }
13 | }
14 |
15 | dependencies {
16 | implementation(projects.core.common)
17 | implementation(projects.core.domain)
18 | implementation(libs.androidx.core)
19 | implementation(libs.w2sv.androidutils.core)
20 | implementation(libs.slimber)
21 | implementation(libs.w2sv.simplestorage)
22 |
23 | testImplementation(libs.bundles.unitTest)
24 |
25 | androidTestImplementation(libs.bundles.instrumentationTest)
26 | androidTestImplementation(libs.androidx.room.testing)
27 | }
28 |
--------------------------------------------------------------------------------
/core/database/schemas/com.w2sv.data.storage.database.AppDatabase/2.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 2,
5 | "identityHash": "820627affaef61c06cbda50dd76a8048",
6 | "entities": [
7 | {
8 | "tableName": "MoveEntryEntity",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileName` TEXT NOT NULL, `fileType` TEXT NOT NULL, `fileSourceKind` TEXT NOT NULL, `destinationDocumentUri` TEXT NOT NULL, `dateTime` TEXT NOT NULL, PRIMARY KEY(`dateTime`))",
10 | "fields": [
11 | {
12 | "fieldPath": "fileName",
13 | "columnName": "fileName",
14 | "affinity": "TEXT",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "fileType",
19 | "columnName": "fileType",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "fileSourceKind",
25 | "columnName": "fileSourceKind",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | },
29 | {
30 | "fieldPath": "destinationDocumentUri",
31 | "columnName": "destinationDocumentUri",
32 | "affinity": "TEXT",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "dateTime",
37 | "columnName": "dateTime",
38 | "affinity": "TEXT",
39 | "notNull": true
40 | }
41 | ],
42 | "primaryKey": {
43 | "autoGenerate": false,
44 | "columnNames": [
45 | "dateTime"
46 | ]
47 | },
48 | "indices": [],
49 | "foreignKeys": []
50 | }
51 | ],
52 | "views": [],
53 | "setupQueries": [
54 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
55 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '820627affaef61c06cbda50dd76a8048')"
56 | ]
57 | }
58 | }
--------------------------------------------------------------------------------
/core/database/schemas/com.w2sv.datastorage.database.AppDatabase/2.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 2,
5 | "identityHash": "820627affaef61c06cbda50dd76a8048",
6 | "entities": [
7 | {
8 | "tableName": "MoveEntryEntity",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileName` TEXT NOT NULL, `fileType` TEXT NOT NULL, `fileSourceKind` TEXT NOT NULL, `destinationDocumentUri` TEXT NOT NULL, `dateTime` TEXT NOT NULL, PRIMARY KEY(`dateTime`))",
10 | "fields": [
11 | {
12 | "fieldPath": "fileName",
13 | "columnName": "fileName",
14 | "affinity": "TEXT",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "fileType",
19 | "columnName": "fileType",
20 | "affinity": "TEXT",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "fileSourceKind",
25 | "columnName": "fileSourceKind",
26 | "affinity": "TEXT",
27 | "notNull": true
28 | },
29 | {
30 | "fieldPath": "destinationDocumentUri",
31 | "columnName": "destinationDocumentUri",
32 | "affinity": "TEXT",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "dateTime",
37 | "columnName": "dateTime",
38 | "affinity": "TEXT",
39 | "notNull": true
40 | }
41 | ],
42 | "primaryKey": {
43 | "autoGenerate": false,
44 | "columnNames": [
45 | "dateTime"
46 | ]
47 | },
48 | "indices": [],
49 | "foreignKeys": []
50 | }
51 | ],
52 | "views": [],
53 | "setupQueries": [
54 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
55 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '820627affaef61c06cbda50dd76a8048')"
56 | ]
57 | }
58 | }
--------------------------------------------------------------------------------
/core/database/src/main/kotlin/com/w2sv/database/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.database
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import androidx.room.TypeConverters
6 | import com.w2sv.database.dao.MovedFileDao
7 | import com.w2sv.database.entity.MovedFileEntity
8 | import com.w2sv.database.typeconverter.FileTypeConverter
9 | import com.w2sv.database.typeconverter.LocalDateTimeConverter
10 | import com.w2sv.database.typeconverter.UriConverter
11 |
12 | @Database(
13 | entities = [MovedFileEntity::class],
14 | version = 5,
15 | exportSchema = true
16 | )
17 | @TypeConverters(
18 | LocalDateTimeConverter::class,
19 | UriConverter::class,
20 | FileTypeConverter::class
21 | )
22 | internal abstract class AppDatabase : RoomDatabase() {
23 | abstract fun getMovedFileDao(): MovedFileDao
24 | }
25 |
--------------------------------------------------------------------------------
/core/database/src/main/kotlin/com/w2sv/database/dao/MovedFileDao.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.database.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 | import com.w2sv.database.entity.MovedFileEntity
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | @Dao
11 | internal interface MovedFileDao {
12 | @Query("SELECT * FROM MovedFileEntity ORDER BY moveDateTime DESC")
13 | fun loadAllInDescendingOrder(): Flow>
14 |
15 | @Insert
16 | fun insert(entry: MovedFileEntity)
17 |
18 | @Query("DELETE FROM MovedFileEntity")
19 | fun deleteAll()
20 |
21 | @Delete
22 | fun delete(entry: MovedFileEntity)
23 | }
24 |
--------------------------------------------------------------------------------
/core/database/src/main/kotlin/com/w2sv/database/di/DataBaseBinderModule.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.database.di
2 |
3 | import com.w2sv.database.repository.RoomMovedFileRepository
4 | import com.w2sv.domain.repository.MovedFileRepository
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 |
10 | @Module
11 | @InstallIn(SingletonComponent::class)
12 | internal interface DataBaseBinderModule {
13 |
14 | @Binds
15 | fun bindsMoveEntryRepository(impl: RoomMovedFileRepository): MovedFileRepository
16 | }
17 |
--------------------------------------------------------------------------------
/core/database/src/main/kotlin/com/w2sv/database/di/DatabaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.database.di
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import com.w2sv.database.AppDatabase
6 | import com.w2sv.database.dao.MovedFileDao
7 | import com.w2sv.database.migration.Migrations
8 | import dagger.Module
9 | import dagger.Provides
10 | import dagger.hilt.InstallIn
11 | import dagger.hilt.android.qualifiers.ApplicationContext
12 | import dagger.hilt.components.SingletonComponent
13 | import javax.inject.Singleton
14 |
15 | @InstallIn(SingletonComponent::class)
16 | @Module
17 | internal object DatabaseModule {
18 |
19 | @Singleton
20 | @Provides
21 | fun appDatabase(@ApplicationContext context: Context): AppDatabase =
22 | Room
23 | .databaseBuilder(
24 | context,
25 | AppDatabase::class.java,
26 | "app-database"
27 | )
28 | .addMigrations(
29 | Migrations.Migration2to3(context = context),
30 | Migrations.Migration3to4,
31 | Migrations.Migration4to5
32 | )
33 | .fallbackToDestructiveMigration(true)
34 | .build()
35 |
36 | @Provides
37 | fun moveEntryDao(appDatabase: AppDatabase): MovedFileDao =
38 | appDatabase.getMovedFileDao()
39 | }
40 |
--------------------------------------------------------------------------------
/core/database/src/main/kotlin/com/w2sv/database/repository/RoomMovedFileRepository.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.database.repository
2 |
3 | import com.w2sv.database.dao.MovedFileDao
4 | import com.w2sv.database.entity.MovedFileEntity
5 | import com.w2sv.domain.model.MovedFile
6 | import com.w2sv.domain.repository.MovedFileRepository
7 | import javax.inject.Inject
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.map
10 |
11 | internal class RoomMovedFileRepository @Inject constructor(
12 | private val movedFileDao: MovedFileDao
13 | ) : MovedFileRepository {
14 |
15 | override suspend fun insert(file: MovedFile) {
16 | movedFileDao.insert(MovedFileEntity(file))
17 | }
18 |
19 | override suspend fun delete(file: MovedFile) {
20 | movedFileDao.delete(MovedFileEntity(file))
21 | }
22 |
23 | override suspend fun deleteAll() {
24 | movedFileDao.deleteAll()
25 | }
26 |
27 | override fun getAllInDescendingOrder(): Flow> =
28 | movedFileDao
29 | .loadAllInDescendingOrder()
30 | .map { it.map { entity -> entity.asExternal() } }
31 | }
32 |
--------------------------------------------------------------------------------
/core/database/src/main/kotlin/com/w2sv/database/typeconverter/LocalDateTimeConverter.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.database.typeconverter
2 |
3 | import androidx.room.TypeConverter
4 | import java.time.LocalDateTime
5 |
6 | internal object LocalDateTimeConverter {
7 |
8 | @TypeConverter
9 | fun toDate(dateString: String): LocalDateTime =
10 | LocalDateTime.parse(dateString)
11 |
12 | @TypeConverter
13 | fun toDateString(date: LocalDateTime): String =
14 | date.toString()
15 | }
16 |
--------------------------------------------------------------------------------
/core/database/src/main/kotlin/com/w2sv/database/typeconverter/UriConverter.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.database.typeconverter
2 |
3 | import android.net.Uri
4 | import androidx.core.net.toUri
5 | import androidx.room.TypeConverter
6 |
7 | internal object UriConverter {
8 |
9 | @TypeConverter
10 | fun fromUri(uri: Uri?): String {
11 | return uri?.toString() ?: ""
12 | }
13 |
14 | @TypeConverter
15 | fun toUri(uriString: String): Uri? {
16 | return if (uriString.isNotEmpty()) uriString.toUri() else null
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/core/database/src/test/kotlin/com/w2sv/database/typeconverter/FileTypeConverterTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.database.typeconverter
2 |
3 | import com.w2sv.domain.model.filetype.CustomFileType
4 | import com.w2sv.domain.model.filetype.FileType
5 | import com.w2sv.domain.model.filetype.PresetFileType
6 | import junit.framework.TestCase.assertEquals
7 | import org.junit.Test
8 |
9 | internal class FileTypeConverterTest {
10 |
11 | @Test
12 | fun testBackAndForthPresetFileTypeConversion() {
13 | PresetFileType.values.forEach {
14 | assertEquals(
15 | it,
16 | it.toDefaultFileType().backAndForthConverted().wrappedPresetTypeOrNull
17 | )
18 | }
19 | }
20 |
21 | @Test
22 | fun testBackAndForthCustomFileTypeConversion() {
23 | val customFileType = CustomFileType("Html", emptyList(), 342523, 1006)
24 | val recreatedFileType = customFileType.backAndForthConverted() as CustomFileType
25 |
26 | assertEquals(customFileType.name, recreatedFileType.name)
27 | assertEquals(customFileType.colorInt, recreatedFileType.colorInt)
28 | assertEquals(customFileType.ordinal, recreatedFileType.ordinal)
29 | }
30 | }
31 |
32 | private fun FileType.backAndForthConverted(): FileType =
33 | FileTypeConverter.toFileType(FileTypeConverter.fromFileType(this))
34 |
--------------------------------------------------------------------------------
/core/datastore/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/datastore/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.google.protobuf.gradle.id
2 |
3 | plugins {
4 | alias(libs.plugins.filenavigator.library)
5 | alias(libs.plugins.filenavigator.hilt)
6 | alias(libs.plugins.protobuf)
7 | alias(libs.plugins.kotlin.parcelize)
8 | }
9 |
10 | android {
11 | defaultConfig {
12 | consumerProguardFiles("consumer-proguard-rules.pro")
13 | }
14 | }
15 |
16 | // Setup protobuf configuration, generating lite Java and Kotlin classes
17 | protobuf {
18 | protoc {
19 | artifact = libs.protobuf.protoc.get().toString()
20 | }
21 | generateProtoTasks {
22 | all().forEach { task ->
23 | task.builtins {
24 | register("java") {
25 | option("lite")
26 | }
27 | id("kotlin") // Enables kotlin DSL
28 | }
29 | }
30 | }
31 | }
32 |
33 | dependencies {
34 | implementation(projects.core.common)
35 | implementation(projects.core.domain)
36 |
37 | implementation(libs.androidx.core)
38 | implementation(libs.protobuf.kotlin.lite)
39 |
40 | implementation(libs.w2sv.kotlinutils)
41 | implementation(libs.w2sv.datastoreutils.preferences)
42 | implementation(libs.w2sv.datastoreutils.datastoreflow)
43 | implementation(libs.w2sv.androidutils.core)
44 | implementation(libs.slimber)
45 |
46 | testImplementation(projects.core.test)
47 | }
48 |
--------------------------------------------------------------------------------
/core/datastore/consumer-proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Prevent Proto DataStore fields from being deleted
2 | -keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
3 | ;
4 | }
--------------------------------------------------------------------------------
/core/datastore/src/main/kotlin/com/w2sv/datastore/di/DataStoreBinderModule.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.datastore.di
2 |
3 | import com.w2sv.datastore.preferences.PreferencesRepositoryImpl
4 | import com.w2sv.datastore.proto.navigatorconfig.NavigatorConfigDataSourceImpl
5 | import com.w2sv.domain.repository.NavigatorConfigDataSource
6 | import com.w2sv.domain.repository.PreferencesRepository
7 | import dagger.Binds
8 | import dagger.Module
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 |
12 | @InstallIn(SingletonComponent::class)
13 | @Module
14 | internal interface DataStoreBinderModule {
15 |
16 | @Binds
17 | fun bindsNavigatorConfigDataSource(impl: NavigatorConfigDataSourceImpl): NavigatorConfigDataSource
18 |
19 | @Binds
20 | fun bindsPreferencesRepository(impl: PreferencesRepositoryImpl): PreferencesRepository
21 | }
22 |
--------------------------------------------------------------------------------
/core/datastore/src/main/kotlin/com/w2sv/datastore/proto/ProtoMapper.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.datastore.proto
2 |
3 | internal interface ProtoMapper {
4 | fun toExternal(proto: Proto): External
5 | fun toProto(external: External): Proto
6 | }
7 |
--------------------------------------------------------------------------------
/core/datastore/src/main/kotlin/com/w2sv/datastore/proto/navigatorconfig/NavigatorConfigProtoSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.datastore.proto.navigatorconfig
2 |
3 | import androidx.datastore.core.CorruptionException
4 | import androidx.datastore.core.Serializer
5 | import com.google.protobuf.InvalidProtocolBufferException
6 | import com.w2sv.datastore.NavigatorConfigProto
7 | import com.w2sv.domain.model.navigatorconfig.NavigatorConfig
8 | import java.io.InputStream
9 | import java.io.OutputStream
10 |
11 | internal object NavigatorConfigProtoSerializer : Serializer {
12 | override val defaultValue: NavigatorConfigProto by lazy {
13 | NavigatorConfig.default.toProto(false)
14 | }
15 |
16 | override suspend fun readFrom(input: InputStream): NavigatorConfigProto =
17 | try {
18 | // readFrom is already called on the data store background thread
19 | NavigatorConfigProto.parseFrom(input)
20 | } catch (exception: InvalidProtocolBufferException) {
21 | throw CorruptionException("Cannot read proto.", exception)
22 | }
23 |
24 | override suspend fun writeTo(t: NavigatorConfigProto, output: OutputStream) {
25 | // writeTo is already called on the data store background thread
26 | t.writeTo(output)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/core/datastore/src/main/proto/navigator_config.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_package = "com.w2sv.datastore";
4 | option java_multiple_files = true;
5 |
6 | message NavigatorConfigProto {
7 | map file_type_to_config = 1;
8 | map extension_preset_file_types = 6;
9 | map extension_configurable_file_types = 7;
10 | map custom_file_types = 8;
11 | bool disable_on_low_battery = 2;
12 | bool start_on_boot = 3;
13 | bool has_been_migrated = 4;
14 | bool show_batch_move_notification = 5;
15 | }
16 |
17 | message ExtensionPresetFileTypeProto {
18 | int32 color = 1;
19 | }
20 |
21 | message ExtensionConfigurableFileTypeProto {
22 | int32 color = 1;
23 | repeated string excluded_extensions = 2;
24 | }
25 |
26 | message CustomFileTypeProto {
27 | string name = 1;
28 | repeated string extensions = 2;
29 | int32 color = 3;
30 | int32 ordinal = 4;
31 | }
32 |
33 | message FileTypeConfigProto {
34 | bool enabled = 1;
35 | map source_type_to_config = 2;
36 | }
37 |
38 | message SourceConfigProto {
39 | bool enabled = 1;
40 | repeated string last_move_destinations = 2;
41 | AutoMoveConfigProto auto_move_config = 3;
42 | }
43 |
44 | message AutoMoveConfigProto {
45 | bool enabled = 1;
46 | string destination = 2;
47 | }
48 |
--------------------------------------------------------------------------------
/core/datastore/src/test/kotlin/com/w2sv/datastore/migration/PreMigrationNavigatorPreferencesKeyTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.datastore.migration
2 |
3 | import junit.framework.TestCase.assertEquals
4 | import org.junit.Test
5 |
6 | class PreMigrationNavigatorPreferencesKeyTest {
7 |
8 | @Test
9 | fun keys() {
10 | assertEquals(
11 | "[disableNavigatorOnLowBattery, Image, Image.Camera.IS_ENABLED, Image.Camera.LAST_MOVE_DESTINATION, " +
12 | "Image.Screenshot.IS_ENABLED, Image.Screenshot.LAST_MOVE_DESTINATION, Image.OtherApp.IS_ENABLED, " +
13 | "Image.OtherApp.LAST_MOVE_DESTINATION, Image.Download.IS_ENABLED, Image.Download.LAST_MOVE_DESTINATION, " +
14 | "Video, Video.Camera.IS_ENABLED, Video.Camera.LAST_MOVE_DESTINATION, Video.OtherApp.IS_ENABLED, " +
15 | "Video.OtherApp.LAST_MOVE_DESTINATION, Video.Download.IS_ENABLED, Video.Download.LAST_MOVE_DESTINATION, " +
16 | "Audio, Audio.Recording.IS_ENABLED, Audio.Recording.LAST_MOVE_DESTINATION, Audio.OtherApp.IS_ENABLED, " +
17 | "Audio.OtherApp.LAST_MOVE_DESTINATION, Audio.Download.IS_ENABLED, Audio.Download.LAST_MOVE_DESTINATION, " +
18 | "PDF, PDF.Download.IS_ENABLED, PDF.Download.LAST_MOVE_DESTINATION, Text, Text.Download.IS_ENABLED," +
19 | " Text.Download.LAST_MOVE_DESTINATION, Archive, Archive.Download.IS_ENABLED, Archive.Download.LAST_MOVE_DESTINATION, " +
20 | "APK, APK.Download.IS_ENABLED, APK.Download.LAST_MOVE_DESTINATION, EBook, EBook.Download.IS_ENABLED," +
21 | " EBook.Download.LAST_MOVE_DESTINATION]",
22 | PreMigrationNavigatorPreferencesKey.keys().toList().toString()
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/core/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/domain/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.filenavigator.library)
3 | alias(libs.plugins.filenavigator.hilt)
4 | alias(libs.plugins.kotlin.parcelize)
5 | }
6 |
7 | dependencies {
8 | implementation(projects.core.common)
9 |
10 | api(libs.w2sv.datastoreutils.datastoreflow)
11 | implementation(libs.w2sv.androidutils.core)
12 | implementation(libs.w2sv.kotlinutils)
13 | implementation(libs.slimber)
14 | implementation(libs.w2sv.simplestorage)
15 | implementation(libs.androidx.core)
16 |
17 | testImplementation(projects.core.test)
18 | }
19 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model
2 |
3 | enum class Theme {
4 | Light,
5 | Default,
6 | Dark
7 | }
8 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/filetype/CustomFileType.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.filetype
2 |
3 | import android.content.Context
4 | import android.graphics.Color
5 | import android.os.Parcelable
6 | import androidx.annotation.ColorInt
7 | import androidx.annotation.DrawableRes
8 | import androidx.annotation.VisibleForTesting
9 | import com.w2sv.core.common.R
10 | import kotlin.random.Random
11 | import kotlinx.parcelize.IgnoredOnParcel
12 | import kotlinx.parcelize.Parcelize
13 |
14 | @Parcelize
15 | data class CustomFileType(
16 | val name: String,
17 | override val fileExtensions: List,
18 | @ColorInt override val colorInt: Int,
19 | override val ordinal: Int
20 | ) : StaticFileType.NonMedia,
21 | FileType,
22 | Parcelable {
23 |
24 | @IgnoredOnParcel
25 | @DrawableRes
26 | override val iconRes: Int = R.drawable.ic_custom_file_type_24
27 |
28 | override fun label(context: Context): String =
29 | name
30 |
31 | companion object {
32 | fun newEmpty(existingFileTypes: Collection): CustomFileType =
33 | CustomFileType(
34 | name = "",
35 | fileExtensions = emptyList(),
36 | colorInt = randomColor(),
37 | ordinal = maxOf(MIN_ORDINAL, existingFileTypes.maxOfOrNull { it.ordinal }?.let { it + 1 } ?: MIN_ORDINAL)
38 | )
39 |
40 | @VisibleForTesting
41 | internal const val MIN_ORDINAL = 1_000
42 | }
43 | }
44 |
45 | @ColorInt
46 | private fun randomColor(): Int =
47 | Color.rgb(
48 | Random.Default.nextInt(256),
49 | Random.Default.nextInt(256),
50 | Random.Default.nextInt(256)
51 | )
52 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/filetype/FileAndSourceType.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.filetype
2 |
3 | import android.content.Context
4 | import android.os.Parcelable
5 | import androidx.annotation.DrawableRes
6 | import com.w2sv.core.common.R
7 | import kotlinx.parcelize.IgnoredOnParcel
8 | import kotlinx.parcelize.Parcelize
9 |
10 | @Parcelize
11 | data class FileAndSourceType(val fileType: FileType, val sourceType: SourceType) : Parcelable {
12 |
13 | @IgnoredOnParcel
14 | @get:DrawableRes
15 | val iconRes: Int by lazy {
16 | when {
17 | sourceType in listOf(SourceType.Screenshot, SourceType.Camera, SourceType.Recording) -> sourceType.iconRes
18 | else -> fileType.iconRes
19 | }
20 | }
21 |
22 | /**
23 | * @return
24 | * - Gif -> 'GIF'
25 | * - Photo -> 'Photo'
26 | * - Screenshot, Recording -> sourceTypeLabel
27 | * - Download -> '{fileTypeLabel} Download'
28 | * - else -> fileTypeLabel
29 | */
30 | fun label(context: Context, isGif: Boolean): String =
31 | when {
32 | isGif -> context.getString(R.string.gif)
33 | fileType.wrappedPresetTypeOrNull is PresetFileType.Image && sourceType == SourceType.Camera -> context.getString(
34 | R.string.photo
35 | )
36 | sourceType == SourceType.Screenshot || sourceType == SourceType.Recording -> context.getString(
37 | sourceType.labelRes
38 | )
39 |
40 | fileType is CustomFileType -> fileType.name
41 |
42 | sourceType == SourceType.Download -> context.getString(
43 | R.string.file_type_download,
44 | context.getString((fileType as AnyPresetWrappingFileType).presetFileType.labelRes)
45 | )
46 |
47 | else -> context.getString((fileType as AnyPresetWrappingFileType).presetFileType.labelRes)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/filetype/FileType.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.filetype
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.IgnoredOnParcel
5 |
6 | sealed interface FileType : StaticFileType.ExtensionSet, Parcelable {
7 | val colorInt: Int
8 |
9 | @IgnoredOnParcel
10 | val asExtensionConfigurableTypeOrNull: PresetWrappingFileType.ExtensionConfigurable?
11 | get() = this as? PresetWrappingFileType.ExtensionConfigurable
12 |
13 | @IgnoredOnParcel
14 | val wrappedPresetTypeOrNull: PresetFileType?
15 | get() = (this as? PresetWrappingFileType<*>)?.presetFileType
16 |
17 | @IgnoredOnParcel
18 | val isMediaType: Boolean
19 | get() = wrappedPresetTypeOrNull is PresetFileType.Media
20 | }
21 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/filetype/SourceType.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.filetype
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.StringRes
5 | import com.w2sv.core.common.R
6 |
7 | enum class SourceType(
8 | @StringRes val labelRes: Int,
9 | @DrawableRes val iconRes: Int
10 | ) {
11 | Camera(
12 | R.string.camera,
13 | R.drawable.ic_camera_24
14 | ),
15 | Screenshot(
16 | R.string.screenshot,
17 | R.drawable.ic_screenshot_24
18 | ),
19 | Recording(
20 | R.string.recording,
21 | R.drawable.ic_mic_24
22 | ),
23 | Download(
24 | R.string.download,
25 | R.drawable.ic_file_download_24
26 | ),
27 | OtherApp(
28 | R.string.other_app,
29 | R.drawable.ic_apps_24
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/filetype/StaticFileType.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.filetype
2 |
3 | import android.content.Context
4 | import com.anggrayudi.storage.media.MediaType
5 | import com.w2sv.domain.model.navigatorconfig.FileTypeConfig
6 | import com.w2sv.domain.model.navigatorconfig.SourceConfig
7 |
8 | interface StaticFileType {
9 | val mediaType: MediaType
10 | val sourceTypes: List
11 | val ordinal: Int
12 | val iconRes: Int
13 |
14 | fun label(context: Context): String
15 |
16 | fun defaultConfig(enabled: Boolean = true): FileTypeConfig =
17 | FileTypeConfig(
18 | enabled = enabled,
19 | sourceTypeConfigMap = sourceTypes.associateWith { SourceConfig() }
20 | )
21 |
22 | interface ExtensionSet : StaticFileType {
23 | val fileExtensions: Collection
24 | }
25 |
26 | interface ExtensionConfigurable : StaticFileType {
27 | val defaultFileExtensions: Set
28 | }
29 |
30 | sealed interface NonMedia : StaticFileType {
31 | override val mediaType get() = MediaType.DOWNLOADS
32 | override val sourceTypes get() = listOf(SourceType.Download)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/movedestination/ExternalDestination.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.movedestination
2 |
3 | import android.content.Context
4 | import com.w2sv.common.util.DocumentUri
5 | import com.w2sv.core.common.R
6 |
7 | data class ExternalDestination(
8 | override val documentUri: DocumentUri,
9 | override val providerPackageName: String?,
10 | override val providerAppLabel: String?
11 | ) : ExternalDestinationApi, FileDestinationApi {
12 |
13 | override fun uiRepresentation(context: Context): String {
14 | return providerAppLabel
15 | ?: documentUri.uri.authority
16 | ?: context.getString(R.string.unrecognized_destination)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/movedestination/ExternalDestinationApi.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.movedestination
2 |
3 | interface ExternalDestinationApi : MoveDestinationApi {
4 | val providerAppLabel: String?
5 | val providerPackageName: String?
6 | }
7 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/movedestination/FileDestinationApi.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.movedestination
2 |
3 | import android.content.Context
4 |
5 | interface FileDestinationApi : MoveDestinationApi {
6 | override fun fileName(context: Context): String =
7 | documentFile(context).name!! // TODO
8 | }
9 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/movedestination/LocalDestinationApi.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.movedestination
2 |
3 | import android.content.Context
4 |
5 | interface LocalDestinationApi : MoveDestinationApi {
6 | val isVolumeRoot: Boolean
7 |
8 | fun pathRepresentation(context: Context, includeVolumeName: Boolean): String
9 | }
10 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/movedestination/MoveDestinationApi.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.movedestination
2 |
3 | import android.content.Context
4 | import androidx.documentfile.provider.DocumentFile
5 | import com.w2sv.common.util.DocumentUri
6 |
7 | interface MoveDestinationApi {
8 | val documentUri: DocumentUri
9 | fun fileName(context: Context): String
10 |
11 | fun uiRepresentation(context: Context): String
12 |
13 | /**
14 | * @see DocumentFile.fromSingleUri
15 | */
16 | fun documentFile(context: Context): DocumentFile =
17 | documentUri.documentFile(context)
18 | }
19 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/navigatorconfig/AutoMoveConfig.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.navigatorconfig
2 |
3 | import com.w2sv.domain.model.movedestination.LocalDestinationApi
4 |
5 | data class AutoMoveConfig(
6 | val enabled: Boolean,
7 | val destination: LocalDestinationApi?
8 | ) {
9 | companion object {
10 | val Empty = AutoMoveConfig(enabled = false, destination = null)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/navigatorconfig/FileTypeConfig.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.navigatorconfig
2 |
3 | import com.w2sv.domain.model.filetype.SourceType
4 |
5 | typealias SourceTypeConfigMap = Map
6 |
7 | data class FileTypeConfig(
8 | val enabled: Boolean,
9 | val sourceTypeConfigMap: SourceTypeConfigMap
10 | )
11 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/model/navigatorconfig/SourceConfig.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.navigatorconfig
2 |
3 | import com.w2sv.domain.model.movedestination.LocalDestinationApi
4 |
5 | data class SourceConfig(
6 | val enabled: Boolean = true,
7 | val quickMoveDestinations: List = emptyList(),
8 | val autoMoveConfig: AutoMoveConfig = AutoMoveConfig.Empty
9 | )
10 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/repository/MovedFileRepository.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.repository
2 |
3 | import com.w2sv.domain.model.MovedFile
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface MovedFileRepository {
7 | suspend fun insert(file: MovedFile)
8 | fun getAllInDescendingOrder(): Flow>
9 | suspend fun delete(file: MovedFile)
10 | suspend fun deleteAll()
11 | }
12 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/repository/NavigatorConfigDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.repository
2 |
3 | import com.w2sv.datastoreutils.datastoreflow.DataStoreFlow
4 | import com.w2sv.domain.model.filetype.FileType
5 | import com.w2sv.domain.model.filetype.SourceType
6 | import com.w2sv.domain.model.movedestination.LocalDestinationApi
7 | import com.w2sv.domain.model.navigatorconfig.NavigatorConfig
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | interface NavigatorConfigDataSource {
11 | val navigatorConfig: DataStoreFlow
12 |
13 | // ==================
14 | // Auto move
15 | // ==================
16 |
17 | suspend fun unsetAutoMoveConfig(fileType: FileType, sourceType: SourceType)
18 |
19 | // ==================
20 | // Quick move
21 | // ==================
22 |
23 | suspend fun saveQuickMoveDestination(
24 | fileType: FileType,
25 | sourceType: SourceType,
26 | destination: LocalDestinationApi
27 | )
28 |
29 | suspend fun unsetQuickMoveDestination(fileType: FileType, sourceType: SourceType)
30 |
31 | fun quickMoveDestinations(fileType: FileType, sourceType: SourceType): Flow>
32 | }
33 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/repository/PreferencesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.repository
2 |
3 | import com.w2sv.datastoreutils.datastoreflow.DataStoreFlow
4 | import com.w2sv.datastoreutils.datastoreflow.DataStoreStateFlow
5 | import com.w2sv.domain.model.Theme
6 |
7 | interface PreferencesRepository {
8 | val theme: DataStoreFlow
9 | val useAmoledBlackTheme: DataStoreFlow
10 | val useDynamicColors: DataStoreFlow
11 | val postNotificationsPermissionRequested: DataStoreFlow
12 | val showStorageVolumeNames: DataStoreFlow
13 | val showAutoMoveIntroduction: DataStoreFlow
14 | val showQuickMovePermissionQueryExplanation: DataStoreStateFlow
15 | }
16 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/usecase/InsertMovedFileUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.usecase
2 |
3 | import com.w2sv.domain.model.MovedFile
4 | import com.w2sv.domain.repository.MovedFileRepository
5 | import javax.inject.Inject
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.withContext
8 |
9 | class InsertMovedFileUseCase @Inject constructor(private val movedFileRepository: MovedFileRepository) {
10 | suspend operator fun invoke(movedFile: MovedFile) {
11 | withContext(Dispatchers.IO) {
12 | movedFileRepository.insert(movedFile)
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/core/domain/src/main/kotlin/com/w2sv/domain/usecase/MoveDestinationPathConverter.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.usecase
2 |
3 | import android.content.Context
4 | import com.w2sv.common.di.AppDispatcher
5 | import com.w2sv.common.di.GlobalScope
6 | import com.w2sv.domain.model.movedestination.ExternalDestinationApi
7 | import com.w2sv.domain.model.movedestination.LocalDestinationApi
8 | import com.w2sv.domain.model.movedestination.MoveDestinationApi
9 | import com.w2sv.domain.repository.PreferencesRepository
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 | import kotlinx.coroutines.CoroutineScope
13 | import kotlinx.coroutines.flow.SharingStarted
14 |
15 | @Singleton
16 | class MoveDestinationPathConverter @Inject constructor(
17 | preferencesRepository: PreferencesRepository,
18 | @GlobalScope(AppDispatcher.Default) scope: CoroutineScope
19 | ) {
20 | private val showStorageVolumeNames =
21 | preferencesRepository.showStorageVolumeNames.stateIn(scope, SharingStarted.Eagerly)
22 |
23 | operator fun invoke(moveDestination: MoveDestinationApi, context: Context): String =
24 | when (moveDestination) {
25 | is LocalDestinationApi -> {
26 | moveDestination.pathRepresentation(
27 | context = context,
28 | includeVolumeName = showStorageVolumeNames.value
29 | )
30 | }
31 |
32 | is ExternalDestinationApi -> {
33 | moveDestination.uiRepresentation(
34 | context
35 | )
36 | }
37 |
38 | else -> throw IllegalArgumentException()
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/core/domain/src/test/kotlin/com/w2sv/domain/model/filetype/CustomFileTypeTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.filetype
2 |
3 | import com.w2sv.test.testParceling
4 | import junit.framework.TestCase.assertEquals
5 | import org.junit.Test
6 | import org.junit.runner.RunWith
7 | import org.robolectric.RobolectricTestRunner
8 |
9 | @RunWith(RobolectricTestRunner::class)
10 | class CustomFileTypeTest {
11 |
12 | @Test
13 | fun testParceling() {
14 | CustomFileType(
15 | name = "Html",
16 | fileExtensions = listOf("html"),
17 | colorInt = 2134124,
18 | ordinal = 1004
19 | )
20 | .testParceling()
21 | }
22 |
23 | @Test
24 | fun testNewEmpty() {
25 | fun test(existingOrdinals: List, expectedOrdinal: Int) {
26 | assertEquals(
27 | expectedOrdinal,
28 | CustomFileType.newEmpty(
29 | existingOrdinals.map {
30 | CustomFileType(
31 | name = "",
32 | fileExtensions = emptyList(),
33 | colorInt = -1,
34 | ordinal = it
35 | )
36 | }
37 | ).ordinal
38 | )
39 | }
40 |
41 | test(listOf(34, 0, 1, 3, 1003, 1007), 1008)
42 | test(listOf(0, 1, 3), CustomFileType.MIN_ORDINAL)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/core/domain/src/test/kotlin/com/w2sv/domain/model/filetype/FileAndSourceTypeTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.filetype
2 |
3 | import com.w2sv.test.testParceling
4 | import org.junit.Test
5 | import org.junit.runner.RunWith
6 | import org.robolectric.RobolectricTestRunner
7 |
8 | @RunWith(RobolectricTestRunner::class)
9 | class FileAndSourceTypeTest {
10 |
11 | @Test
12 | fun `test parcelling`() {
13 | FileAndSourceType(PresetFileType.Image.toFileType(), SourceType.Screenshot).testParceling()
14 | FileAndSourceType(PresetFileType.Image.toFileType(color = 78325), SourceType.Screenshot).testParceling()
15 | FileAndSourceType(PresetFileType.EBook.toFileType(color = 234453, setOf("sdasf", "xscvs")), SourceType.Download).testParceling()
16 | FileAndSourceType(CustomFileType("Custom", listOf("ext"), 2345213, 1008), SourceType.Download).testParceling()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/core/domain/src/test/kotlin/com/w2sv/domain/model/filetype/PresetFileTypeTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.domain.model.filetype
2 |
3 | import junit.framework.TestCase.assertEquals
4 | import org.junit.Test
5 |
6 | class PresetFileTypeTest {
7 |
8 | @Test
9 | fun testOrdinalsMap() {
10 | assertEquals(
11 | "{Image=0, Video=1, Audio=2, PDF=3, Text=4, Archive=5, APK=6, EBook=7}",
12 | PresetFileType.ordinalsMap.toString()
13 | )
14 | }
15 |
16 | @Test
17 | fun testGet() {
18 | assertEquals(PresetFileType.Image, PresetFileType[0])
19 | assertEquals(PresetFileType.EBook, PresetFileType[7])
20 | }
21 |
22 | @Test
23 | fun testExtensionSetToFileType() {
24 | assertEquals(
25 | PresetWrappingFileType.ExtensionSet(PresetFileType.Image, PresetFileType.Image.defaultColorInt),
26 | PresetFileType.Image.toDefaultFileType()
27 | )
28 |
29 | assertEquals(
30 | PresetWrappingFileType.ExtensionSet(PresetFileType.Image, 342347),
31 | PresetFileType.Image.toFileType(342347)
32 | )
33 | }
34 |
35 | @Test
36 | fun testExtensionConfigurableToFileType() {
37 | assertEquals(
38 | PresetWrappingFileType.ExtensionConfigurable(
39 | presetFileType = PresetFileType.Archive,
40 | colorInt = PresetFileType.Archive.defaultColorInt,
41 | excludedExtensions = emptySet()
42 | ),
43 | PresetFileType.Archive.toDefaultFileType()
44 | )
45 |
46 | assertEquals(
47 | PresetWrappingFileType.ExtensionConfigurable(
48 | presetFileType = PresetFileType.Archive,
49 | colorInt = 124325,
50 | excludedExtensions = setOf("sdfa")
51 | ),
52 | PresetFileType.Archive.toFileType(124325, setOf("sdfa"))
53 | )
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/core/navigator/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/navigator/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.filenavigator.library)
3 | alias(libs.plugins.filenavigator.hilt)
4 | alias(libs.plugins.kotlin.parcelize)
5 | }
6 |
7 | android {
8 | buildFeatures {
9 | viewBinding = true
10 | }
11 | }
12 |
13 | dependencies {
14 | implementation(projects.core.domain)
15 | implementation(projects.core.common)
16 |
17 | implementation(libs.androidx.core)
18 | implementation(libs.androidx.activity)
19 | implementation(libs.androidx.appcompat)
20 |
21 | implementation(libs.w2sv.androidutils.core)
22 | implementation(libs.w2sv.kotlinutils)
23 | implementation(libs.slimber)
24 |
25 | implementation(libs.google.guava)
26 |
27 | implementation(libs.w2sv.simplestorage)
28 |
29 | // ==============
30 | // Test
31 | // ==============
32 |
33 | testImplementation(projects.core.test)
34 | }
35 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/FileNavigatorModule.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator
2 |
3 | import android.content.Context
4 | import com.w2sv.androidutils.isServiceRunning
5 | import com.w2sv.navigator.moving.model.MediaIdWithMediaType
6 | import com.w2sv.navigator.moving.model.MoveResult
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.qualifiers.ApplicationContext
11 | import dagger.hilt.components.SingletonComponent
12 | import javax.inject.Singleton
13 | import kotlinx.coroutines.channels.Channel
14 | import kotlinx.coroutines.flow.MutableSharedFlow
15 | import kotlinx.coroutines.flow.MutableStateFlow
16 | import kotlinx.coroutines.flow.SharedFlow
17 | import kotlinx.coroutines.flow.asSharedFlow
18 |
19 | internal typealias MoveResultChannel = Channel
20 |
21 | @InstallIn(SingletonComponent::class)
22 | @Module
23 | object FileNavigatorModule {
24 |
25 | @Singleton
26 | @Provides
27 | fun fileNavigatorIsRunning(@ApplicationContext context: Context): FileNavigator.IsRunning =
28 | FileNavigator.IsRunning(mutableStateFlow = MutableStateFlow(context.isServiceRunning()))
29 |
30 | @Singleton // TODO: ServiceScoped
31 | @Provides
32 | internal fun moveResultChannel(): MoveResultChannel = Channel(Channel.BUFFERED)
33 |
34 | @Singleton // TODO: ServiceScoped
35 | @Provides
36 | internal fun mutableBlacklistedMediaUriSharedFlow(): MutableSharedFlow =
37 | MutableSharedFlow()
38 |
39 | @Provides
40 | internal fun blacklistedMediaUriSharedFlow(
41 | mutableBlacklistedMediaUriSharedFlow: MutableSharedFlow
42 | ): SharedFlow =
43 | mutableBlacklistedMediaUriSharedFlow.asSharedFlow()
44 | }
45 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/moving/MoveBroadcastReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.moving
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import com.w2sv.common.di.AppDispatcher
7 | import com.w2sv.common.di.GlobalScope
8 | import com.w2sv.navigator.MoveResultChannel
9 | import com.w2sv.navigator.moving.model.AnyMoveBundle
10 | import com.w2sv.navigator.moving.model.MoveBundle
11 | import dagger.hilt.android.AndroidEntryPoint
12 | import javax.inject.Inject
13 | import kotlinx.coroutines.CoroutineScope
14 | import kotlinx.coroutines.launch
15 |
16 | @AndroidEntryPoint
17 | internal class MoveBroadcastReceiver : BroadcastReceiver() {
18 |
19 | @Inject
20 | lateinit var moveResultChannel: MoveResultChannel
21 |
22 | @Inject
23 | @GlobalScope(AppDispatcher.IO)
24 | lateinit var scope: CoroutineScope
25 |
26 | override fun onReceive(context: Context, intent: Intent) {
27 | val moveBundle = MoveBundle.fromIntent(intent)
28 |
29 | scope.launch {
30 | with(moveBundle) {
31 | file.moveTo(destination = destination, context = context) { result ->
32 | moveResultChannel.trySend(
33 | result bundleWith this
34 | )
35 | }
36 | }
37 | }
38 | }
39 |
40 | companion object {
41 | fun sendBroadcast(moveBundle: AnyMoveBundle, context: Context) {
42 | context.sendBroadcast(
43 | getIntent(
44 | moveBundle = moveBundle,
45 | context = context
46 | )
47 | )
48 | }
49 |
50 | fun getIntent(moveBundle: AnyMoveBundle, context: Context): Intent =
51 | Intent(context, MoveBroadcastReceiver::class.java)
52 | .putExtra(MoveBundle.EXTRA, moveBundle)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/moving/api/activity/AbstractDestinationPickerActivity.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.moving.api.activity
2 |
3 | import android.content.ComponentName
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import android.os.Parcelable
8 | import androidx.annotation.CallSuper
9 | import com.w2sv.common.util.DocumentUri
10 | import com.w2sv.common.util.isExternalStorageManger
11 | import com.w2sv.navigator.moving.model.MoveResult
12 |
13 | internal abstract class AbstractDestinationPickerActivity : AbstractMoveActivity() {
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 |
18 | preemptiveMoveFailure()?.let { sendMoveResultBundleAndFinishAndRemoveTask(it) } ?: run { launchPicker() }
19 | }
20 |
21 | abstract fun launchPicker()
22 |
23 | @CallSuper
24 | protected open fun preemptiveMoveFailure(): MoveResult.Failure? =
25 | when {
26 | !isExternalStorageManger -> MoveResult.ManageAllFilesPermissionMissing
27 | else -> null
28 | }
29 |
30 | interface Args : Parcelable {
31 | val pickerStartDestination: DocumentUri?
32 |
33 | companion object {
34 | const val EXTRA = "com.w2sv.navigator.extra.AbstractDestinationPickerActivity.Args"
35 | }
36 | }
37 |
38 | companion object {
39 | inline fun makeRestartActivityIntent(args: Args, context: Context): Intent =
40 | Intent.makeRestartActivityTask(
41 | ComponentName(
42 | context,
43 | T::class.java
44 | )
45 | )
46 | .putExtra(Args.EXTRA, args)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/moving/api/activity/AbstractMoveActivity.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.moving.api.activity
2 |
3 | import com.w2sv.common.util.LoggingComponentActivity
4 | import com.w2sv.navigator.MoveResultChannel
5 | import com.w2sv.navigator.moving.model.MoveResult
6 | import com.w2sv.navigator.notifications.NotificationResources
7 | import dagger.hilt.android.AndroidEntryPoint
8 | import javax.inject.Inject
9 |
10 | @AndroidEntryPoint
11 | internal abstract class AbstractMoveActivity : LoggingComponentActivity() {
12 |
13 | @Inject
14 | lateinit var moveResultChannel: MoveResultChannel
15 |
16 | protected fun sendMoveResultBundleAndFinishAndRemoveTask(
17 | moveFailure: MoveResult.Failure,
18 | notificationResources: NotificationResources? = null
19 | ) {
20 | moveResultChannel.trySend(moveFailure bundleWith notificationResources)
21 | finishAndRemoveTask()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/moving/batch/CancelBatchMoveBroadcastReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.moving.batch
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import com.w2sv.common.util.LoggingBroadcastReceiver
6 | import dagger.hilt.android.AndroidEntryPoint
7 | import javax.inject.Inject
8 |
9 | @AndroidEntryPoint
10 | internal class CancelBatchMoveBroadcastReceiver : LoggingBroadcastReceiver() {
11 |
12 | @Inject
13 | lateinit var batchMoveJobHolder: BatchMoveBroadcastReceiver.JobHolder
14 |
15 | override fun onReceive(context: Context, intent: Intent) {
16 | super.onReceive(context, intent)
17 | batchMoveJobHolder.job?.cancel()
18 | }
19 |
20 | companion object {
21 | fun intent(context: Context): Intent =
22 | Intent(context, CancelBatchMoveBroadcastReceiver::class.java)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/moving/model/DestinationSelectionManner.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.moving.model
2 |
3 | import android.os.Parcelable
4 | import com.w2sv.navigator.notifications.NotificationResources
5 | import kotlinx.parcelize.Parcelize
6 |
7 | @Parcelize
8 | internal sealed interface DestinationSelectionManner : Parcelable {
9 |
10 | sealed interface NotificationBased : DestinationSelectionManner {
11 | val notificationResources: NotificationResources
12 | }
13 |
14 | @Parcelize
15 | data class Picked(
16 | override val notificationResources: NotificationResources
17 | ) : NotificationBased
18 |
19 | @Parcelize
20 | data class Quick(
21 | override val notificationResources: NotificationResources
22 | ) : NotificationBased
23 |
24 | @Parcelize
25 | data object Auto : DestinationSelectionManner
26 |
27 | val isPicked: Boolean
28 | get() = this is Picked
29 |
30 | val isAuto: Boolean
31 | get() = this is Auto
32 | }
33 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/moving/model/MediaIdWithMediaType.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.moving.model
2 |
3 | import com.anggrayudi.storage.media.MediaType
4 | import com.w2sv.common.util.MediaId
5 |
6 | data class MediaIdWithMediaType(val mediaId: MediaId, val mediaType: MediaType)
7 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/moving/model/MoveFile.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.moving.model
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Parcelable
6 | import com.anggrayudi.storage.media.MediaFile
7 | import com.anggrayudi.storage.media.MediaStoreCompat
8 | import com.w2sv.androidutils.os.getParcelableCompat
9 | import com.w2sv.common.util.MediaUri
10 | import com.w2sv.domain.model.filetype.FileAndSourceType
11 | import com.w2sv.domain.model.filetype.FileType
12 | import com.w2sv.domain.model.filetype.PresetFileType
13 | import com.w2sv.domain.model.filetype.SourceType
14 | import com.w2sv.navigator.observing.model.MediaStoreFileData
15 | import kotlinx.parcelize.Parcelize
16 |
17 | @Parcelize
18 | internal data class MoveFile(
19 | val mediaUri: MediaUri,
20 | val mediaStoreFileData: MediaStoreFileData,
21 | val fileAndSourceType: FileAndSourceType
22 | ) : Parcelable {
23 |
24 | fun simpleStorageMediaFile(context: Context): MediaFile? =
25 | MediaStoreCompat.fromMediaId(
26 | context = context,
27 | mediaType = fileType.mediaType,
28 | id = mediaStoreFileData.rowId
29 | )
30 |
31 | val fileType: FileType
32 | get() = fileAndSourceType.fileType
33 |
34 | val sourceType: SourceType
35 | get() = fileAndSourceType.sourceType
36 |
37 | val isGif: Boolean
38 | get() = fileType.wrappedPresetTypeOrNull is PresetFileType.Image && mediaStoreFileData.extension.lowercase() == "gif"
39 |
40 | companion object {
41 | const val EXTRA = "com.w2sv.filenavigator.extra.MoveFile"
42 |
43 | fun fromIntent(intent: Intent): MoveFile =
44 | intent.getParcelableCompat(EXTRA)!!
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/notifications/CleanupNotificationResourcesBroadcastReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.notifications
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import com.w2sv.common.util.LoggingBroadcastReceiver
6 | import com.w2sv.common.util.log
7 | import com.w2sv.navigator.notifications.api.MultiInstanceNotificationManager
8 | import com.w2sv.navigator.shared.plus
9 | import dagger.hilt.android.AndroidEntryPoint
10 | import javax.inject.Inject
11 |
12 | @AndroidEntryPoint
13 | internal class CleanupNotificationResourcesBroadcastReceiver : LoggingBroadcastReceiver() {
14 |
15 | @Inject
16 | @JvmSuppressWildcards
17 | lateinit var multiInstanceAppNotificationManagers: Set>
18 |
19 | override fun onReceive(context: Context, intent: Intent) {
20 | super.onReceive(context, intent)
21 |
22 | NotificationResources.Companion.optionalFromIntent(intent)
23 | ?.let { resources ->
24 | multiInstanceAppNotificationManagers
25 | .first { notificationManager ->
26 | resources.managerClassName == notificationManager.resourcesIdentifier
27 | }
28 | .log { "Cleaning up ${it.resourcesIdentifier} resources" }
29 | .cancelNotification(resources.id)
30 | }
31 | }
32 |
33 | companion object {
34 | fun getIntent(context: Context, notificationResources: NotificationResources): Intent =
35 | Intent(
36 | context,
37 | CleanupNotificationResourcesBroadcastReceiver::class.java
38 | ) + notificationResources
39 |
40 | fun start(context: Context, notificationResources: NotificationResources) {
41 | context.sendBroadcast(getIntent(context, notificationResources))
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/notifications/NotificationModule.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.notifications
2 |
3 | import android.app.NotificationManager
4 | import android.content.Context
5 | import com.w2sv.androidutils.getNotificationManager
6 | import com.w2sv.navigator.notifications.api.MultiInstanceNotificationManager
7 | import com.w2sv.navigator.notifications.appnotifications.AutoMoveDestinationInvalidNotificationManager
8 | import com.w2sv.navigator.notifications.appnotifications.movefile.MoveFileNotificationManager
9 | import dagger.Module
10 | import dagger.Provides
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.android.qualifiers.ApplicationContext
13 | import dagger.hilt.components.SingletonComponent
14 | import dagger.multibindings.ElementsIntoSet
15 | import javax.inject.Singleton
16 |
17 | @InstallIn(SingletonComponent::class)
18 | @Module
19 | internal object NotificationModule {
20 |
21 | @Singleton
22 | @Provides
23 | fun notificationManager(@ApplicationContext context: Context): NotificationManager =
24 | context.getNotificationManager()
25 |
26 | @Singleton
27 | @Provides
28 | @ElementsIntoSet
29 | fun multiInstanceAppNotificationManagers(
30 | moveFileNotificationManager: MoveFileNotificationManager,
31 | autoMoveDestinationInvalidNotificationManager: AutoMoveDestinationInvalidNotificationManager
32 | ): Set> =
33 | setOf(moveFileNotificationManager, autoMoveDestinationInvalidNotificationManager)
34 | }
35 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/notifications/NotificationResources.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.notifications
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Parcelable
6 | import com.w2sv.androidutils.os.getParcelableCompat
7 | import kotlinx.parcelize.Parcelize
8 |
9 | @Parcelize
10 | internal data class NotificationResources(
11 | val id: Int,
12 | val managerClassName: String
13 | ) : Parcelable {
14 |
15 | fun pendingIntentRequestCodes(count: Int): List =
16 | (id until id + count).toList()
17 |
18 | fun cancelNotification(context: Context) {
19 | CleanupNotificationResourcesBroadcastReceiver.start(
20 | context = context,
21 | notificationResources = this
22 | )
23 | }
24 |
25 | companion object {
26 | const val EXTRA = "com.w2sv.filenavigator.extra.NotificationResources"
27 |
28 | fun optionalFromIntent(intent: Intent): NotificationResources? =
29 | intent.getParcelableCompat(EXTRA)
30 |
31 | fun fromIntent(intent: Intent): NotificationResources =
32 | optionalFromIntent(intent)!!
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/notifications/api/AppNotificationManager.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.notifications.api
2 |
3 | import android.app.Notification
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.content.Context
7 | import androidx.annotation.CallSuper
8 | import androidx.core.app.NotificationCompat
9 | import com.w2sv.core.common.R
10 | import com.w2sv.navigator.notifications.appnotifications.AppNotificationChannel
11 |
12 | internal abstract class AppNotificationManager(
13 | appNotificationChannel: AppNotificationChannel,
14 | protected val notificationManager: NotificationManager,
15 | protected val context: Context
16 | ) {
17 | protected val notificationChannel: NotificationChannel =
18 | appNotificationChannel.getNotificationChannel(context)
19 |
20 | init {
21 | notificationManager.createNotificationChannel(notificationChannel)
22 | }
23 |
24 | open inner class Builder : NotificationCompat.Builder(context, notificationChannel.id) {
25 |
26 | @CallSuper
27 | override fun build(): Notification {
28 | setSmallIcon(R.drawable.ic_app_logo_24)
29 |
30 | priority = NotificationCompat.PRIORITY_DEFAULT
31 |
32 | return super.build()
33 | }
34 | }
35 |
36 | protected fun buildAndPostNotification(id: Int, args: Args) {
37 | notificationManager.notify(id, buildNotification(args))
38 | }
39 |
40 | fun buildNotification(args: Args): Notification =
41 | getBuilder(args).build()
42 |
43 | protected abstract fun getBuilder(args: Args): Builder
44 | }
45 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/notifications/api/SingleInstanceNotificationManager.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.notifications.api
2 |
3 | import android.app.NotificationManager
4 | import android.content.Context
5 | import com.w2sv.navigator.notifications.appnotifications.AppNotificationChannel
6 | import com.w2sv.navigator.notifications.appnotifications.AppNotificationId
7 |
8 | /**
9 | * Manager for notifications of which only a single instance may be active at a time.
10 | */
11 | internal abstract class SingleInstanceNotificationManager(
12 | appNotificationChannel: AppNotificationChannel,
13 | notificationManager: NotificationManager,
14 | context: Context,
15 | private val appNotificationId: AppNotificationId
16 | ) : AppNotificationManager(
17 | appNotificationChannel = appNotificationChannel,
18 | notificationManager = notificationManager,
19 | context = context
20 | ) {
21 | fun buildAndPostNotification(args: Args) {
22 | buildAndPostNotification(appNotificationId.id, args)
23 | }
24 |
25 | fun cancelNotification() {
26 | notificationManager.cancel(appNotificationId.id)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/notifications/appnotifications/AppNotificationChannel.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.notifications.appnotifications
2 |
3 | import android.app.NotificationChannel
4 | import android.app.NotificationManager
5 | import android.content.Context
6 | import androidx.annotation.StringRes
7 | import com.w2sv.core.common.R
8 |
9 | /**
10 | * Enum assures required id-uniqueness of resulting [NotificationChannel].
11 | */
12 | internal enum class AppNotificationChannel(@StringRes val nameRes: Int) {
13 | FileNavigatorIsRunning(R.string.file_navigator_is_running),
14 | NewNavigatableFile(R.string.new_navigatable_file),
15 | AutoMoveDestinationInvalid(R.string.auto_move_destination_invalid),
16 | MoveProgress(R.string.move_progress);
17 |
18 | fun getNotificationChannel(context: Context, importance: Int = NotificationManager.IMPORTANCE_DEFAULT): NotificationChannel =
19 | NotificationChannel(
20 | name,
21 | context.getString(nameRes),
22 | importance
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/notifications/appnotifications/AppNotificationId.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.notifications.appnotifications
2 |
3 | /**
4 | * Enum assures required [id]-uniqueness.
5 | */
6 | internal enum class AppNotificationId {
7 | FileNavigatorIsRunning,
8 | NewNavigatableFile,
9 | AutoMoveDestinationInvalid,
10 | BatchMoveFiles,
11 | MoveProgress;
12 |
13 | val id: Int by lazy {
14 | ordinal + 1 // 0 is an invalid notification ID
15 | }
16 |
17 | val multiInstanceIdBase: Int by lazy {
18 | id * 1000
19 | }
20 |
21 | val summaryId: Int by lazy {
22 | multiInstanceIdBase + 999
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/notifications/appnotifications/Shared.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.notifications.appnotifications
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import androidx.annotation.ColorInt
6 | import androidx.annotation.DrawableRes
7 | import androidx.appcompat.content.res.AppCompatResources
8 | import androidx.core.graphics.drawable.toBitmap
9 | import com.w2sv.domain.model.filetype.FileAndSourceType
10 |
11 | internal fun FileAndSourceType.iconBitmap(context: Context, colored: Boolean = false): Bitmap? =
12 | context.drawableBitmap(
13 | drawable = iconRes,
14 | tint = if (colored) fileType.colorInt else null
15 | )
16 |
17 | internal fun Context.drawableBitmap(@DrawableRes drawable: Int, @ColorInt tint: Int? = null): Bitmap? =
18 | AppCompatResources.getDrawable(this, drawable)
19 | ?.apply { tint?.let { setTint(it) } }
20 | ?.toBitmap()
21 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/observing/FileObserverModule.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.observing
2 |
3 | import android.os.HandlerThread
4 | import com.w2sv.common.util.log
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.components.ServiceComponent
9 | import dagger.hilt.android.scopes.ServiceScoped
10 | import javax.inject.Qualifier
11 |
12 | @Qualifier
13 | @Retention(AnnotationRetention.BINARY)
14 | annotation class FileObserverHandlerThread
15 |
16 | @InstallIn(ServiceComponent::class)
17 | @Module
18 | internal object FileObserverModule {
19 |
20 | @Provides
21 | @ServiceScoped
22 | @FileObserverHandlerThread
23 | fun fileObserverHandlerThread(): HandlerThread =
24 | HandlerThread("com.w2sv.filenavigator.FileObserverHandlerThread")
25 | .apply { start() }
26 | .log { "Initialized ${it.name}" }
27 | }
28 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/shared/AlertDialog.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.shared
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import androidx.annotation.DrawableRes
6 | import androidx.appcompat.app.AlertDialog
7 | import com.w2sv.core.navigator.R
8 | import com.w2sv.core.navigator.databinding.DialogHeaderBinding
9 |
10 | internal fun AlertDialog.Builder.setIconHeader(@DrawableRes iconRes: Int): AlertDialog.Builder =
11 | apply {
12 | setCustomTitle(
13 | DialogHeaderBinding
14 | .inflate(LayoutInflater.from(context))
15 | .apply { icon.setImageResource(iconRes) }
16 | .root
17 | )
18 | }
19 |
20 | internal fun roundedCornersAlertDialogBuilder(context: Context): AlertDialog.Builder =
21 | AlertDialog
22 | .Builder(context, R.style.RoundedCornersAlertDialog)
23 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/shared/DialogHostingActivity.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.shared
2 |
3 | import android.app.Dialog
4 | import androidx.appcompat.app.AlertDialog
5 | import com.w2sv.navigator.moving.api.activity.AbstractMoveActivity
6 |
7 | internal abstract class DialogHostingActivity : AbstractMoveActivity() {
8 |
9 | protected var dialog: Dialog? = null
10 |
11 | protected fun showDialog(builder: AlertDialog.Builder) {
12 | dialog = builder.create().also { it.show() }
13 | }
14 |
15 | override fun onDestroy() {
16 | super.onDestroy()
17 |
18 | // Prevents 'android.view.WindowLeaked: Activity ... has
19 | // leaked window DecorView@f70b286[QuickMoveDestinationPermissionQueryOverlayDialogActivity] that was originally added here'
20 | dialog?.dismiss()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/shared/Intent.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.shared
2 |
3 | import android.app.PendingIntent
4 | import android.content.ComponentName
5 | import android.content.Context
6 | import android.content.Intent
7 | import com.w2sv.navigator.moving.model.MoveFile
8 | import com.w2sv.navigator.notifications.NotificationResources
9 |
10 | internal fun mainActivityPendingIntent(context: Context): PendingIntent =
11 | PendingIntent.getActivity(
12 | context,
13 | 1,
14 | mainActivityIntent(context),
15 | PendingIntent.FLAG_IMMUTABLE
16 | )
17 |
18 | internal fun mainActivityIntent(context: Context): Intent =
19 | Intent.makeRestartActivityTask(
20 | ComponentName(context, "com.w2sv.filenavigator.MainActivity")
21 | )
22 |
23 | internal operator fun Intent.plus(notificationResources: NotificationResources?): Intent =
24 | putExtra(NotificationResources.EXTRA, notificationResources)
25 |
26 | internal operator fun Intent.plus(moveFile: MoveFile): Intent =
27 | putExtra(MoveFile.EXTRA, moveFile)
28 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/shared/Logging.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.shared
2 |
3 | import slimber.log.i
4 |
5 | internal fun emitDiscardedLog(reason: () -> String) {
6 | i { "DISCARDED: ${reason()}" }
7 | }
8 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/system_broadcastreceiver/BootCompletedReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.system_broadcastreceiver
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import com.w2sv.navigator.FileNavigator
6 | import slimber.log.i
7 |
8 | internal class BootCompletedReceiver : SystemBroadcastReceiver(Intent.ACTION_BOOT_COMPLETED) {
9 |
10 | override fun onReceiveMatchingIntent(context: Context, intent: Intent) {
11 | i { "BootCompletedReceiver.onReceive" }
12 |
13 | FileNavigator.start(context)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/system_broadcastreceiver/PowerSaveModeChangedReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.system_broadcastreceiver
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.PowerManager
6 | import com.w2sv.navigator.FileNavigator
7 | import dagger.hilt.android.AndroidEntryPoint
8 | import javax.inject.Inject
9 | import slimber.log.i
10 |
11 | @AndroidEntryPoint
12 | internal class PowerSaveModeChangedReceiver :
13 | SystemBroadcastReceiver(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) {
14 |
15 | @Inject
16 | internal lateinit var powerManager: PowerManager
17 |
18 | override fun onReceiveMatchingIntent(context: Context, intent: Intent) {
19 | if (powerManager.isPowerSaveMode) {
20 | i { "Stopping FileNavigator due to power save mode" }
21 | FileNavigator.stop(context)
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/system_broadcastreceiver/SystemBroadcastReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.system_broadcastreceiver
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.content.IntentFilter
6 | import com.w2sv.common.util.LoggingBroadcastReceiver
7 | import com.w2sv.common.util.logIdentifier
8 | import slimber.log.i
9 |
10 | abstract class SystemBroadcastReceiver(private val action: String) : LoggingBroadcastReceiver() {
11 |
12 | override fun onReceive(context: Context, intent: Intent) {
13 | super.onReceive(context, intent)
14 |
15 | if (intent.action != action) return
16 |
17 | onReceiveMatchingIntent(context, intent)
18 | }
19 |
20 | protected abstract fun onReceiveMatchingIntent(context: Context, intent: Intent)
21 |
22 | fun toggle(register: Boolean, context: Context) {
23 | try {
24 | if (register) {
25 | register(context)
26 | } else {
27 | unregister(context)
28 | }
29 | } catch (_: IllegalArgumentException) { // Thrown upon attempting to unregister unregistered receiver
30 | }
31 | }
32 |
33 | fun register(context: Context) {
34 | context.registerReceiver(
35 | this,
36 | IntentFilter()
37 | .apply {
38 | addAction(action)
39 | }
40 | )
41 | i { "Registered $logIdentifier" }
42 | }
43 |
44 | fun unregister(context: Context) {
45 | context.unregisterReceiver(this)
46 | i { "Unregistered $logIdentifier" }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/system_broadcastreceiver/di/SystemBroadcastReceiverBinderModule.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.system_broadcastreceiver.di
2 |
3 | import com.w2sv.navigator.system_broadcastreceiver.manager.NavigatorConfigControlledSystemBroadcastReceiverManager
4 | import com.w2sv.navigator.system_broadcastreceiver.manager.NavigatorConfigControlledSystemBroadcastReceiverManagerImpl
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 |
10 | @InstallIn(SingletonComponent::class)
11 | @Module
12 | internal interface SystemBroadcastReceiverBinderModule {
13 |
14 | @Binds
15 | fun navigatorConfigControlledSystemBroadcastReceiverManager(
16 | impl: NavigatorConfigControlledSystemBroadcastReceiverManagerImpl
17 | ): NavigatorConfigControlledSystemBroadcastReceiverManager
18 | }
19 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/system_broadcastreceiver/di/SystemBroadcastReceiverModule.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.system_broadcastreceiver.di
2 |
3 | import com.w2sv.navigator.system_broadcastreceiver.BootCompletedReceiver
4 | import com.w2sv.navigator.system_broadcastreceiver.PowerSaveModeChangedReceiver
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 |
10 | @InstallIn(SingletonComponent::class)
11 | @Module
12 | internal object SystemBroadcastReceiverModule {
13 |
14 | @Provides
15 | fun bootCompletedReceiver(): BootCompletedReceiver =
16 | BootCompletedReceiver()
17 |
18 | @Provides
19 | fun powerSaveModeChangedReceiver(): PowerSaveModeChangedReceiver =
20 | PowerSaveModeChangedReceiver()
21 | }
22 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/system_broadcastreceiver/manager/NavigatorConfigControlledSystemBroadcastReceiverManager.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.system_broadcastreceiver.manager
2 |
3 | import android.content.Context
4 | import kotlinx.coroutines.CoroutineScope
5 |
6 | interface NavigatorConfigControlledSystemBroadcastReceiverManager {
7 | fun toggleReceiversOnStatusChange(collectionScope: CoroutineScope, context: Context)
8 | }
9 |
--------------------------------------------------------------------------------
/core/navigator/src/main/kotlin/com/w2sv/navigator/system_broadcastreceiver/manager/NavigatorConfigControlledSystemBroadcastReceiverManagerImpl.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.system_broadcastreceiver.manager
2 |
3 | import android.content.Context
4 | import com.w2sv.domain.repository.NavigatorConfigDataSource
5 | import com.w2sv.kotlinutils.coroutines.collectFromFlow
6 | import com.w2sv.navigator.system_broadcastreceiver.BootCompletedReceiver
7 | import com.w2sv.navigator.system_broadcastreceiver.PowerSaveModeChangedReceiver
8 | import javax.inject.Inject
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.flow.distinctUntilChanged
11 | import kotlinx.coroutines.flow.map
12 | import slimber.log.i
13 |
14 | internal class NavigatorConfigControlledSystemBroadcastReceiverManagerImpl @Inject constructor(
15 | navigatorConfigDataSource: NavigatorConfigDataSource,
16 | private val bootCompletedReceiver: BootCompletedReceiver,
17 | private val powerSaveModeChangedReceiver: PowerSaveModeChangedReceiver
18 | ) : NavigatorConfigControlledSystemBroadcastReceiverManager {
19 |
20 | override fun toggleReceiversOnStatusChange(collectionScope: CoroutineScope, context: Context) {
21 | with(collectionScope) {
22 | collectFromFlow(disabledOnLowBatteryDistinctUntilChanged) {
23 | i { "Collected disableOnLowBattery=$it" }
24 | powerSaveModeChangedReceiver.toggle(it, context)
25 | }
26 | collectFromFlow(startOnBootDistinctUntilChanged) {
27 | i { "Collected startOnBootCompleted=$it" }
28 | bootCompletedReceiver.toggle(it, context)
29 | }
30 | }
31 | }
32 |
33 | private val disabledOnLowBatteryDistinctUntilChanged =
34 | navigatorConfigDataSource.navigatorConfig.map { it.disableOnLowBattery }
35 | .distinctUntilChanged()
36 |
37 | private val startOnBootDistinctUntilChanged =
38 | navigatorConfigDataSource.navigatorConfig.map { it.startOnBoot }
39 | .distinctUntilChanged()
40 | }
41 |
--------------------------------------------------------------------------------
/core/navigator/src/main/res/layout/dialog_header.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
16 |
17 |
--------------------------------------------------------------------------------
/core/navigator/src/main/res/layout/tile_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
19 |
20 |
23 |
24 |
30 |
31 |
34 |
35 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/core/navigator/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
--------------------------------------------------------------------------------
/core/navigator/src/test/kotlin/com/w2sv/navigator/moving/model/DestinationSelectionMannerTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.moving.model
2 |
3 | import com.w2sv.navigator.notifications.NotificationResources
4 | import com.w2sv.test.testParceling
5 | import org.junit.Test
6 | import org.junit.runner.RunWith
7 | import org.robolectric.RobolectricTestRunner
8 |
9 | @RunWith(RobolectricTestRunner::class)
10 | internal class DestinationSelectionMannerTest {
11 |
12 | @Test
13 | fun testParceling() {
14 | DestinationSelectionManner
15 | .Picked(NotificationResources(12, "manager"))
16 | .testParceling()
17 |
18 | DestinationSelectionManner
19 | .Quick(NotificationResources(19, "manager"))
20 | .testParceling()
21 |
22 | DestinationSelectionManner.Auto.testParceling()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/navigator/src/test/kotlin/com/w2sv/navigator/moving/model/MoveBundleTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.moving.model
2 |
3 | import com.w2sv.navigator.notifications.NotificationResources
4 | import com.w2sv.test.testParceling
5 | import org.junit.Test
6 | import org.junit.runner.RunWith
7 | import org.robolectric.RobolectricTestRunner
8 | import util.TestInstance
9 |
10 | @RunWith(RobolectricTestRunner::class)
11 | internal class MoveBundleTest {
12 |
13 | @Test
14 | fun testParceling() {
15 | MoveBundle.DirectoryDestinationPicked(
16 | file = TestInstance.moveFile(),
17 | destination = NavigatorMoveDestination.Directory.parse("lkasjdflkajhlk"),
18 | destinationSelectionManner = DestinationSelectionManner.Picked(
19 | NotificationResources(
20 | 7,
21 | "MoveFileNotificationManager"
22 | )
23 | ),
24 | batched = true
25 | )
26 | .testParceling()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/core/navigator/src/test/kotlin/com/w2sv/navigator/moving/model/MoveFileTest.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.navigator.moving.model
2 |
3 | import com.w2sv.test.testParceling
4 | import org.junit.Test
5 | import org.junit.runner.RunWith
6 | import org.robolectric.RobolectricTestRunner
7 | import util.TestInstance
8 |
9 | @RunWith(RobolectricTestRunner::class)
10 | internal class MoveFileTest {
11 |
12 | @Test
13 | fun testParceling() {
14 | TestInstance
15 | .moveFile()
16 | .testParceling()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/core/navigator/src/test/kotlin/util/ResourceFileLoading.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import java.io.File
4 |
5 | internal fun getResourceFile(fileName: String): File =
6 | File("src/test/resources/$fileName")
7 |
8 | internal val File.sizeInMb: Double
9 | get() = length().toDouble() / 1e6
10 |
--------------------------------------------------------------------------------
/core/navigator/src/test/kotlin/util/TestInstance.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import com.w2sv.common.util.MediaUri
4 | import com.w2sv.domain.model.filetype.FileAndSourceType
5 | import com.w2sv.domain.model.filetype.PresetFileType
6 | import com.w2sv.domain.model.filetype.SourceType
7 | import com.w2sv.navigator.moving.model.MoveFile
8 | import com.w2sv.navigator.observing.model.MediaStoreFileData
9 |
10 | internal object TestInstance {
11 |
12 | val mediaStoreFileData = MediaStoreFileData(
13 | rowId = "1000012597",
14 | absPath = "primary/0/DCIM/Screenshots/somepicture.jpg",
15 | volumeRelativeDirPath = "DCIM/Screenshots",
16 | size = 7862183L,
17 | isPending = false,
18 | isTrashed = false
19 | )
20 |
21 | fun mediaStoreFileData(
22 | absPath: String,
23 | volumeRelativeDirPath: String,
24 | rowId: String = "1000012597",
25 | size: Long = 7862183L,
26 | isPending: Boolean = false,
27 | isTrashed: Boolean = false
28 | ): MediaStoreFileData =
29 | MediaStoreFileData(
30 | rowId = rowId,
31 | absPath = absPath,
32 | volumeRelativeDirPath = volumeRelativeDirPath,
33 | size = size,
34 | isPending = isPending,
35 | isTrashed = isTrashed
36 | )
37 |
38 | fun moveFile(
39 | mediaUri: MediaUri = MediaUri.parse("content://media/external/images/media/1000012597"),
40 | mediaStoreFileData: MediaStoreFileData = this.mediaStoreFileData,
41 | fileAndSourceType: FileAndSourceType = FileAndSourceType(
42 | fileType = PresetFileType.Image.toFileType(),
43 | sourceType = SourceType.Screenshot
44 | )
45 | ): MoveFile =
46 | MoveFile(
47 | mediaUri = mediaUri,
48 | mediaStoreFileData = mediaStoreFileData,
49 | fileAndSourceType = fileAndSourceType
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/core/navigator/src/test/resources/Kyuss_Welcome_to_Sky_Valley.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/core/navigator/src/test/resources/Kyuss_Welcome_to_Sky_Valley.jpg
--------------------------------------------------------------------------------
/core/navigator/src/test/resources/Mandelbulb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/core/navigator/src/test/resources/Mandelbulb.png
--------------------------------------------------------------------------------
/core/navigator/src/test/resources/Sandro_Botticelli_-_La_Carte_de_l'Enfer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/core/navigator/src/test/resources/Sandro_Botticelli_-_La_Carte_de_l'Enfer.jpg
--------------------------------------------------------------------------------
/core/navigator/src/test/resources/empty.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/core/navigator/src/test/resources/empty.txt
--------------------------------------------------------------------------------
/core/navigator/src/test/resources/other_empty.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/core/navigator/src/test/resources/other_empty.txt
--------------------------------------------------------------------------------
/core/test/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/test/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.filenavigator.library)
3 | alias(libs.plugins.kotlin.parcelize)
4 | }
5 |
6 | dependencies {
7 | implementation(libs.slimber)
8 | api(libs.bundles.unitTest)
9 | }
10 |
--------------------------------------------------------------------------------
/core/test/src/main/kotlin/com/w2sv/test/Parcelable.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.test
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import kotlinx.parcelize.parcelableCreator
6 | import org.junit.Assert.assertEquals
7 |
8 | /**
9 | * Parcels the receiver, recreates it from the parcel and asserts whether the original instance and the recreated one are equal.
10 | *
11 | * Requires `@RunWith(RobolectricTestRunner::class)` annotation on calling test class.
12 | */
13 | inline fun T.testParceling(flags: Int = 0) {
14 | val parcel = Parcel.obtain()
15 | this.writeToParcel(parcel, flags)
16 |
17 | // Reset the parcel's position for reading
18 | parcel.setDataPosition(0)
19 |
20 | // Assert that the original and recreated objects are equal
21 | val recreated = parcelableCreator().createFromParcel(parcel)
22 | assertEquals(this, recreated)
23 |
24 | // Recycle the parcel to avoid memory leaks
25 | parcel.recycle()
26 | }
27 |
--------------------------------------------------------------------------------
/core/test/src/main/kotlin/com/w2sv/test/TimberTestRule.kt:
--------------------------------------------------------------------------------
1 | package com.w2sv.test
2 |
3 | import org.junit.rules.TestRule
4 | import org.junit.runner.Description
5 | import org.junit.runners.model.Statement
6 | import timber.log.Timber
7 |
8 | /**
9 | * May be used to receive [Timber] logs during unit testing.
10 | */
11 | class TimberTestRule : TestRule {
12 | override fun apply(base: Statement, description: Description): Statement =
13 | object : Statement() {
14 | override fun evaluate() {
15 | Timber.plant(TestDebugTree)
16 | try {
17 | base.evaluate()
18 | } finally {
19 | Timber.uprootAll()
20 | }
21 | }
22 | }
23 | }
24 |
25 | private object TestDebugTree : Timber.Tree() {
26 | override fun log(
27 | priority: Int,
28 | tag: String?,
29 | message: String,
30 | t: Throwable?
31 | ) {
32 | println(
33 | buildString {
34 | tag?.let { append("$tag: ") }
35 | append(message)
36 | }
37 | )
38 | t?.printStackTrace(System.out)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # ======= Kotlin =======
2 | kotlin.code.style=official
3 | kotlin.daemon.jvmargs=-Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=1 -XX:ReservedCodeCacheSize=320m -XX:+HeapDumpOnOutOfMemoryError -Xmx4g -Xms4g
4 | # ======= Gradle =======
5 | org.gradle.jvmargs=-Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=1 -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx4g -Xms4g
6 | org.gradle.parallel=true
7 | org.gradle.caching=true
8 | org.gradle.configureondemand=true
9 | org.gradle.configuration-cache=true
10 | org.gradle.configuration-cache.parallel=true
11 | # ======= Android =======
12 | android.useAndroidX=true
13 | android.nonTransitiveRClass=true
14 | android.uniquePackageNames=true
15 | android.nonFinalResIds=false
16 | # ======= Version =======
17 | version=0.3.0.1
18 | versionCode=15
19 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w2sv/FileNavigator/60cc5c220659dca1cdade72c68d8eed742f8d528/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | includeBuild("build-logic")
7 | }
8 | }
9 |
10 | @Suppress("UnstableApiUsage")
11 | dependencyResolutionManagement {
12 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
13 | repositories {
14 | google()
15 | mavenCentral()
16 | maven(url = "https://jitpack.io")
17 | }
18 | }
19 |
20 | rootProject.name = "FileNavigator"
21 |
22 | include(":app")
23 | include(":benchmarking")
24 | include(":core:datastore")
25 | include(":core:database")
26 | include(":core:domain")
27 | include(":core:common")
28 | include(":core:navigator")
29 | include(":core:test")
30 |
--------------------------------------------------------------------------------