├── common ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── wa2c │ │ └── android │ │ └── cifsdocumentsprovider │ │ └── common │ │ ├── values │ │ ├── ImportOption.kt │ │ ├── ProtocolType.kt │ │ ├── AccessMode.kt │ │ ├── ThumbnailType.kt │ │ ├── HostSortType.kt │ │ ├── UiTheme.kt │ │ ├── StorageType.kt │ │ └── Const.kt │ │ ├── exception │ │ ├── AppException.kt │ │ ├── EditException.kt │ │ └── StorageException.kt │ │ └── utils │ │ └── LogUtils.kt ├── proguard-rules.pro └── build.gradle.kts ├── domain ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── wa2c │ │ │ └── android │ │ │ └── cifsdocumentsprovider │ │ │ └── domain │ │ │ ├── model │ │ │ ├── KnownHost.kt │ │ │ ├── RemoteConnectionIndex.kt │ │ │ ├── HostData.kt │ │ │ ├── RemoteFile.kt │ │ │ ├── ConnectionResult.kt │ │ │ ├── SendDataState.kt │ │ │ ├── SendData.kt │ │ │ ├── StorageUri.kt │ │ │ └── DocumentId.kt │ │ │ ├── DomainModule.kt │ │ │ └── repository │ │ │ └── HostRepository.kt │ └── test │ │ └── java │ │ └── com │ │ └── wa2c │ │ └── android │ │ └── cifsdocumentsprovider │ │ └── domain │ │ └── ExampleUnitTest.kt └── build.gradle.kts ├── app ├── .gitignore ├── src │ └── main │ │ ├── res │ │ └── xml │ │ │ ├── backup_roles.xml │ │ │ └── backup_rules_v31.xml │ │ ├── java │ │ └── com │ │ │ └── wa2c │ │ │ └── android │ │ │ └── cifsdocumentsprovider │ │ │ └── App.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle.kts ├── data ├── data │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── wa2c │ │ │ │ └── android │ │ │ │ └── cifsdocumentsprovider │ │ │ │ └── data │ │ │ │ ├── db │ │ │ │ ├── AppDb.kt │ │ │ │ ├── ConnectionSettingEntity.kt │ │ │ │ ├── ConnectionIO.kt │ │ │ │ └── ConnectionSettingDao.kt │ │ │ │ ├── DataModule.kt │ │ │ │ ├── EncryptUtils.kt │ │ │ │ └── HostFinder.kt │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── wa2c │ │ │ └── android │ │ │ └── cifsdocumentsprovider │ │ │ └── data │ │ │ └── ExampleUnitTest.kt │ ├── build.gradle.kts │ └── schemas │ │ └── com.wa2c.android.cifsdocumentsprovider.data.db.AppDatabase │ │ ├── 2.json │ │ └── 1.json └── storage │ ├── apache │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── wa2c │ │ │ │ └── android │ │ │ │ └── cifsdocumentsprovider │ │ │ │ └── data │ │ │ │ └── storage │ │ │ │ └── apache │ │ │ │ ├── ApacheFtpClient.kt │ │ │ │ └── ApacheSftpClient.kt │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── wa2c │ │ │ └── android │ │ │ └── cifsdocumentsprovider │ │ │ └── data │ │ │ └── storage │ │ │ └── apache │ │ │ └── ExampleUnitTest.kt │ └── build.gradle.kts │ ├── jcifsng │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── wa2c │ │ │ │ └── android │ │ │ │ └── cifsdocumentsprovider │ │ │ │ └── data │ │ │ │ └── storage │ │ │ │ └── jcifsng │ │ │ │ └── JCifsNgProxyFileCallbackSafe.kt │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── wa2c │ │ │ └── android │ │ │ └── cifsdocumentsprovider │ │ │ └── data │ │ │ └── storage │ │ │ └── jcifsng │ │ │ └── ExampleUnitTest.kt │ └── build.gradle.kts │ ├── manager │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── wa2c │ │ │ │ └── android │ │ │ │ └── cifsdocumentsprovider │ │ │ │ └── data │ │ │ │ └── storage │ │ │ │ └── manager │ │ │ │ └── SshKeyManager.kt │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── wa2c │ │ │ └── android │ │ │ └── cifsdocumentsprovider │ │ │ └── data │ │ │ └── storage │ │ │ └── manager │ │ │ └── ExampleUnitTest.kt │ └── build.gradle.kts │ ├── smbj │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── wa2c │ │ │ │ └── android │ │ │ │ └── cifsdocumentsprovider │ │ │ │ └── data │ │ │ │ └── storage │ │ │ │ └── smbj │ │ │ │ ├── SmbjProxyFileCallbackSafe.kt │ │ │ │ └── SmbjProxyFileCallback.kt │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── wa2c │ │ │ └── android │ │ │ └── cifsdocumentsprovider │ │ │ └── data │ │ │ └── storage │ │ │ └── smbj │ │ │ └── ExampleUnitTest.kt │ └── build.gradle.kts │ └── interfaces │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── wa2c │ │ │ └── android │ │ │ └── cifsdocumentsprovider │ │ │ └── data │ │ │ └── storage │ │ │ └── interfaces │ │ │ ├── StorageFile.kt │ │ │ ├── StorageClient.kt │ │ │ ├── utils │ │ │ ├── DataUtils.kt │ │ │ └── ErrorUtils.kt │ │ │ └── StorageRequest.kt │ └── test │ │ └── java │ │ └── com │ │ └── wa2c │ │ └── android │ │ └── cifsdocumentsprovider │ │ └── data │ │ └── storage │ │ └── interfaces │ │ └── ExampleUnitTest.kt │ └── build.gradle.kts ├── presentation ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ └── strings-common.xml │ │ │ ├── values-night │ │ │ │ └── colors.xml │ │ │ ├── drawable │ │ │ │ ├── bg_launcher.xml │ │ │ │ ├── ic_reload_rotate.xml │ │ │ │ ├── ic_back.xml │ │ │ │ ├── ic_check_bg.xml │ │ │ │ ├── ic_sort.xml │ │ │ │ ├── ic_close.xml │ │ │ │ ├── ic_edit.xml │ │ │ │ ├── ic_folder.xml │ │ │ │ ├── ic_check_wn.xml │ │ │ │ ├── ic_check_ok.xml │ │ │ │ ├── ic_save.xml │ │ │ │ ├── ic_host.xml │ │ │ │ ├── ic_key.xml │ │ │ │ ├── ic_add_folder.xml │ │ │ │ ├── ic_visibility.xml │ │ │ │ ├── ic_check_ng.xml │ │ │ │ ├── ic_folder_check.xml │ │ │ │ ├── ic_folder_up.xml │ │ │ │ ├── ic_delete.xml │ │ │ │ ├── ic_search.xml │ │ │ │ ├── ic_reload.xml │ │ │ │ ├── ic_check_uc.xml │ │ │ │ ├── ic_share.xml │ │ │ │ ├── ic_settings.xml │ │ │ │ ├── ic_notification.xml │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── mipmap │ │ │ │ └── ic_launcher.xml │ │ │ └── xml │ │ │ │ └── locales_config.xml │ │ └── java │ │ │ └── com │ │ │ └── wa2c │ │ │ └── android │ │ │ └── cifsdocumentsprovider │ │ │ └── presentation │ │ │ ├── ui │ │ │ ├── common │ │ │ │ ├── OptionItem.kt │ │ │ │ ├── Divider.kt │ │ │ │ ├── AppTopAppBarColors.kt │ │ │ │ ├── MutableStateAdapter.kt │ │ │ │ ├── LoadingBox.kt │ │ │ │ ├── ComposeExt.kt │ │ │ │ ├── Modifier.kt │ │ │ │ ├── BottomButton.kt │ │ │ │ ├── LoadingIconButton.kt │ │ │ │ └── CommonSingleChoiceDialog.kt │ │ │ ├── home │ │ │ │ └── HomeViewModel.kt │ │ │ ├── edit │ │ │ │ └── components │ │ │ │ │ ├── SubsectionTitle.kt │ │ │ │ │ ├── UriText.kt │ │ │ │ │ ├── SectionTitle.kt │ │ │ │ │ ├── KeyInputDialog.kt │ │ │ │ │ └── InputCheck.kt │ │ │ ├── settings │ │ │ │ └── components │ │ │ │ │ ├── SettingsItem.kt │ │ │ │ │ ├── TitleItem.kt │ │ │ │ │ ├── SettingsCheckItem.kt │ │ │ │ │ ├── SettingsSingleChoiceItem.kt │ │ │ │ │ └── SettingsInputNumberItem.kt │ │ │ ├── send │ │ │ │ └── SendViewModel.kt │ │ │ ├── receive │ │ │ │ └── ReceiveFile.kt │ │ │ └── MainViewModel.kt │ │ │ ├── ext │ │ │ ├── KeyInputType.kt │ │ │ ├── Language.kt │ │ │ └── UiExt.kt │ │ │ ├── worker │ │ │ ├── WorkerLifecycleOwner.kt │ │ │ ├── SendWorker.kt │ │ │ ├── ProviderWorker.kt │ │ │ └── ProviderNotification.kt │ │ │ └── PresentationModule.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── wa2c │ │ │ └── android │ │ │ └── cifsdocumentsprovider │ │ │ └── presentation │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── wa2c │ │ └── android │ │ └── cifsdocumentsprovider │ │ └── presentation │ │ └── ExampleInstrumentedTest.kt └── build.gradle.kts ├── .idea ├── .name ├── .gitignore ├── codeStyles │ └── codeStyleConfig.xml ├── encodings.xml ├── AndroidProjectSystem.xml ├── kotlinc.xml ├── compiler.xml ├── deploymentTargetSelector.xml ├── csv-editor.xml ├── runConfigurations.xml └── jarRepositories.xml ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── title.txt │ ├── short_description.txt │ ├── images │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ ├── 7.png │ │ │ └── 8.png │ └── full_description.txt │ └── ja │ ├── short_description.txt │ └── full_description.txt ├── debug.keystore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitmodules ├── .github └── ISSUE_TEMPLATE │ ├── issue.md │ ├── feature_request.md │ └── bug_report.md ├── settings.gradle.kts ├── LICENSE ├── gradle.properties ├── .gitignore └── gradlew.bat /common/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /common/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/data/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /domain/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /presentation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /presentation/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | CIFS Documents Provider -------------------------------------------------------------------------------- /data/storage/apache/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/storage/apache/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/storage/jcifsng/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/storage/manager/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/storage/smbj/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/storage/smbj/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/storage/interfaces/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/storage/interfaces/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/storage/jcifsng/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/storage/manager/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | CIFS Documents Provider 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/ja/short_description.txt: -------------------------------------------------------------------------------- 1 | 共有ネットワークストレージへのアクセスを提供します 2 | -------------------------------------------------------------------------------- /common/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa2c/cifs-documents-provider/HEAD/debug.keystore -------------------------------------------------------------------------------- /domain/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /presentation/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/storage/smbj/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Provides access to shared network storage. 2 | -------------------------------------------------------------------------------- /data/storage/apache/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/storage/interfaces/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/storage/jcifsng/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/storage/manager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa2c/cifs-documents-provider/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tools/string_converter"] 2 | path = tools/string_converter 3 | url = https://github.com/wa2c/android-string-converter.git 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa2c/cifs-documents-provider/HEAD/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: Report issue 4 | title: '' 5 | labels: '' 6 | assignees: wa2c 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #5f6368 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #9aa0a6 4 | 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa2c/cifs-documents-provider/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa2c/cifs-documents-provider/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa2c/cifs-documents-provider/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa2c/cifs-documents-provider/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa2c/cifs-documents-provider/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa2c/cifs-documents-provider/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa2c/cifs-documents-provider/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wa2c/cifs-documents-provider/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | /misc.xml 5 | /deploymentTargetDropDown.xml 6 | # GitHub Copilot persisted chat sessions 7 | /copilot/chatSessions 8 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_reload_rotate.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri May 05 21:41:29 JST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_roles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/common/OptionItem.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.common 2 | 3 | /** 4 | * Option item. 5 | */ 6 | data class OptionItem( 7 | /** Value */ 8 | val value: T, 9 | /** Label */ 10 | val label: String, 11 | ) 12 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/values/ImportOption.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.values 2 | 3 | /** 4 | * Import option 5 | */ 6 | enum class ImportOption { 7 | /** Replace Data */ 8 | Replace, 9 | /** Overwrite Exits */ 10 | Overwrite, 11 | /** Ignore Data */ 12 | Ignore, 13 | /** Change ID */ 14 | Append, 15 | } 16 | -------------------------------------------------------------------------------- /presentation/src/main/res/xml/locales_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/values/ProtocolType.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.values 2 | 3 | /** 4 | * Protocol type 5 | */ 6 | enum class ProtocolType( 7 | val schema: String, 8 | ) { 9 | /** SMB */ 10 | SMB("smb"), 11 | /** FTP */ 12 | FTP("ftp"), 13 | /** FTP over SSL */ 14 | FTPS("ftps"), 15 | /** SSH FTP */ 16 | SFTP("sftp"), 17 | ; 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/exception/AppException.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.exception 2 | 3 | sealed class AppException(e: Exception?): RuntimeException(e) { 4 | sealed class Settings(e: Exception?) : AppException(e) { 5 | class Empty: Settings(null) 6 | class Import(e: Exception? = null) : Settings(e) 7 | class Export(e: Exception? = null): Settings(e) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_back.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_check_bg.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/csv-editor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_sort.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_folder.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ext/KeyInputType.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ext 2 | 3 | /** 4 | * Key input type 5 | */ 6 | enum class KeyInputType { 7 | /** Not used */ 8 | NOT_USED, 9 | /** External file */ 10 | EXTERNAL_FILE, 11 | /** Import file */ 12 | IMPORTED_FILE, 13 | /** Input text */ 14 | INPUT_TEXT, 15 | ; 16 | 17 | /** Index */ 18 | val index: Int = this.ordinal 19 | } 20 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_check_wn.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /data/data/src/test/java/com/wa2c/android/cifsdocumentsprovider/data/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /domain/src/test/java/com/wa2c/android/cifsdocumentsprovider/domain/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_check_ok.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_save.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_host.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_key.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/test/java/com/wa2c/android/cifsdocumentsprovider/presentation/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/wa2c/android/cifsdocumentsprovider/domain/model/KnownHost.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | /** 7 | * Known Host 8 | */ 9 | @Parcelize 10 | data class KnownHost( 11 | /** Host */ 12 | val host: String, 13 | /** Type */ 14 | val type: String, 15 | /** Key */ 16 | val key: String, 17 | /** Connection list */ 18 | val connections: List, 19 | ): Parcelable 20 | -------------------------------------------------------------------------------- /data/storage/interfaces/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/storage/interfaces/StorageFile.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces 2 | 3 | /** 4 | * Storage File 5 | */ 6 | data class StorageFile( 7 | /** File name */ 8 | val name: String, 9 | /** URI */ 10 | val uri: String, 11 | /** File size */ 12 | val size: Long = 0, 13 | /** Last modified time */ 14 | val lastModified: Long = 0, 15 | /** True if directory */ 16 | val isDirectory: Boolean, 17 | ) 18 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/values/AccessMode.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.values 2 | 3 | /** 4 | * Access Mode 5 | */ 6 | enum class AccessMode( 7 | val smbMode: String, 8 | val safMode: String 9 | ) { 10 | /** Read */ 11 | R("r", "r"), 12 | /** Write */ 13 | W("rw", "w"); 14 | 15 | companion object { 16 | fun fromSafMode(mode: String): AccessMode { 17 | return if (mode.contains(W.safMode, true)) W else R 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /domain/src/main/java/com/wa2c/android/cifsdocumentsprovider/domain/model/RemoteConnectionIndex.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain.model 2 | 3 | import android.os.Parcelable 4 | import com.wa2c.android.cifsdocumentsprovider.common.values.StorageType 5 | import kotlinx.parcelize.Parcelize 6 | 7 | /** 8 | * CIFS Connection 9 | */ 10 | @Parcelize 11 | data class RemoteConnectionIndex( 12 | val id: String, 13 | val name: String, 14 | val storage: StorageType = StorageType.default, 15 | val uri: String, 16 | ): Parcelable 17 | -------------------------------------------------------------------------------- /data/storage/smbj/src/test/java/com/wa2c/android/cifsdocumentsprovider/data/storage/smbj/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.smbj 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_add_folder.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /data/storage/apache/src/test/java/com/wa2c/android/cifsdocumentsprovider/data/storage/apache/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.apache 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /data/storage/jcifsng/src/test/java/com/wa2c/android/cifsdocumentsprovider/data/storage/jcifsng/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.jcifsng 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /data/storage/manager/src/test/java/com/wa2c/android/cifsdocumentsprovider/data/storage/manager/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.manager 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_visibility.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/storage/interfaces/src/test/java/com/wa2c/android/cifsdocumentsprovider/data/storage/interfaces/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /domain/src/main/java/com/wa2c/android/cifsdocumentsprovider/domain/model/HostData.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | /** 7 | * Host Data 8 | */ 9 | @Parcelize 10 | data class HostData( 11 | /** Host Name */ 12 | val hostName: String, 13 | /** IP Address */ 14 | val ipAddress: String, 15 | /** Detection Time */ 16 | val detectionTime: Long, 17 | ): Parcelable { 18 | 19 | val hasHostName: Boolean 20 | get() = ipAddress != hostName 21 | 22 | } -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_check_ng.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_folder_check.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_folder_up.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | maven(url = "https://jitpack.io") 6 | maven(url = "https://plugins.gradle.org/m2/") 7 | } 8 | } 9 | 10 | rootProject.name = "CIFS Documents Provider" 11 | 12 | include(":app") 13 | include(":common") 14 | include(":data:data") 15 | include(":data:storage:interfaces") 16 | include(":data:storage:manager") 17 | include(":data:storage:apache") 18 | include(":data:storage:jcifsng") 19 | include(":data:storage:smbj") 20 | include(":domain") 21 | include(":presentation") 22 | 23 | includeBuild("tools/string_converter") 24 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_reload.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/values/ThumbnailType.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.values 2 | 3 | /** 4 | * Thumbnail type 5 | */ 6 | enum class ThumbnailType( 7 | /** type */ 8 | val type: String 9 | ) { 10 | /** Image */ 11 | IMAGE("image"), 12 | /** Audio */ 13 | AUDIO("audio"), 14 | /** Video */ 15 | VIDEO("video"), 16 | ; 17 | 18 | companion object { 19 | /** Find value by type. */ 20 | fun findByType(type: String?): ThumbnailType? { 21 | return entries.firstOrNull { type?.startsWith(it.type) == true } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /domain/src/main/java/com/wa2c/android/cifsdocumentsprovider/domain/model/RemoteFile.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | /** 7 | * CIFS File 8 | */ 9 | @Parcelize 10 | data class RemoteFile( 11 | /** Connection ID */ 12 | val documentId: DocumentId, 13 | /** File name */ 14 | val name: String, 15 | /** File URI */ 16 | val uri: StorageUri, 17 | /** File size */ 18 | val size: Long = 0, 19 | /** Last modified time */ 20 | val lastModified: Long = 0, 21 | /** True if directory */ 22 | val isDirectory: Boolean, 23 | ) : Parcelable 24 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/common/Divider.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.common 2 | 3 | import androidx.compose.material3.Divider 4 | import androidx.compose.material3.HorizontalDivider 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.unit.dp 7 | 8 | @Composable 9 | fun DividerWide() = HorizontalDivider(thickness = 2.dp, color = Theme.Colors.Divider) 10 | 11 | @Composable 12 | fun DividerNormal() = HorizontalDivider(thickness = 1.dp, color = Theme.Colors.Divider) 13 | 14 | @Composable 15 | fun DividerThin() = HorizontalDivider(thickness = 0.5.dp, color = Theme.Colors.Divider) 16 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/exception/EditException.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.exception 2 | 3 | sealed class EditException(e: Exception?): RuntimeException(e) { 4 | sealed class SaveCheck(e: Exception?) : EditException(e) { 5 | class InputRequiredException : SaveCheck(null) 6 | class InvalidIdException: SaveCheck(null) 7 | class DuplicatedIdException : SaveCheck(null) 8 | } 9 | 10 | sealed class KeyCheck(e: Exception?) : EditException(e) { 11 | class AccessFailedException(e: Exception? = null) : KeyCheck(e) 12 | class InvalidException(e: Exception? = null) : KeyCheck(e) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules_v31.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: wa2c 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /domain/src/main/java/com/wa2c/android/cifsdocumentsprovider/domain/model/ConnectionResult.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain.model 2 | 3 | /** 4 | * Server connection result 5 | */ 6 | sealed class ConnectionResult { 7 | 8 | abstract val cause: Throwable? 9 | 10 | /** Success */ 11 | data object Success: ConnectionResult() { 12 | override val cause: Throwable? = null 13 | } 14 | /** Warning */ 15 | data class Warning( 16 | override val cause: Throwable = RuntimeException() 17 | ): ConnectionResult() 18 | /** Failure */ 19 | data class Failure( 20 | override val cause: Throwable = RuntimeException() 21 | ): ConnectionResult() 22 | } 23 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_check_uc.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/wa2c/android/cifsdocumentsprovider/App.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider 2 | 3 | import android.app.Application 4 | import com.wa2c.android.cifsdocumentsprovider.common.utils.initLog 5 | import com.wa2c.android.cifsdocumentsprovider.domain.repository.AppRepository 6 | import dagger.hilt.android.HiltAndroidApp 7 | import kotlinx.coroutines.runBlocking 8 | import javax.inject.Inject 9 | 10 | @HiltAndroidApp 11 | class App: Application() { 12 | 13 | @Inject 14 | lateinit var repository: AppRepository 15 | 16 | override fun onCreate() { 17 | super.onCreate() 18 | 19 | initLog(BuildConfig.DEBUG) 20 | runBlocking { 21 | repository.migrate() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/values/HostSortType.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.values 2 | 3 | enum class HostSortType( 4 | val intValue: Int, 5 | ) { 6 | DetectionAscend(10), 7 | DetectionDescend(1), 8 | HostNameAscend(20), 9 | HostNameDescend(21), 10 | IpAddressAscend(30), 11 | IpAddressDescend(31), 12 | ; 13 | 14 | companion object { 15 | 16 | /** Default sort type */ 17 | val default = DetectionAscend 18 | 19 | /** 20 | * Find soft type or default (TimeAscend). 21 | */ 22 | fun findByValueOrDefault(value: Int?): HostSortType { 23 | return entries.firstOrNull { it.intValue == value } ?: default 24 | } 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /common/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/common/AppTopAppBarColors.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.common 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.TopAppBarColors 6 | import androidx.compose.material3.TopAppBarDefaults 7 | import androidx.compose.runtime.Composable 8 | 9 | @OptIn(ExperimentalMaterial3Api::class) 10 | @Composable 11 | fun getAppTopAppBarColors(): TopAppBarColors { 12 | return TopAppBarDefaults.mediumTopAppBarColors( 13 | containerColor = MaterialTheme.colorScheme.primary, 14 | navigationIconContentColor = MaterialTheme.colorScheme.onSurface, 15 | titleContentColor = MaterialTheme.colorScheme.onSurface, 16 | actionIconContentColor = MaterialTheme.colorScheme.onSurface, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/values/UiTheme.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.values 2 | 3 | /** 4 | * UI theme 5 | */ 6 | enum class UiTheme( 7 | /** Key */ 8 | val key: String 9 | ) { 10 | /** Default */ 11 | DEFAULT("default"), 12 | /** Light */ 13 | LIGHT("light"), 14 | /** Dark */ 15 | DARK("dark"), 16 | ; 17 | 18 | /** Index */ 19 | val index: Int = this.ordinal 20 | 21 | companion object { 22 | /** Find value or default by key. */ 23 | fun findByKeyOrDefault(key: String?): UiTheme { 24 | return entries.firstOrNull { it.key == key } ?: DEFAULT 25 | } 26 | 27 | /** Find value or default by index. */ 28 | fun findByIndexOrDefault(index: Int?): UiTheme { 29 | return entries.firstOrNull { it.index == index } ?: DEFAULT 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /presentation/src/androidTest/java/com/wa2c/android/cifsdocumentsprovider/presentation/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals( 21 | "com.wa2c.android.cifsdocumentsprovider.presentation.test", 22 | appContext.packageName 23 | ) 24 | } 25 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/worker/WorkerLifecycleOwner.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.worker 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.lifecycle.LifecycleOwner 7 | import androidx.lifecycle.LifecycleRegistry 8 | 9 | /** 10 | * Worker lifecycle owner 11 | */ 12 | class WorkerLifecycleOwner : LifecycleOwner { 13 | private val lifecycleRegistry = LifecycleRegistry(this) 14 | private val handler = Handler(Looper.getMainLooper()) 15 | override val lifecycle: Lifecycle get() = lifecycleRegistry 16 | fun start() { 17 | handler.postAtFrontOfQueue { 18 | lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) 19 | } 20 | } 21 | 22 | fun stop() { 23 | handler.postAtFrontOfQueue { 24 | lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/values/StorageType.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.values 2 | 3 | /** 4 | * Storage type 5 | */ 6 | enum class StorageType( 7 | val value: String, 8 | val protocol: ProtocolType, 9 | ) { 10 | /** JCIFS-NG */ 11 | JCIFS("JCIFS", ProtocolType.SMB), 12 | /** SMBJ */ 13 | SMBJ("SMBJ", ProtocolType.SMB), 14 | /** JCIFS */ 15 | JCIFS_LEGACY("JCIFS_LEGACY", ProtocolType.SMB), 16 | /** Apache FTP */ 17 | APACHE_FTP("APACHE_FTP", ProtocolType.FTP), 18 | /** Apache FTPS */ 19 | APACHE_FTPS("APACHE_FTPS", ProtocolType.FTPS), 20 | /** Apache SFTP */ 21 | APACHE_SFTP("APACHE_SFTP", ProtocolType.SFTP), 22 | ; 23 | 24 | companion object { 25 | val default: StorageType = JCIFS 26 | 27 | /** 28 | * Find storage type. 29 | */ 30 | fun findByValue(value: String): StorageType? { 31 | return entries.firstOrNull { it.value == value } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /domain/src/main/java/com/wa2c/android/cifsdocumentsprovider/domain/model/SendDataState.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain.model 2 | 3 | /** 4 | * Send data state. 5 | */ 6 | enum class SendDataState { 7 | /** In ready */ 8 | READY, 9 | /** In confirmation */ 10 | CONFIRM, 11 | /** In overwriting */ 12 | OVERWRITE, 13 | /** In progress */ 14 | PROGRESS, 15 | /** Succeeded */ 16 | SUCCESS, 17 | /** Failed */ 18 | FAILURE, 19 | /** Canceled */ 20 | CANCEL, 21 | ; 22 | 23 | val isReady: Boolean 24 | get() = this == READY 25 | 26 | val inProgress: Boolean 27 | get() = this == PROGRESS 28 | 29 | val isCancelable: Boolean 30 | get() = this == READY || this == PROGRESS 31 | 32 | val isRetryable: Boolean 33 | get() = this == OVERWRITE || this == FAILURE || this == CANCEL 34 | 35 | val isFinished: Boolean 36 | get() = this == SUCCESS || this == FAILURE || this == CANCEL 37 | 38 | val isIncomplete: Boolean 39 | get() = this == FAILURE || this == CANCEL 40 | 41 | } 42 | -------------------------------------------------------------------------------- /common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.kotlin.parcelize) 5 | alias(libs.plugins.kotlin.serialization) 6 | } 7 | 8 | val applicationId: String by rootProject.extra 9 | val javaVersion: JavaVersion by rootProject.extra 10 | val androidCompileSdk: Int by rootProject.extra 11 | val androidMinSdk: Int by rootProject.extra 12 | 13 | android { 14 | compileSdk = androidCompileSdk 15 | namespace = "${applicationId}.common" 16 | 17 | defaultConfig { 18 | minSdk = androidMinSdk 19 | 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | consumerProguardFiles("consumer-rules.pro") 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility = javaVersion 26 | targetCompatibility = javaVersion 27 | } 28 | 29 | kotlin { 30 | jvmToolchain { 31 | languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) 32 | } 33 | } 34 | } 35 | 36 | dependencies { 37 | implementation(libs.timber) 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 wa2c 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /data/data/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/db/AppDb.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.db 2 | 3 | import android.content.Context 4 | import androidx.room.AutoMigration 5 | import androidx.room.Database 6 | import androidx.room.Room 7 | import androidx.room.RoomDatabase 8 | 9 | @Database( 10 | entities = [ 11 | ConnectionSettingEntity::class, 12 | ], 13 | version = AppDatabase.DB_VERSION, 14 | exportSchema = true, 15 | autoMigrations = [ 16 | AutoMigration (from = 1, to = 2) 17 | ] 18 | ) 19 | internal abstract class AppDatabase : RoomDatabase() { 20 | 21 | abstract fun getStorageSettingDao(): ConnectionSettingDao 22 | 23 | companion object { 24 | /** DB name */ 25 | private const val DB_NAME = "app.db" 26 | /** DB version */ 27 | const val DB_VERSION = 2 28 | 29 | /** 30 | * Build DB 31 | */ 32 | fun buildDb(context: Context) : AppDatabase { 33 | return Room.databaseBuilder(context, AppDatabase::class.java, DB_NAME) 34 | .build() 35 | } 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.home 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.wa2c.android.cifsdocumentsprovider.domain.repository.AppRepository 5 | import com.wa2c.android.cifsdocumentsprovider.presentation.ext.MainCoroutineScope 6 | import dagger.hilt.android.lifecycle.HiltViewModel 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.runBlocking 9 | import javax.inject.Inject 10 | 11 | /** 12 | * Home Screen ViewModel 13 | */ 14 | @HiltViewModel 15 | class HomeViewModel @Inject constructor( 16 | private val appRepository: AppRepository, 17 | ): ViewModel(), CoroutineScope by MainCoroutineScope() { 18 | 19 | val connectionListFlow = appRepository.connectionListFlow 20 | 21 | /** 22 | * Move item. 23 | */ 24 | fun onItemMove(fromPosition: Int, toPosition: Int) { 25 | runBlocking { 26 | // run blocking for drag animation 27 | appRepository.moveConnection(fromPosition, toPosition) 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/values/Const.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.values 2 | 3 | const val URI_AUTHORITY = "com.wa2c.android.cifsdocumentsprovider.documents" 4 | 5 | const val URI_START = "://" 6 | const val URI_SEPARATOR = '/' 7 | const val UNC_START = "\\\\" 8 | const val UNC_SEPARATOR = "\\" 9 | 10 | const val DOCUMENT_ID_DELIMITER = ":" 11 | 12 | const val USER_GUEST = "guest" 13 | const val DEFAULT_ENCODING = "UTF-8" 14 | 15 | const val CONNECTION_TIMEOUT = 10000 16 | const val READ_TIMEOUT = 10000 17 | const val BUFFER_SIZE = 1024 * 1024 18 | const val CACHE_TIMEOUT = 300 * 1000 19 | const val OPEN_FILE_LIMIT_DEFAULT = 30 20 | const val OPEN_FILE_LIMIT_MIN = 1 21 | const val OPEN_FILE_LIMIT_MAX = 999 22 | 23 | const val NOTIFICATION_CHANNEL_ID_SEND = "notification_channel_send" 24 | const val NOTIFICATION_ID_SEND = 100 25 | const val NOTIFICATION_CHANNEL_ID_PROVIDER = "notification_channel_provider" 26 | const val NOTIFICATION_ID_PROVIDER = 101 27 | 28 | const val DEFAULT_FTPS_IMPLICIT_PORT = 990 29 | 30 | const val PASSWORD_LENGTH_16 = 16 31 | const val PASSWORD_LENGTH_32 = 32 32 | -------------------------------------------------------------------------------- /presentation/src/main/res/values/strings-common.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | CIFS Documents Provider 7 | 8 | English 9 | 日本語 10 | العربية 11 | Slovenský 12 | 简体中文 13 | မြန်မာစာ 14 | Русский 15 | 16 | SMB2,3 (jCIFS NG) 17 | SMB2,3 (SMBJ) 18 | SMB1 (jCIFS NG) 19 | FTP (Apache Commons) 20 | FTP over SSL/TLS (Apache Commons) 21 | SSH FTP (Apache Commons) 22 | 23 | 24 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/common/MutableStateAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.common 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.MutableState 5 | import androidx.compose.runtime.State 6 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlin.coroutines.CoroutineContext 9 | import kotlin.coroutines.EmptyCoroutineContext 10 | 11 | 12 | internal class MutableStateAdapter( 13 | private val state: State, 14 | private val mutate: (T) -> Unit 15 | ) : MutableState { 16 | 17 | override var value: T 18 | get() = state.value 19 | set(value) { mutate(value) } 20 | 21 | override fun component1(): T = value 22 | override fun component2(): (T) -> Unit = { value = it } 23 | } 24 | 25 | 26 | @Composable 27 | internal fun MutableStateFlow.collectAsMutableState( 28 | context: CoroutineContext = EmptyCoroutineContext 29 | ): MutableState = MutableStateAdapter( 30 | state = collectAsStateWithLifecycle(context = context), 31 | mutate = { value = it } 32 | ) 33 | -------------------------------------------------------------------------------- /data/data/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/DataModule.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data 2 | 3 | import android.content.Context 4 | import com.wa2c.android.cifsdocumentsprovider.data.db.AppDatabase 5 | import com.wa2c.android.cifsdocumentsprovider.data.preference.AppPreferencesDataStore 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.qualifiers.ApplicationContext 10 | import dagger.hilt.components.SingletonComponent 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | internal object DataModule { 16 | 17 | /** AppDatabase */ 18 | @Singleton 19 | @Provides 20 | fun provideDatabase( 21 | @ApplicationContext context: Context 22 | ) = AppDatabase.buildDb(context) 23 | 24 | /** StorageSettingDao */ 25 | @Singleton 26 | @Provides 27 | fun provideDao(db: AppDatabase) = db.getStorageSettingDao() 28 | 29 | 30 | /** DataStore */ 31 | @Singleton 32 | @Provides 33 | fun providePreferencesDataStore( 34 | @ApplicationContext context: Context 35 | ): AppPreferencesDataStore = AppPreferencesDataStore(context) 36 | 37 | } 38 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ext/Language.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ext 2 | 3 | import java.util.Locale 4 | 5 | /** 6 | * Language 7 | */ 8 | enum class Language( 9 | /** Language code */ 10 | val code: String 11 | ) { 12 | /** English */ 13 | ENGLISH("en"), 14 | /** Japanese */ 15 | JAPANESE("ja"), 16 | /** Arabic */ 17 | ARABIC("ar"), 18 | /** Slovak */ 19 | SLOVAK("sk"), 20 | /** Chinese */ 21 | CHINESE("zh"), 22 | /** Burmese */ 23 | MYANMAR("my"), 24 | /** Russian */ 25 | RUSSIAN("ru"), 26 | ; 27 | 28 | companion object { 29 | val default: Language 30 | get() = findByCodeOrDefault(Locale.getDefault().language) 31 | 32 | /** Find value or default by code */ 33 | fun findByCodeOrDefault(code: String?): Language { 34 | val locale = Locale.getDefault() 35 | return entries.firstOrNull { it.code == code } 36 | ?: entries.firstOrNull { it.code == locale.toLanguageTag() } 37 | ?: entries.firstOrNull { it.code == locale.language } 38 | ?: ENGLISH 39 | } 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /data/storage/interfaces/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.kotlin.serialization) 5 | } 6 | 7 | val applicationId: String by rootProject.extra 8 | val javaVersion: JavaVersion by rootProject.extra 9 | val androidCompileSdk: Int by rootProject.extra 10 | val androidMinSdk: Int by rootProject.extra 11 | 12 | android { 13 | compileSdk = androidCompileSdk 14 | namespace = "${applicationId}.data.storage.interfaces" 15 | 16 | defaultConfig { 17 | minSdk = androidMinSdk 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | consumerProguardFiles("consumer-rules.pro") 21 | } 22 | 23 | compileOptions { 24 | sourceCompatibility = javaVersion 25 | targetCompatibility = javaVersion 26 | } 27 | 28 | kotlin { 29 | jvmToolchain { 30 | languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) 31 | } 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation(project(":common")) 37 | implementation(libs.kotlinx.coroutines.android) 38 | implementation(libs.kotlinx.serialization.json) 39 | testImplementation(libs.junit) 40 | } 41 | -------------------------------------------------------------------------------- /data/storage/jcifsng/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | val applicationId: String by rootProject.extra 7 | val javaVersion: JavaVersion by rootProject.extra 8 | val androidCompileSdk: Int by rootProject.extra 9 | val androidMinSdk: Int by rootProject.extra 10 | 11 | android { 12 | compileSdk = androidCompileSdk 13 | namespace = "${applicationId}.data.storage.jcifsng" 14 | 15 | defaultConfig { 16 | minSdk = androidMinSdk 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | consumerProguardFiles("consumer-rules.pro") 20 | } 21 | 22 | compileOptions { 23 | sourceCompatibility = javaVersion 24 | targetCompatibility = javaVersion 25 | } 26 | 27 | kotlin { 28 | jvmToolchain { 29 | languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) 30 | } 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation(project(":common")) 36 | implementation(project(":data:storage:interfaces")) 37 | 38 | implementation(libs.kotlinx.coroutines.android) 39 | implementation(libs.jcifs.ng) 40 | 41 | testImplementation(libs.junit) 42 | } 43 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/edit/components/SubsectionTitle.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.edit.components 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.text.font.FontWeight 9 | import androidx.compose.ui.tooling.preview.Preview 10 | import androidx.compose.ui.unit.sp 11 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.Theme 12 | 13 | @Composable 14 | fun SubsectionTitle( 15 | text: String, 16 | ) { 17 | Text( 18 | text = text, 19 | fontSize = 12.sp, 20 | fontWeight = FontWeight.Bold, 21 | modifier = Modifier 22 | .padding(top = Theme.Sizes.S) 23 | ) 24 | } 25 | 26 | 27 | @Preview( 28 | name = "Preview", 29 | group = "Group", 30 | uiMode = Configuration.UI_MODE_NIGHT_YES, 31 | showBackground = true, 32 | ) 33 | @Composable 34 | private fun SubsectionTitlePreview() { 35 | Theme.AppTheme { 36 | SubsectionTitle( 37 | text = "Subsection Title", 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /domain/src/main/java/com/wa2c/android/cifsdocumentsprovider/domain/DomainModule.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.Dispatchers 9 | import javax.inject.Qualifier 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | object CoroutineDispatcherModule { 14 | @DefaultDispatcher 15 | @Provides 16 | fun provideDefaultDispatcher(): CoroutineDispatcher { 17 | return Dispatchers.Default 18 | } 19 | 20 | @IoDispatcher 21 | @Provides 22 | fun provideIODispatcher(): CoroutineDispatcher { 23 | return Dispatchers.IO 24 | } 25 | 26 | @MainDispatcher 27 | @Provides 28 | fun provideMainDispatcher(): CoroutineDispatcher { 29 | Dispatchers.Unconfined 30 | return Dispatchers.Main 31 | } 32 | } 33 | 34 | @Qualifier 35 | @Retention(AnnotationRetention.BINARY) 36 | annotation class IoDispatcher 37 | 38 | @Qualifier 39 | @Retention(AnnotationRetention.BINARY) 40 | annotation class MainDispatcher 41 | 42 | @Qualifier 43 | @Retention(AnnotationRetention.BINARY) 44 | annotation class DefaultDispatcher 45 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/utils/LogUtils.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.utils 2 | 3 | import timber.log.Timber 4 | 5 | /** 6 | * Initialize log 7 | */ 8 | fun initLog(isDebug: Boolean) { 9 | // Set logger 10 | if (isDebug) { 11 | Timber.plant(Timber.DebugTree()) 12 | } 13 | } 14 | 15 | /** Output the verbose message */ 16 | fun logV(obj: Any?, vararg args: Any?) = run { 17 | if (obj is Throwable) { Timber.asTree().v(obj) } 18 | Timber.asTree().v(obj.toString(), *args) 19 | } 20 | /** Output the debug message */ 21 | fun logD(obj: Any?, vararg args: Any?) = run { 22 | if (obj is Throwable) { Timber.asTree().d(obj) } 23 | Timber.asTree().d(obj.toString(), *args) 24 | } 25 | /** Output the info message */ 26 | fun logI(obj: Any?, vararg args: Any?) = run { 27 | if (obj is Throwable) { Timber.asTree().i(obj) } 28 | Timber.asTree().i(obj.toString(), *args) 29 | } 30 | /** Output the warning message */ 31 | fun logW(obj: Any?, vararg args: Any?) = run { 32 | if (obj is Throwable) { Timber.asTree().w(obj) } 33 | Timber.asTree().w(obj.toString(), *args) 34 | } 35 | /** Output the error message */ 36 | fun logE(obj: Any?, vararg args: Any?) = run { 37 | if (obj is Throwable) { Timber.asTree().e(obj) } 38 | Timber.asTree().e(obj.toString(), *args) 39 | } 40 | -------------------------------------------------------------------------------- /data/storage/interfaces/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/storage/interfaces/StorageClient.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces 2 | 3 | import android.os.ProxyFileDescriptorCallback 4 | import com.wa2c.android.cifsdocumentsprovider.common.values.AccessMode 5 | 6 | interface StorageClient { 7 | 8 | suspend fun getFile(request: StorageRequest, ignoreCache: Boolean = false): StorageFile 9 | 10 | suspend fun getChildren(request: StorageRequest, ignoreCache: Boolean = false): List 11 | 12 | suspend fun createDirectory(request: StorageRequest): StorageFile 13 | 14 | suspend fun createFile(request: StorageRequest): StorageFile 15 | 16 | suspend fun copyFile(sourceRequest: StorageRequest, targetRequest: StorageRequest): StorageFile 17 | 18 | suspend fun renameFile(request: StorageRequest, newName: String): StorageFile 19 | 20 | suspend fun moveFile(sourceRequest: StorageRequest, targetRequest: StorageRequest): StorageFile 21 | 22 | suspend fun deleteFile(request: StorageRequest): Boolean 23 | 24 | suspend fun getProxyFileDescriptorCallback(request: StorageRequest, mode: AccessMode, onFileRelease: suspend () -> Unit): ProxyFileDescriptorCallback 25 | 26 | suspend fun removeCache(request: StorageRequest? = null): Boolean 27 | 28 | suspend fun close() 29 | 30 | } 31 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/common/LoadingBox.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.common 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.CircularProgressIndicator 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | 14 | @Composable 15 | fun LoadingBox(isLoading: Boolean) { 16 | if (!isLoading) return 17 | val interactionSource = remember { MutableInteractionSource() } 18 | Box( 19 | modifier = Modifier 20 | .fillMaxSize() 21 | .background(Theme.Colors.LoadingBackground) 22 | .clickable( 23 | enabled = isLoading, 24 | indication = null, 25 | interactionSource = interactionSource, 26 | onClick = {} 27 | ), 28 | ) { 29 | CircularProgressIndicator( 30 | modifier = Modifier 31 | .align(Alignment.Center) 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /data/data/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/db/ConnectionSettingEntity.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.db 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.Index 6 | import androidx.room.PrimaryKey 7 | import com.wa2c.android.cifsdocumentsprovider.common.values.StorageType 8 | import kotlinx.serialization.Serializable 9 | 10 | @Serializable 11 | @Entity( 12 | tableName = ConnectionSettingEntity.TABLE_NAME, 13 | indices = [ 14 | Index(value = ["sort_order"]), 15 | ] 16 | ) 17 | data class ConnectionSettingEntity( 18 | /** ID */ 19 | @PrimaryKey 20 | @ColumnInfo(name = "id") 21 | val id: String, 22 | /** Name */ 23 | @ColumnInfo(name = "name") 24 | val name: String, 25 | /** Type */ 26 | @ColumnInfo(name = "type") 27 | val type: String = StorageType.default.value, 28 | /** URI */ 29 | @ColumnInfo(name = "uri") 30 | val uri: String, 31 | /** Data (Encrypted) */ 32 | @ColumnInfo(name = "data") 33 | val data: String, 34 | @ColumnInfo(name = "sort_order") 35 | val sortOrder: Int, 36 | /** Modified Date */ 37 | @ColumnInfo(name = "modified_date") 38 | val modifiedDate: Long, 39 | ) { 40 | companion object { 41 | /** Table name. */ 42 | const val TABLE_NAME = "connection_setting" 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /data/storage/apache/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | val applicationId: String by rootProject.extra 7 | val javaVersion: JavaVersion by rootProject.extra 8 | val androidCompileSdk: Int by rootProject.extra 9 | val androidMinSdk: Int by rootProject.extra 10 | 11 | android { 12 | compileSdk = androidCompileSdk 13 | namespace = "${applicationId}.data.storage.apache" 14 | 15 | defaultConfig { 16 | minSdk = androidMinSdk 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | consumerProguardFiles("consumer-rules.pro") 20 | } 21 | 22 | compileOptions { 23 | sourceCompatibility = javaVersion 24 | targetCompatibility = javaVersion 25 | } 26 | 27 | kotlin { 28 | jvmToolchain { 29 | languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) 30 | } 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation(project(":common")) 36 | implementation(project(":data:storage:interfaces")) 37 | 38 | implementation(libs.kotlinx.coroutines.android) 39 | implementation(libs.androidx.documentfile) 40 | implementation(libs.apache.commons.net) 41 | implementation(libs.apache.commons.vfs) 42 | implementation(libs.jsch) 43 | 44 | testImplementation(libs.junit) 45 | } 46 | -------------------------------------------------------------------------------- /domain/src/main/java/com/wa2c/android/cifsdocumentsprovider/domain/model/SendData.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain.model 2 | 3 | import android.net.Uri 4 | 5 | /** 6 | * Send Data 7 | */ 8 | data class SendData( 9 | /** Unique ID */ 10 | val id: String, 11 | /** File name */ 12 | val name: String, 13 | /** File size */ 14 | val size: Long, 15 | /** File mime type */ 16 | val mimeType: String, 17 | /** Source file URI (File URI) */ 18 | val sourceFileUri: Uri, 19 | /** Target file URI (File URI) */ 20 | val targetFileUri: Uri, 21 | /** Send start time. */ 22 | val startTime: Long = 0, 23 | /** Send progress size. */ 24 | val progressSize: Long = 0, 25 | /** True if success */ 26 | val state: SendDataState = SendDataState.READY, 27 | ) { 28 | /** Progress percentage */ 29 | val progress: Int 30 | get() = if (progressSize > 0) (progressSize * 100 / size).toInt() else 0 31 | 32 | /** Elapsed Time */ 33 | val elapsedTime: Long 34 | get() = (System.currentTimeMillis() - startTime) 35 | 36 | /** Speed (bps) */ 37 | val bps: Long 38 | get() = (elapsedTime / 1000).let { if (it > 0) { progressSize / it } else { 0 } } 39 | 40 | } 41 | 42 | /** 43 | * Get current ready data 44 | */ 45 | fun List.getCurrentReady(): SendData? { 46 | return firstOrNull { it.state.isReady } 47 | } 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: wa2c 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **CIFS Documents Provider** 27 | - App Versin: [e.g. 2.0.0] 28 | - Connection Settings 29 | - Storage Type: [e.g. SMB2,3(JCIFS-NG)] 30 | - Authantication: [e.g. Guest, Anonymous, User&Password] 31 | - Safe Data Taransfer: [e.g. yes] 32 | - Other: ... 33 | - App Settings 34 | - Show notification when opening files: [e.g. no] 35 | - Use as local storage: [e.g. yes] 36 | - Other: ... 37 | 38 | **App installed device (please complete the following information):** 39 | - OS: [e.g. Android 14] 40 | - Device: [e.g. Pixel 8] 41 | - Cliant App: [e.g. Microsoft Word 16.0] 42 | 43 | **Server (please complete the following information):** 44 | - OS: [e.g. Windows 11, Ubuntu 24.04] 45 | - Device: [e.g. Surface Laptop 5] 46 | - Server Software: [e.g. Samba 4.19] 47 | 48 | **Additional context** 49 | Add any other context about the problem here. 50 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/edit/components/UriText.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.edit.components 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.text.selection.SelectionContainer 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.Theme 12 | 13 | @Composable 14 | fun UriText( 15 | uriText: String, 16 | onCopyToClipboard: (String) -> Unit, 17 | ) { 18 | SelectionContainer { 19 | Text( 20 | text = uriText, 21 | modifier = Modifier 22 | .padding(Theme.Sizes.S) 23 | .clickable { 24 | onCopyToClipboard(uriText) 25 | } 26 | ) 27 | } 28 | } 29 | 30 | @Preview( 31 | name = "Preview", 32 | group = "Group", 33 | uiMode = Configuration.UI_MODE_NIGHT_YES, 34 | showBackground = true, 35 | ) 36 | @Composable 37 | private fun UriTextPreview() { 38 | Theme.AppTheme { 39 | UriText( 40 | uriText = "https://example.com/test", 41 | onCopyToClipboard = { } 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /data/storage/smbj/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | val applicationId: String by rootProject.extra 7 | val javaVersion: JavaVersion by rootProject.extra 8 | val androidCompileSdk: Int by rootProject.extra 9 | val androidMinSdk: Int by rootProject.extra 10 | 11 | android { 12 | compileSdk = androidCompileSdk 13 | namespace = "${applicationId}.data.storage.smbj" 14 | 15 | defaultConfig { 16 | minSdk = androidMinSdk 17 | 18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 19 | consumerProguardFiles("consumer-rules.pro") 20 | } 21 | 22 | compileOptions { 23 | sourceCompatibility = javaVersion 24 | targetCompatibility = javaVersion 25 | } 26 | 27 | kotlin { 28 | jvmToolchain { 29 | languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) 30 | } 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation(project(":common")) 36 | implementation(project(":data:storage:interfaces")) 37 | 38 | implementation(libs.kotlinx.coroutines.android) 39 | 40 | implementation(libs.smbj) 41 | implementation(libs.dcerpc) { 42 | exclude(group = "org.bouncycastle", module = "bcprov-jdk15on") // Duplicate with JCIFS-NG 43 | } 44 | implementation(libs.listenablefuture) // to avoid conflict 45 | 46 | testImplementation(libs.junit) 47 | } 48 | -------------------------------------------------------------------------------- /fastlane/metadata/android/ja/full_description.txt: -------------------------------------------------------------------------------- 1 | CIFS Documents Providerは、共有オンラインストレージへのアクセスを提供するAndroidアプリです。 2 | 3 | [機能・仕様] 4 | 5 | * Storage Access Framework (SAF) を介した、共有オンラインストレージへのアクセスを他のアプリに提供します。 6 | * ファイルおよびディレクトリに対するアクセスを提供します。 7 | * SMB、FTP、FTPS、SFTPをサポートしています。 8 | * オンラインストレージのファイル共有、転送を行うことができます。 9 | * 複数の接続設定を保存することができます。 10 | * 接続設定のエクスポート/インポートに対応しています。 11 | * 複数の言語に対応しています。 12 | * ダークモードに対応しています。 13 | * ローカルストレージとして扱うことができます。(要設定) 14 | * 通知を表示してタスクキルを防止できます。(要設定) 15 | 16 | [主な用途] 17 | 18 | * アプリで作成したファイルのインポートおよびエクスポート。 19 | * ストレージ管理アプリによるファイルおよびディレクトリの操作。 20 | * メディアプレイヤーアプリによる、音楽、動画等の再生。 21 | * カメラアプリで撮影した写真の保存。 22 | 23 | [注意] 24 | 25 | * 本アプリにファイル管理機能はありません。 26 | * 他のアプリから利用するためには、SAF(Storage Access Framework)をサポートしている必要があります。 27 | * ローカルストレージを前提としたアプリでは、正常に動作しない場合があります。 28 | * 音声や動画のストリーミングデータの保存先に指定すると、アプリがクラッシュする場合があります。 29 | 30 | [使用方法] 31 | 32 | 次のページを参照してください。 33 | https://github.com/wa2c/cifs-documents-provider/wiki/Manual-ja 34 | 35 | [ソースコード] 36 | 37 | GitHub 38 | https://github.com/wa2c/cifs-documents-provider 39 | 40 | [課題管理] 41 | 42 | GitHub Issue 43 | https://github.com/wa2c/cifs-documents-provider/issues 44 | 45 | バグ報告、機能要望、その他情報等があればこちらに書き込んでください。 (日本語または英語) 46 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/PresentationModule.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation 2 | 3 | import android.content.Context 4 | import com.wa2c.android.cifsdocumentsprovider.domain.repository.StorageRepository 5 | import com.wa2c.android.cifsdocumentsprovider.domain.repository.SendRepository 6 | import dagger.Module 7 | import dagger.hilt.EntryPoint 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.EntryPointAccessors 10 | import dagger.hilt.components.SingletonComponent 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | internal object PresentationModule { 15 | 16 | @EntryPoint 17 | @InstallIn(SingletonComponent::class) 18 | interface DocumentsProviderEntryPoint { 19 | fun getStorageRepository(): StorageRepository 20 | } 21 | 22 | @EntryPoint 23 | @InstallIn(SingletonComponent::class) 24 | interface SendEntryPoint { 25 | fun getSendRepository(): SendRepository 26 | } 27 | 28 | } 29 | 30 | fun provideStorageRepository(context: Context): StorageRepository { 31 | val clazz = PresentationModule.DocumentsProviderEntryPoint::class.java 32 | val hiltEntryPoint = EntryPointAccessors.fromApplication(context, clazz) 33 | return hiltEntryPoint.getStorageRepository() 34 | } 35 | 36 | fun provideSendRepository(context: Context): SendRepository { 37 | val clazz = PresentationModule.SendEntryPoint::class.java 38 | val hiltEntryPoint = EntryPointAccessors.fromApplication(context, clazz) 39 | return hiltEntryPoint.getSendRepository() 40 | } 41 | -------------------------------------------------------------------------------- /domain/src/main/java/com/wa2c/android/cifsdocumentsprovider/domain/repository/HostRepository.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain.repository 2 | 3 | import com.wa2c.android.cifsdocumentsprovider.common.values.HostSortType 4 | import com.wa2c.android.cifsdocumentsprovider.data.HostFinder 5 | import com.wa2c.android.cifsdocumentsprovider.data.preference.AppPreferencesDataStore 6 | import com.wa2c.android.cifsdocumentsprovider.domain.model.HostData 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.map 9 | import javax.inject.Inject 10 | import javax.inject.Singleton 11 | 12 | /** 13 | * Host Repository 14 | */ 15 | @Singleton 16 | class HostRepository @Inject internal constructor( 17 | private val hostFinder: HostFinder, 18 | private val preferences: AppPreferencesDataStore, 19 | ) { 20 | 21 | /** Sort type */ 22 | val hostSortTypeFlow = preferences.hostSortTypeFlow 23 | 24 | /** Sort type */ 25 | suspend fun setSortType(value: HostSortType) = preferences.setHostSortType(value) 26 | 27 | /** Host flow */ 28 | val hostFlow: Flow = hostFinder.hostFlow.map { ipHost -> 29 | ipHost?.let { 30 | HostData( 31 | ipAddress = it.first, 32 | hostName = it.second, 33 | detectionTime = System.currentTimeMillis() 34 | ) 35 | } 36 | } 37 | 38 | /** 39 | * Start discovery 40 | */ 41 | suspend fun startDiscovery() = hostFinder.startDiscovery() 42 | 43 | /** 44 | * Stop discovery 45 | */ 46 | suspend fun stopDiscovery() = hostFinder.stopDiscovery() 47 | 48 | } 49 | -------------------------------------------------------------------------------- /data/storage/interfaces/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/storage/interfaces/utils/DataUtils.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.utils 2 | 3 | import com.wa2c.android.cifsdocumentsprovider.common.utils.getUriText 4 | import com.wa2c.android.cifsdocumentsprovider.common.values.StorageType 5 | import com.wa2c.android.cifsdocumentsprovider.common.values.UNC_SEPARATOR 6 | import com.wa2c.android.cifsdocumentsprovider.common.values.UNC_START 7 | import com.wa2c.android.cifsdocumentsprovider.common.values.URI_SEPARATOR 8 | 9 | /** 10 | * Rename URI name 11 | */ 12 | fun String.rename(newName: String): String { 13 | return substringBeforeLast(URI_SEPARATOR) + URI_SEPARATOR + newName 14 | } 15 | 16 | /** Convert UNC Path (\\\\ to URI (smb:////) */ 17 | fun String.uncPathToUri(isDirectory: Boolean): String? { 18 | val elements = this.substringAfter(UNC_START).split(UNC_SEPARATOR).ifEmpty { return null } 19 | val params = elements.getOrNull(0)?.split('@') ?: return null 20 | val server = params.getOrNull(0) ?: return null 21 | val port = if (params.size >= 2) params.lastOrNull() else null 22 | val path = elements.subList(1, elements.size).joinToString(UNC_SEPARATOR) 23 | return getUriText(StorageType.SMBJ, server, port, path, isDirectory) 24 | } 25 | 26 | /** 27 | * Convert path separator to UNC 28 | */ 29 | fun String.toUncSeparator(): String { 30 | return this.replace(URI_SEPARATOR.toString(), UNC_SEPARATOR) 31 | } 32 | 33 | /** True if invalid file name */ 34 | val String.isInvalidFileName: Boolean 35 | get() = this == "." || this == ".." 36 | -------------------------------------------------------------------------------- /data/storage/interfaces/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/storage/interfaces/utils/ErrorUtils.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.utils 2 | 3 | import android.system.ErrnoException 4 | import android.system.OsConstants 5 | import com.wa2c.android.cifsdocumentsprovider.common.exception.StorageException 6 | import com.wa2c.android.cifsdocumentsprovider.common.utils.logE 7 | import com.wa2c.android.cifsdocumentsprovider.common.values.AccessMode 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.runBlocking 10 | import java.io.IOException 11 | import kotlin.coroutines.CoroutineContext 12 | 13 | /** 14 | * Proxy Callback process 15 | */ 16 | @Throws(ErrnoException::class) 17 | fun processFileIo(context: CoroutineContext, process: suspend CoroutineScope.() -> T): T { 18 | return try { 19 | runBlocking(context = context) { 20 | process() 21 | } 22 | } catch (e: IOException) { 23 | logE(e) 24 | when (e.cause) { 25 | is ErrnoException -> throw (e.cause as ErrnoException) 26 | is StorageException -> throw ErrnoException("Writing", OsConstants.EBADF, e) 27 | else -> throw ErrnoException("I/O", OsConstants.EIO, e) 28 | } 29 | } 30 | } 31 | 32 | /** 33 | * Get throwable cause. 34 | */ 35 | fun Throwable.getCause(): Throwable { 36 | val c = cause 37 | return c?.getCause() ?: return this 38 | } 39 | 40 | /** 41 | * Check write permission. 42 | */ 43 | fun checkAccessMode(mode: AccessMode) { 44 | if (mode != AccessMode.W) { 45 | throw StorageException.Operation.AccessMode() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /data/storage/manager/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.ksp) 5 | alias(libs.plugins.hilt.android) 6 | } 7 | 8 | val applicationId: String by rootProject.extra 9 | val javaVersion: JavaVersion by rootProject.extra 10 | val androidCompileSdk: Int by rootProject.extra 11 | val androidMinSdk: Int by rootProject.extra 12 | 13 | android { 14 | namespace = "${applicationId}.data.storage.manager" 15 | compileSdk = androidCompileSdk 16 | 17 | defaultConfig { 18 | minSdk = androidMinSdk 19 | 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | consumerProguardFiles("consumer-rules.pro") 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility = javaVersion 26 | targetCompatibility = javaVersion 27 | } 28 | 29 | kotlin { 30 | jvmToolchain { 31 | languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) 32 | } 33 | } 34 | } 35 | 36 | dependencies { 37 | // Project 38 | implementation(project(":common")) 39 | implementation(project(":data:data")) 40 | implementation(project(":data:storage:interfaces")) 41 | implementation(project(":data:storage:jcifsng")) 42 | implementation(project(":data:storage:smbj")) 43 | implementation(project(":data:storage:apache")) 44 | 45 | // Libraries 46 | implementation(libs.hilt.android) 47 | ksp(libs.hilt.android.compiler) 48 | implementation(libs.kotlinx.coroutines.android) 49 | implementation(libs.androidx.documentfile) 50 | implementation(libs.jsch) 51 | 52 | // Test 53 | testImplementation(libs.junit) 54 | } 55 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/edit/components/SectionTitle.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.edit.components 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.sp 13 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.DividerWide 14 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.Theme 15 | 16 | @Composable 17 | fun SectionTitle( 18 | text: String, 19 | ) { 20 | Column( 21 | modifier = Modifier 22 | .fillMaxWidth() 23 | .padding(top = Theme.Sizes.S) 24 | ) { 25 | Text( 26 | text = text, 27 | fontSize = 14.sp, 28 | fontWeight = FontWeight.Bold, 29 | modifier = Modifier 30 | .padding( 31 | top = Theme.Sizes.M, 32 | bottom = Theme.Sizes.SS, 33 | ) 34 | ) 35 | DividerWide() 36 | } 37 | } 38 | 39 | @Preview( 40 | name = "Preview", 41 | group = "Group", 42 | uiMode = Configuration.UI_MODE_NIGHT_YES, 43 | showBackground = true, 44 | ) 45 | @Composable 46 | private fun SectionTitlePreview() { 47 | Theme.AppTheme { 48 | SectionTitle( 49 | text = "Section Title", 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/common/ComposeExt.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.common 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.foundation.layout.calculateEndPadding 5 | import androidx.compose.material3.LocalTextStyle 6 | import androidx.compose.material3.TextFieldDefaults 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.platform.LocalDensity 9 | import androidx.compose.ui.platform.LocalLayoutDirection 10 | import androidx.compose.ui.text.TextStyle 11 | import androidx.compose.ui.text.rememberTextMeasurer 12 | import androidx.compose.ui.unit.Dp 13 | import androidx.compose.ui.unit.TextUnit 14 | import com.wa2c.android.cifsdocumentsprovider.common.values.UiTheme 15 | 16 | @Composable 17 | fun Dp.toSp() = with(LocalDensity.current) { this@toSp.toSp() } 18 | 19 | @Composable 20 | fun TextUnit.toDp() = with(LocalDensity.current) { this@toDp.toDp() } 21 | 22 | 23 | @Composable 24 | fun UiTheme.isDark() : Boolean { 25 | return if (this == UiTheme.DEFAULT) { 26 | isSystemInDarkTheme() 27 | } else { 28 | this == UiTheme.DARK 29 | } 30 | } 31 | 32 | @Composable 33 | fun getTextWidth(text: String, style: TextStyle = LocalTextStyle.current): Dp { 34 | val measurer = rememberTextMeasurer() 35 | val measureResult = measurer.measure( 36 | text = text, 37 | style = style, 38 | maxLines = 1, 39 | ) 40 | val padding = TextFieldDefaults.contentPaddingWithLabel() 41 | return with(LocalDensity.current) { 42 | measureResult.size.width.toDp() + 43 | padding.calculateEndPadding(LocalLayoutDirection.current) + 44 | padding.calculateEndPadding(LocalLayoutDirection.current) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.ksp) 5 | alias(libs.plugins.hilt.android) 6 | alias(libs.plugins.kotlin.parcelize) 7 | alias(libs.plugins.kotlin.serialization) 8 | } 9 | 10 | val applicationId: String by rootProject.extra 11 | val javaVersion: JavaVersion by rootProject.extra 12 | val androidCompileSdk: Int by rootProject.extra 13 | val androidMinSdk: Int by rootProject.extra 14 | 15 | android { 16 | compileSdk = androidCompileSdk 17 | namespace = "${applicationId}.domain" 18 | 19 | defaultConfig { 20 | minSdk = androidMinSdk 21 | 22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 23 | consumerProguardFiles("consumer-rules.pro") 24 | 25 | buildConfigField("String", "K", "\"com.wa2c.android\"") 26 | } 27 | 28 | compileOptions { 29 | sourceCompatibility = javaVersion 30 | targetCompatibility = javaVersion 31 | } 32 | 33 | kotlin { 34 | jvmToolchain { 35 | languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) 36 | } 37 | } 38 | 39 | buildFeatures { 40 | buildConfig = true 41 | } 42 | } 43 | 44 | dependencies { 45 | 46 | implementation(project(":common")) 47 | implementation(project(":data:data")) 48 | implementation(project(":data:storage:interfaces")) 49 | implementation(project(":data:storage:manager")) 50 | 51 | implementation(libs.androidx.core.ktx) 52 | implementation(libs.androidx.appcompat) 53 | implementation(libs.kotlinx.coroutines.android) 54 | implementation(libs.hilt.android) 55 | ksp(libs.hilt.android.compiler) 56 | 57 | // Json 58 | implementation(libs.kotlinx.serialization.json) 59 | 60 | // Test 61 | testImplementation(libs.junit) 62 | 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/appInsightsSettings.xml 46 | .idea/deploymentTargetSelector 47 | .idea/dictionaries 48 | .idea/libraries 49 | .idea/other.xml 50 | # Android Studio 3 in .gitignore file. 51 | .idea/caches 52 | .idea/modules.xml 53 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 54 | .idea/navEditor.xml 55 | 56 | # Keystore files 57 | # Uncomment the following lines if you do not want to check your keystore files in. 58 | #*.jks 59 | #*.keystore 60 | 61 | # External native build folder generated in Android Studio 2.2 and later 62 | .externalNativeBuild 63 | .cxx/ 64 | 65 | # Google Services (e.g. APIs or Firebase) 66 | # google-services.json 67 | 68 | # Freeline 69 | freeline.py 70 | freeline/ 71 | freeline_project_description.json 72 | 73 | # fastlane 74 | fastlane/report.xml 75 | fastlane/Preview.html 76 | fastlane/screenshots 77 | fastlane/test_output 78 | fastlane/readme.md 79 | 80 | # Version control 81 | vcs.xml 82 | 83 | # lint 84 | lint/intermediates/ 85 | lint/generated/ 86 | lint/outputs/ 87 | lint/tmp/ 88 | # lint/reports/ 89 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/settings/components/SettingsItem.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.settings.components 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.heightIn 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import androidx.compose.ui.unit.dp 15 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.DividerThin 16 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.Theme 17 | 18 | /** 19 | * Settings item. 20 | */ 21 | @Composable 22 | internal fun SettingsItem( 23 | text: String, 24 | onClick: () -> Unit, 25 | ) { 26 | Row( 27 | modifier = Modifier 28 | .fillMaxWidth() 29 | .heightIn(64.dp) 30 | .clickable(enabled = true, onClick = onClick) 31 | .padding(horizontal = Theme.Sizes.M, vertical = Theme.Sizes.S) 32 | ) { 33 | Text( 34 | text = text, 35 | modifier = Modifier 36 | .align(alignment = Alignment.CenterVertically) 37 | ) 38 | } 39 | DividerThin() 40 | } 41 | 42 | /** 43 | * Preview 44 | */ 45 | @Preview( 46 | name = "Preview", 47 | group = "Group", 48 | uiMode = Configuration.UI_MODE_NIGHT_YES, 49 | showBackground = true, 50 | ) 51 | @Composable 52 | private fun SettingsItemPreview() { 53 | Theme.AppTheme { 54 | SettingsItem( 55 | text = "Settings Item", 56 | ) {} 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/common/Modifier.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.common 2 | 3 | import android.view.KeyEvent 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.draw.alpha 6 | import androidx.compose.ui.focus.FocusDirection 7 | import androidx.compose.ui.focus.FocusManager 8 | import androidx.compose.ui.input.key.KeyEventType 9 | import androidx.compose.ui.input.key.isShiftPressed 10 | import androidx.compose.ui.input.key.onPreviewKeyEvent 11 | import androidx.compose.ui.input.key.type 12 | 13 | /** 14 | * Move focus on Enter key 15 | */ 16 | fun Modifier.moveFocusOnEnter(focusManager: FocusManager) = 17 | onPreviewKeyEvent { key -> 18 | when (key.nativeKeyEvent.keyCode) { 19 | KeyEvent.KEYCODE_ENTER -> { 20 | if (key.type == KeyEventType.KeyDown) focusManager.moveFocus(FocusDirection.Next) 21 | true 22 | } 23 | else -> { 24 | false 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * Move focus on Tab key 31 | */ 32 | fun Modifier.moveFocusOnTab(focusManager: FocusManager) = 33 | onPreviewKeyEvent { key -> 34 | when (key.nativeKeyEvent.keyCode) { 35 | KeyEvent.KEYCODE_TAB -> { 36 | if (key.type == KeyEventType.KeyDown) { 37 | if (key.isShiftPressed) { 38 | focusManager.moveFocus(FocusDirection.Previous) 39 | } else { 40 | focusManager.moveFocus(FocusDirection.Next) 41 | } 42 | } 43 | true 44 | } 45 | else -> { 46 | false 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * Enabled style 53 | */ 54 | fun Modifier.enabledStyle(enabled: Boolean): Modifier { 55 | return if (enabled) this else this.alpha(0.5f) 56 | } 57 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/settings/components/TitleItem.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.settings.components 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.heightIn 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.tooling.preview.Preview 15 | import androidx.compose.ui.unit.dp 16 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.DividerNormal 17 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.Theme 18 | 19 | /** 20 | * Title item. 21 | */ 22 | @Composable 23 | internal fun TitleItem( 24 | text: String, 25 | ) { 26 | Box( 27 | modifier = Modifier 28 | .fillMaxWidth() 29 | .heightIn(48.dp) 30 | .padding(horizontal = Theme.Sizes.S, vertical = Theme.Sizes.S) 31 | ) { 32 | Text( 33 | text = text, 34 | style = MaterialTheme.typography.titleSmall, 35 | fontWeight = FontWeight.Bold, 36 | modifier = Modifier 37 | .align(alignment = Alignment.BottomStart) 38 | 39 | ) 40 | } 41 | DividerNormal() 42 | } 43 | 44 | /** 45 | * Preview 46 | */ 47 | @Preview( 48 | name = "Preview", 49 | group = "Group", 50 | uiMode = Configuration.UI_MODE_NIGHT_YES, 51 | showBackground = true, 52 | ) 53 | @Composable 54 | private fun TitleItemPreview() { 55 | Theme.AppTheme { 56 | TitleItem( 57 | text = "Title Item", 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | CIFS Documents Provider is an Android app to provide access to shared online storage. 2 | 3 | [Features] 4 | 5 | * Provide other apps with access to shared network storage via the Storage Access Framework (SAF). 6 | * Provides access to files and directories. 7 | * Supports SMB (Samba, Common Internet File System (CIFS), Windows Network Shared Folder), FTP, FTPS and SFTP. 8 | * Share and transfer files on online storage. 9 | * Multiple connection settings can be stored. 10 | * Supports connection settings export/import. 11 | * Supports dark mode. 12 | * Can be treated as local storage (configuration required) 13 | * Notifications can be displayed to prevent task kills. (configuration required) 14 | 15 | [Objective] 16 | 17 | * Import and export of files created by the app. 18 | * Manage files and directories with the Storage Manager app. 19 | * Play music, videos, etc. with the media player app. 20 | * Direct saving of photos taken with the camera app. 21 | 22 | [Note] 23 | 24 | * No file management function in this app. 25 | * To use this app, your apps must support SAF (Storage Access Framework). 26 | * Apps that assume local storage may not work properly. 27 | * Apps may crash when specified as a storage destination for streaming audio or video data. 28 | 29 | [How to use] 30 | 31 | See the following page. (Japanese) 32 | https://github.com/wa2c/cifs-documents-provider/wiki/Manual-ja 33 | 34 | [Sources] 35 | 36 | GitHub 37 | https://github.com/wa2c/cifs-documents-provider 38 | 39 | [Issues] 40 | 41 | GitHub Issue 42 | https://github.com/wa2c/cifs-documents-provider/issues 43 | 44 | Please post here if you have bug reports, Future requests, or other information. 45 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/common/BottomButton.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.common 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.ScrollState 5 | import androidx.compose.foundation.horizontalScroll 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.Button 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.tooling.preview.Preview 15 | 16 | @Composable 17 | fun BottomButton(label: String, subText: String? = null, onClick: () -> Unit) { 18 | Column { 19 | DividerNormal() 20 | 21 | subText?.let { 22 | Text( 23 | text = it, 24 | maxLines = 1, 25 | modifier = Modifier 26 | .padding(top = Theme.Sizes.S, start = Theme.Sizes.ScreenMargin, end = Theme.Sizes.ScreenMargin) 27 | .horizontalScroll(ScrollState(Int.MAX_VALUE)) 28 | ) 29 | } 30 | 31 | Button( 32 | onClick = onClick, 33 | shape = RoundedCornerShape(Theme.Sizes.SS), 34 | modifier = Modifier 35 | .fillMaxWidth() 36 | .padding(horizontal = Theme.Sizes.ScreenMargin, vertical = Theme.Sizes.S) 37 | ) { 38 | Text(text = label) 39 | } 40 | } 41 | } 42 | 43 | @Preview( 44 | name = "Preview", 45 | group = "Group", 46 | uiMode = Configuration.UI_MODE_NIGHT_YES, 47 | showBackground = true, 48 | ) 49 | @Composable 50 | private fun CommonDialogPreview() { 51 | BottomButton( 52 | label = "Label", 53 | subText = "https://example.com/" 54 | ) {} 55 | } 56 | -------------------------------------------------------------------------------- /data/storage/interfaces/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/storage/interfaces/StorageRequest.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces 2 | 3 | import com.wa2c.android.cifsdocumentsprovider.common.utils.appendChild 4 | import com.wa2c.android.cifsdocumentsprovider.common.utils.mimeType 5 | import com.wa2c.android.cifsdocumentsprovider.common.values.ThumbnailType 6 | import com.wa2c.android.cifsdocumentsprovider.common.values.URI_SEPARATOR 7 | import com.wa2c.android.cifsdocumentsprovider.common.values.URI_START 8 | 9 | /** 10 | * Storage Request 11 | */ 12 | data class StorageRequest( 13 | val connection: StorageConnection, 14 | val path: String? = null, 15 | ) { 16 | 17 | /** URI */ 18 | val uri: String 19 | get() = connection.uri.appendChild(path ?: "", false) 20 | 21 | val mimeType: String 22 | get() = uri.mimeType 23 | 24 | /*** Thumbnail type (null if disabled) */ 25 | val thumbnailType: ThumbnailType? 26 | get() = ThumbnailType.findByType(mimeType)?.takeIf { 27 | connection.thumbnailTypes.contains(it.type) 28 | } 29 | 30 | /** Share name */ 31 | val shareName: String 32 | get() = uri 33 | .substringAfter(URI_START, "") 34 | .substringAfter(URI_SEPARATOR, "") 35 | .substringBefore(URI_SEPARATOR) 36 | 37 | /** Share path */ 38 | val sharePath: String 39 | get() = uri 40 | .substringAfter(URI_START, "") 41 | .substringAfter(URI_SEPARATOR, "") 42 | .substringAfter(URI_SEPARATOR) 43 | 44 | /** True if this is root */ 45 | val isRoot: Boolean 46 | get() = shareName.isEmpty() 47 | 48 | /** True if this is share root */ 49 | val isShareRoot: Boolean 50 | get() = shareName.isNotEmpty() && sharePath.isEmpty() 51 | 52 | 53 | fun replacePathByUri(replaceUriText: String): StorageRequest { 54 | return copy(path = connection.getRelativePath(replaceUriText)) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/send/SendViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.send 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.wa2c.android.cifsdocumentsprovider.common.utils.logD 5 | import com.wa2c.android.cifsdocumentsprovider.domain.model.SendData 6 | import com.wa2c.android.cifsdocumentsprovider.domain.repository.SendRepository 7 | import com.wa2c.android.cifsdocumentsprovider.presentation.ext.MainCoroutineScope 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | /** 14 | * Send Screen ViewModel 15 | */ 16 | @HiltViewModel 17 | class SendViewModel @Inject constructor( 18 | private val sendRepository: SendRepository, 19 | ): ViewModel(), CoroutineScope by MainCoroutineScope() { 20 | 21 | val sendDataList = sendRepository.sendDataList 22 | 23 | /** 24 | * Start send job 25 | * @param toReadyConfirm confirm 26 | */ 27 | fun onStartSend(toReadyConfirm: Boolean) { 28 | logD("updateConfirmation") 29 | launch { 30 | sendRepository.start(toReadyConfirm) 31 | } 32 | } 33 | 34 | fun onClickCancel(sendData: SendData) { 35 | logD("onClickCancel") 36 | launch { 37 | sendRepository.cancel(sendData.id) 38 | } 39 | } 40 | 41 | fun onClickRetry(sendData: SendData) { 42 | logD("onClickRetry") 43 | launch { 44 | sendRepository.retry(sendData.id) 45 | } 46 | } 47 | 48 | fun onClickRemove(sendData: SendData) { 49 | logD("onClickRemove") 50 | launch { 51 | sendRepository.remove(sendData.id) 52 | } 53 | } 54 | 55 | /** 56 | * Cancel all 57 | */ 58 | fun onClickCancelAll() { 59 | logD("onClickCancelAll") 60 | launch { 61 | sendRepository.cancelAll() 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /data/data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.ksp) 5 | alias(libs.plugins.hilt.android) 6 | alias(libs.plugins.kotlin.parcelize) 7 | alias(libs.plugins.kotlin.serialization) 8 | alias(libs.plugins.room) 9 | } 10 | 11 | val applicationId: String by rootProject.extra 12 | val javaVersion: JavaVersion by rootProject.extra 13 | val androidCompileSdk: Int by rootProject.extra 14 | val androidMinSdk: Int by rootProject.extra 15 | 16 | android { 17 | compileSdk = androidCompileSdk 18 | namespace = "${applicationId}.data" 19 | 20 | defaultConfig { 21 | minSdk = androidMinSdk 22 | 23 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 24 | consumerProguardFiles("consumer-rules.pro") 25 | } 26 | 27 | compileOptions { 28 | sourceCompatibility = javaVersion 29 | targetCompatibility = javaVersion 30 | } 31 | 32 | kotlin { 33 | jvmToolchain { 34 | languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) 35 | } 36 | } 37 | } 38 | 39 | room { 40 | schemaDirectory("$projectDir/schemas") 41 | } 42 | 43 | dependencies { 44 | implementation(project(":common")) 45 | 46 | // App 47 | 48 | implementation(libs.androidx.core.ktx) 49 | implementation(libs.androidx.appcompat) 50 | implementation(libs.kotlinx.coroutines.android) 51 | implementation(libs.hilt.android) 52 | ksp(libs.hilt.android.compiler) 53 | 54 | // Room 55 | implementation(libs.androidx.room.runtime) 56 | ksp(libs.androidx.room.compiler) 57 | implementation(libs.androidx.room.ktx) 58 | implementation(libs.androidx.room.paging) 59 | // DataStore 60 | implementation(libs.androidx.datastore.preferences) 61 | // Serializer 62 | implementation(libs.kotlinx.serialization.json) 63 | // Android Network Tools 64 | implementation(libs.androidnetworktools) 65 | 66 | // Test 67 | 68 | testImplementation(libs.junit) 69 | } 70 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/receive/ReceiveFile.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.receive 2 | 3 | import android.net.Uri 4 | import androidx.activity.compose.rememberLauncherForActivityResult 5 | import androidx.activity.result.contract.ActivityResultContracts 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.LaunchedEffect 8 | import androidx.compose.ui.platform.LocalContext 9 | import com.wa2c.android.cifsdocumentsprovider.common.utils.getFileName 10 | 11 | @Composable 12 | fun ReceiveFile( 13 | uriList: List, 14 | onNavigateFinish: () -> Unit, 15 | onTargetSelected: (Uri) -> Unit, 16 | ) { 17 | val context = LocalContext.current 18 | 19 | /** Single URI result launcher */ 20 | val singleUriLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { uri -> 21 | if (uri == null) { 22 | onNavigateFinish() 23 | return@rememberLauncherForActivityResult 24 | } else { 25 | onTargetSelected(uri) 26 | } 27 | } 28 | 29 | /** Multiple URI result launcher */ 30 | val multipleUriLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> 31 | if (uri == null) { 32 | onNavigateFinish() 33 | return@rememberLauncherForActivityResult 34 | } else { 35 | onTargetSelected(uri) 36 | } 37 | } 38 | 39 | LaunchedEffect(uriList) { 40 | when { 41 | uriList.size == 1 -> { 42 | // Single 43 | uriList.first().getFileName(context).let { fileName -> 44 | singleUriLauncher.launch(fileName) 45 | } 46 | } 47 | uriList.size > 1 -> { 48 | // Multiple 49 | multipleUriLauncher.launch(uriList.first()) 50 | } 51 | 52 | else -> { 53 | onNavigateFinish() 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /data/data/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/db/ConnectionIO.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.db 2 | 3 | import android.content.Context 4 | import androidx.core.net.toUri 5 | import com.wa2c.android.cifsdocumentsprovider.data.EncryptUtils 6 | import dagger.hilt.android.qualifiers.ApplicationContext 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import kotlinx.serialization.json.Json 10 | import javax.inject.Inject 11 | import javax.inject.Singleton 12 | 13 | @Singleton 14 | class ConnectionIO @Inject constructor( 15 | @ApplicationContext private val context: Context, 16 | ) { 17 | private val formatter by lazy { 18 | Json { 19 | ignoreUnknownKeys = true 20 | } 21 | } 22 | 23 | suspend fun exportConnections( 24 | uriText: String, 25 | password: String, 26 | connectionList: List 27 | ) { 28 | withContext(Dispatchers.IO) { 29 | context.contentResolver.openOutputStream(uriText.toUri())?.use { 30 | val json = formatter.encodeToString(connectionList) 31 | val encryptedJson = EncryptUtils.encrypt(json, password, true) 32 | it.write(encryptedJson.toByteArray(Charsets.UTF_8)) 33 | } 34 | } 35 | } 36 | 37 | suspend fun importConnections( 38 | uriText: String, 39 | password: String, 40 | ): List { 41 | return withContext(Dispatchers.IO) { 42 | val uri = uriText.toUri() 43 | context.contentResolver.openInputStream(uri)?.use { 44 | val encryptedJson = it.readBytes().toString(Charsets.UTF_8) 45 | val json = EncryptUtils.decrypt(encryptedJson, password, true) 46 | formatter.decodeFromString(json) 47 | } ?: emptyList() 48 | } 49 | } 50 | 51 | suspend fun deleteConnection( 52 | uriText: String, 53 | ) { 54 | withContext(Dispatchers.IO) { 55 | context.contentResolver.delete(uriText.toUri(), null, null) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /domain/src/main/java/com/wa2c/android/cifsdocumentsprovider/domain/model/StorageUri.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain.model 2 | 3 | import android.os.Parcelable 4 | import com.wa2c.android.cifsdocumentsprovider.common.utils.appendChild 5 | import com.wa2c.android.cifsdocumentsprovider.common.utils.fileName 6 | import com.wa2c.android.cifsdocumentsprovider.common.utils.isDirectoryUri 7 | import com.wa2c.android.cifsdocumentsprovider.common.values.URI_SEPARATOR 8 | import com.wa2c.android.cifsdocumentsprovider.common.values.URI_START 9 | import kotlinx.parcelize.Parcelize 10 | 11 | /** 12 | * Remote URI. 13 | */ 14 | @Parcelize 15 | data class StorageUri( 16 | /** Encoded URI Text */ 17 | val text: String, 18 | ) : Parcelable { 19 | 20 | /** Path[xxx/xxx] (not start with '/') */ 21 | val path: String 22 | get() { 23 | val startIndex = text.indexOf(URI_START).takeIf { it >= 0 } ?: return text 24 | val pathIndex = text.indexOf(URI_SEPARATOR, startIndex + URI_START.length).takeIf { it >= 0 } ?: return "" 25 | return text.substring(pathIndex + 1) 26 | } 27 | 28 | /** 29 | * Parent URI. ( last character = '/' ) 30 | */ 31 | val parentUri: StorageUri? 32 | get() { 33 | if (isRoot) return null 34 | val currentUriText = if (text.last() == URI_SEPARATOR) text.substring(0, text.length - 1) else text 35 | return StorageUri( 36 | currentUriText.substring( 37 | 0, 38 | currentUriText.lastIndexOf(URI_SEPARATOR) + 1 39 | ) 40 | ) 41 | } 42 | 43 | /** True if root */ 44 | val isRoot: Boolean 45 | get() = path.isEmpty() 46 | 47 | /** File name */ 48 | val fileName: String 49 | get() = text.fileName 50 | 51 | fun addPath(path: String?): StorageUri { 52 | return if (path.isNullOrEmpty()) { this } else { 53 | StorageUri(text.appendChild(path, path.isDirectoryUri)) 54 | } 55 | } 56 | 57 | override fun toString(): String { 58 | return text 59 | } 60 | 61 | companion object { 62 | val ROOT = StorageUri("") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /data/data/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/EncryptUtils.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data 2 | 3 | import android.util.Base64 4 | import com.wa2c.android.cifsdocumentsprovider.common.values.PASSWORD_LENGTH_16 5 | import com.wa2c.android.cifsdocumentsprovider.common.values.PASSWORD_LENGTH_32 6 | import javax.crypto.Cipher 7 | import javax.crypto.spec.IvParameterSpec 8 | import javax.crypto.spec.SecretKeySpec 9 | 10 | /** 11 | * Json Converter 12 | */ 13 | object EncryptUtils { 14 | 15 | private const val ALGORITHM: String = "AES" 16 | 17 | private const val TRANSFORMATION: String = "AES/CBC/PKCS5PADDING" 18 | 19 | private val spec: IvParameterSpec = IvParameterSpec(ByteArray(16)) 20 | 21 | /** 22 | * Encrypt key. 23 | */ 24 | fun encrypt(originalString: String, secretKey: String, is256: Boolean = false): String { 25 | val originalBytes = originalString.toByteArray() 26 | val passwordLength = if (is256) PASSWORD_LENGTH_32 else PASSWORD_LENGTH_16 27 | val secretKeyBytes = secretKey.padEnd(passwordLength, '0') .toByteArray() 28 | val secretKeySpec = SecretKeySpec(secretKeyBytes, ALGORITHM) 29 | val cipher = Cipher.getInstance(TRANSFORMATION) 30 | cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, spec) 31 | val encryptBytes = cipher.doFinal(originalBytes) 32 | val encryptBytesBase64 = Base64.encode(encryptBytes, Base64.DEFAULT) 33 | return String(encryptBytesBase64) 34 | } 35 | 36 | /** 37 | * Decrypt key. 38 | */ 39 | fun decrypt(encryptBytesBase64String: String, secretKey: String, is256: Boolean = false): String { 40 | val encryptBytes = Base64.decode(encryptBytesBase64String, Base64.DEFAULT) 41 | val passwordLength = if (is256) PASSWORD_LENGTH_32 else PASSWORD_LENGTH_16 42 | val secretKeyBytes = secretKey.padEnd(passwordLength, '0').toByteArray() 43 | val secretKeySpec = SecretKeySpec(secretKeyBytes, ALGORITHM) 44 | val cipher = Cipher.getInstance(TRANSFORMATION) 45 | cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, spec) 46 | val originalBytes = cipher.doFinal(encryptBytes) 47 | return String(originalBytes) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/worker/SendWorker.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.worker 2 | 3 | import android.content.Context 4 | import androidx.lifecycle.coroutineScope 5 | import androidx.work.CoroutineWorker 6 | import androidx.work.WorkerParameters 7 | import com.wa2c.android.cifsdocumentsprovider.common.utils.logD 8 | import com.wa2c.android.cifsdocumentsprovider.domain.repository.SendRepository 9 | import com.wa2c.android.cifsdocumentsprovider.presentation.ext.collectIn 10 | import com.wa2c.android.cifsdocumentsprovider.presentation.provideSendRepository 11 | import kotlinx.coroutines.CancellationException 12 | import kotlinx.coroutines.launch 13 | 14 | /** 15 | * Send Worker 16 | */ 17 | class SendWorker( 18 | private val context: Context, 19 | params: WorkerParameters 20 | ) : CoroutineWorker(context, params) { 21 | 22 | private val notification: SendNotification by lazy { SendNotification(context) } 23 | private val sendRepository: SendRepository by lazy { provideSendRepository(context) } 24 | private val lifecycleOwner = WorkerLifecycleOwner() 25 | 26 | override suspend fun doWork(): Result { 27 | logD("SendWorker begin") 28 | 29 | try { 30 | notification.hideCompleted() 31 | lifecycleOwner.start() 32 | lifecycleOwner.lifecycle.coroutineScope.launch { 33 | sendRepository.sendDataList.collectIn(lifecycleOwner) { list -> 34 | notification.updateNotification(list) 35 | } 36 | } 37 | setForeground(notification.getNotificationInfo(sendRepository.sendDataList.value)) 38 | while (!isStopped) { 39 | val result = sendRepository.sendReadyData() 40 | if (!result) break 41 | } 42 | } catch (e: CancellationException) { 43 | logD(e) 44 | } finally { 45 | lifecycleOwner.stop() 46 | notification.showCompleted(sendRepository.sendDataList.value) 47 | } 48 | 49 | logD("SendWorker end") 50 | return Result.success() 51 | } 52 | 53 | companion object { 54 | const val WORKER_NAME = "SendWorker" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | -------------------------------------------------------------------------------- /common/src/main/java/com/wa2c/android/cifsdocumentsprovider/common/exception/StorageException.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.common.exception 2 | 3 | import java.io.IOException 4 | 5 | /** 6 | * Storage exception. 7 | */ 8 | sealed class StorageException(message: String?, cause: Throwable?) : IOException(message, cause) { 9 | class Error : StorageException { 10 | constructor(cause: Throwable) : super(cause.localizedMessage, cause) 11 | constructor(message: String) : super(message, null) 12 | } 13 | 14 | sealed class File(message: String?, cause: Throwable?) : StorageException(message, cause) { 15 | class NotFound(cause: Throwable? = null) : File(cause?.localizedMessage ?: "File is not found.", cause) 16 | class DocumentId(cause: Throwable? = null) : File(cause?.localizedMessage ?: "Invalid document id.", cause) 17 | } 18 | 19 | sealed class Operation(message: String?, cause: Throwable?) : StorageException(message, cause) { 20 | class Unsupported(cause: Throwable) : Operation(cause.localizedMessage ?: "Unsupported operation.", cause) 21 | class AccessMode(cause: Throwable? = null) : Operation("Writing is not allowed in reading mode.", cause) 22 | class ReadOnly(cause: Throwable? = null) : Operation("Writing is not allowed in options.", cause) 23 | class RandomAccessNotPermitted(cause: Throwable? = null) : Operation("This type does not support random writing.", cause) 24 | } 25 | 26 | sealed class Security(message: String?, cause: Throwable?, val id: String) : StorageException(message, cause) { 27 | class Auth(cause: Throwable, id: String) : Security(cause.localizedMessage ?: "Authentication failed.", cause, id) 28 | class UnknownHost(cause: Throwable, id: String) : Security(cause.localizedMessage ?: "Unknown host.", cause, id) 29 | } 30 | 31 | sealed class Transaction(message: String?, cause: Throwable?) : StorageException(message, cause) { 32 | class HostNotFound(cause: Throwable) : Transaction(cause.localizedMessage ?: "Host not found.", cause) 33 | class Timeout(cause: Throwable) : Transaction(cause.localizedMessage ?: "Connection timeout.", cause) 34 | class Network(cause: Throwable) : Transaction(cause.localizedMessage ?: "Network disconnected.", cause) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /data/data/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/HostFinder.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data 2 | 3 | import com.stealthcopter.networktools.SubnetDevices 4 | import com.stealthcopter.networktools.subnet.Device 5 | import com.wa2c.android.cifsdocumentsprovider.common.utils.logD 6 | import com.wa2c.android.cifsdocumentsprovider.common.utils.logE 7 | import kotlinx.coroutines.flow.MutableSharedFlow 8 | import kotlinx.coroutines.runBlocking 9 | import javax.inject.Inject 10 | import javax.inject.Singleton 11 | 12 | typealias IpHost = Pair 13 | 14 | @Singleton 15 | class HostFinder @Inject constructor() { 16 | 17 | private val _hostFlow: MutableSharedFlow = MutableSharedFlow() 18 | val hostFlow: MutableSharedFlow = _hostFlow 19 | 20 | /** 21 | * Start discovery 22 | */ 23 | suspend fun startDiscovery() { 24 | try { 25 | SubnetDevices.fromLocalAddress() 26 | .findDevices(object : SubnetDevices.OnSubnetDeviceFound { 27 | override fun onDeviceFound(device: Device?) { 28 | logD("onDeviceFound: ${device?.hostname} / ${device?.ip}") 29 | IpHost( 30 | device?.ip ?: return, 31 | device.hostname ?: return, 32 | ).let { 33 | runBlocking { 34 | _hostFlow.emit(it) 35 | } 36 | } 37 | } 38 | 39 | override fun onFinished(devicesFound: ArrayList?) { 40 | logD("onFinished: devicesFound=$devicesFound") 41 | runBlocking { 42 | _hostFlow.emit(null) 43 | } 44 | } 45 | }) 46 | } catch (e: Throwable) { 47 | _hostFlow.emit(null) 48 | throw e 49 | } 50 | } 51 | 52 | /** 53 | * Stop discovery 54 | */ 55 | suspend fun stopDiscovery() { 56 | try { 57 | SubnetDevices.fromLocalAddress().cancel() 58 | } catch (e: Throwable) { 59 | logE(e) 60 | } finally { 61 | _hostFlow.emit(null) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /data/storage/apache/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/storage/apache/ApacheFtpClient.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.apache 2 | 3 | import com.wa2c.android.cifsdocumentsprovider.common.values.CONNECTION_TIMEOUT 4 | import com.wa2c.android.cifsdocumentsprovider.common.values.READ_TIMEOUT 5 | import com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.StorageConnection 6 | import kotlinx.coroutines.CoroutineDispatcher 7 | import kotlinx.coroutines.Dispatchers 8 | import org.apache.commons.vfs2.FileSystemOptions 9 | import org.apache.commons.vfs2.provider.ftp.FtpFileSystemConfigBuilder 10 | import org.apache.commons.vfs2.provider.ftp.FtpFileType 11 | import org.apache.commons.vfs2.provider.ftps.FtpsDataChannelProtectionLevel 12 | import org.apache.commons.vfs2.provider.ftps.FtpsFileSystemConfigBuilder 13 | import org.apache.commons.vfs2.provider.ftps.FtpsMode 14 | import java.time.Duration 15 | 16 | class ApacheFtpClient( 17 | private val isFtps: Boolean, 18 | dispatcher: CoroutineDispatcher = Dispatchers.IO, 19 | ): ApacheVfsClient(dispatcher) { 20 | 21 | override fun applyOptions(options: FileSystemOptions, storageConnection: StorageConnection) { 22 | val ftpConnection = storageConnection as StorageConnection.Ftp 23 | 24 | // FTP settings 25 | FtpFileSystemConfigBuilder.getInstance().also { builder -> 26 | builder.setPassiveMode(options, !ftpConnection.isActiveMode) 27 | builder.setSoTimeout(options, Duration.ofMillis(CONNECTION_TIMEOUT.toLong())) 28 | builder.setConnectTimeout(options, Duration.ofMillis(CONNECTION_TIMEOUT.toLong())) 29 | builder.setDataTimeout(options, Duration.ofMillis(READ_TIMEOUT.toLong())) 30 | builder.setFileType(options, FtpFileType.BINARY) 31 | builder.setControlEncoding(options, ftpConnection.encoding) 32 | builder.setUserDirIsRoot(options, false) // true occurs path mismatch 33 | } 34 | if (isFtps) { 35 | FtpsFileSystemConfigBuilder.getInstance().also { builder -> 36 | builder.setFtpsMode(options, if (ftpConnection.isImplicitMode) FtpsMode.IMPLICIT else FtpsMode.EXPLICIT) 37 | builder.setDataChannelProtectionLevel(options, FtpsDataChannelProtectionLevel.P) 38 | } 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui 2 | 3 | import android.net.Uri 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.wa2c.android.cifsdocumentsprovider.common.utils.logD 7 | import com.wa2c.android.cifsdocumentsprovider.common.values.UiTheme 8 | import com.wa2c.android.cifsdocumentsprovider.domain.repository.AppRepository 9 | import com.wa2c.android.cifsdocumentsprovider.domain.repository.SendRepository 10 | import com.wa2c.android.cifsdocumentsprovider.presentation.ext.MainCoroutineScope 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.flow.MutableSharedFlow 14 | import kotlinx.coroutines.flow.SharingStarted 15 | import kotlinx.coroutines.flow.asSharedFlow 16 | import kotlinx.coroutines.flow.distinctUntilChanged 17 | import kotlinx.coroutines.flow.map 18 | import kotlinx.coroutines.flow.stateIn 19 | import kotlinx.coroutines.launch 20 | import javax.inject.Inject 21 | 22 | @HiltViewModel 23 | class MainViewModel @Inject constructor( 24 | private val appRepository: AppRepository, 25 | private val sendRepository: SendRepository, 26 | ): ViewModel(), CoroutineScope by MainCoroutineScope() { 27 | 28 | /** UI Theme */ 29 | val uiThemeFlow = appRepository.uiThemeFlow.stateIn(viewModelScope, SharingStarted.Eagerly, UiTheme.DEFAULT) 30 | 31 | /** Send data list */ 32 | val sendDataList = sendRepository.sendDataList 33 | 34 | /** True if showing send screen */ 35 | val showSend = sendRepository.sendDataList.map { it.isNotEmpty() }.distinctUntilChanged() 36 | 37 | private val _showEdit = MutableSharedFlow() 38 | val showEdit = _showEdit.asSharedFlow() 39 | 40 | /** 41 | * Send URI 42 | */ 43 | fun sendUri(sourceUriList: List, targetUri: Uri) { 44 | logD("sendUri") 45 | launch { 46 | sendRepository.sendUri(sourceUriList, targetUri) 47 | } 48 | } 49 | 50 | 51 | fun clearUri() { 52 | launch { 53 | sendRepository.clear() 54 | } 55 | } 56 | 57 | fun showEditScreen(storageId: String) { 58 | launch { 59 | _showEdit.emit(storageId) 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /data/storage/apache/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/storage/apache/ApacheSftpClient.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.apache 2 | 3 | import com.wa2c.android.cifsdocumentsprovider.common.values.CONNECTION_TIMEOUT 4 | import com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.StorageConnection 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import kotlinx.coroutines.Dispatchers 7 | import org.apache.commons.vfs2.FileSystemOptions 8 | import org.apache.commons.vfs2.provider.sftp.BytesIdentityInfo 9 | import org.apache.commons.vfs2.provider.sftp.SftpFileSystemConfigBuilder 10 | import java.io.File 11 | import java.time.Duration 12 | 13 | class ApacheSftpClient( 14 | private val knownHostPath: String, 15 | private val onKeyRead: (String) -> ByteArray, 16 | dispatcher: CoroutineDispatcher = Dispatchers.IO, 17 | ): ApacheVfsClient(dispatcher) { 18 | 19 | override fun applyOptions(options: FileSystemOptions, storageConnection: StorageConnection) { 20 | val sftpConnection = storageConnection as StorageConnection.Sftp 21 | 22 | SftpFileSystemConfigBuilder.getInstance().also { builder -> 23 | builder.setConnectTimeout(options, Duration.ofMillis(CONNECTION_TIMEOUT.toLong())) 24 | builder.setSessionTimeout(options, Duration.ofMillis(CONNECTION_TIMEOUT.toLong())) 25 | builder.setPreferredAuthentications(options, "publickey,password") 26 | builder.setFileNameEncoding(options, sftpConnection.encoding) 27 | builder.setUserDirIsRoot(options, false) // true occurs path mismatch 28 | // Known hosts 29 | if (storageConnection.ignoreKnownHosts) { 30 | builder.setStrictHostKeyChecking(options, "no") 31 | } else { 32 | builder.setStrictHostKeyChecking(options, "ask") 33 | builder.setKnownHosts(options, File(knownHostPath)) 34 | } 35 | // Key 36 | (sftpConnection.keyData?.encodeToByteArray() ?: sftpConnection.keyFileUri?.let { uri -> 37 | try { onKeyRead(uri) } catch (e: Exception) { null } 38 | })?.let { keyBinary -> 39 | val identity = BytesIdentityInfo(keyBinary, sftpConnection.keyPassphrase?.encodeToByteArray()) 40 | builder.setIdentityProvider(options, identity) 41 | } 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/settings/components/SettingsCheckItem.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.settings.components 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.heightIn 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material3.Checkbox 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.MutableState 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import androidx.compose.ui.unit.dp 19 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.DividerThin 20 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.Theme 21 | 22 | /** 23 | * Settings check item. 24 | */ 25 | @Composable 26 | internal fun SettingsCheckItem( 27 | text: String, 28 | checked: MutableState, 29 | ) { 30 | Row( 31 | modifier = Modifier 32 | .fillMaxWidth() 33 | .heightIn(64.dp) 34 | .clickable(enabled = true, onClick = { checked.value = !checked.value }) 35 | .padding(horizontal = Theme.Sizes.M, vertical = Theme.Sizes.S) 36 | ) { 37 | Text( 38 | text = text, 39 | modifier = Modifier 40 | .align(alignment = Alignment.CenterVertically) 41 | .weight(weight = 1f, fill = true) 42 | , 43 | ) 44 | Checkbox( 45 | checked = checked.value, 46 | onCheckedChange = { checked.value = !checked.value }, 47 | ) 48 | } 49 | DividerThin() 50 | } 51 | 52 | /** 53 | * Preview 54 | */ 55 | @Preview( 56 | name = "Preview", 57 | group = "Group", 58 | uiMode = Configuration.UI_MODE_NIGHT_YES, 59 | showBackground = true, 60 | ) 61 | @Composable 62 | private fun SettingsCheckItemPreview() { 63 | Theme.AppTheme { 64 | SettingsCheckItem( 65 | text = "Settings Check Item", 66 | checked = remember { mutableStateOf(true) }, 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.ksp) 5 | alias(libs.plugins.hilt.android) 6 | } 7 | 8 | val applicationId: String by rootProject.extra 9 | val javaVersion: JavaVersion by rootProject.extra 10 | val androidCompileSdk: Int by rootProject.extra 11 | val androidMinSdk: Int by rootProject.extra 12 | val appVersionName: String by rootProject.extra 13 | val appVersionCode: Int by rootProject.extra 14 | 15 | android { 16 | compileSdk = androidCompileSdk 17 | namespace = applicationId 18 | compileOptions { 19 | sourceCompatibility = javaVersion 20 | targetCompatibility = javaVersion 21 | } 22 | 23 | defaultConfig { 24 | applicationId = applicationId 25 | minSdk = androidMinSdk 26 | targetSdk = androidCompileSdk 27 | versionCode = appVersionCode 28 | versionName = appVersionName 29 | 30 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 31 | 32 | base.archivesName.set("CIFSDocumentsProvider-${versionName}") 33 | } 34 | 35 | buildTypes { 36 | debug { 37 | versionNameSuffix = "D" 38 | } 39 | release { 40 | isMinifyEnabled = false 41 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 42 | } 43 | } 44 | 45 | signingConfigs { 46 | getByName("debug") { 47 | storeFile = file("$rootDir/debug.keystore") 48 | } 49 | } 50 | 51 | kotlin { 52 | jvmToolchain { 53 | languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) 54 | } 55 | } 56 | 57 | packaging { 58 | // For commons-vfs2-jackrabbit2 59 | resources.excludes.addAll(setOf( 60 | "META-INF/*", 61 | "META-INF/versions/*/OSGI-INF/MANIFEST.MF", 62 | )) 63 | } 64 | 65 | buildFeatures { 66 | buildConfig = true 67 | } 68 | } 69 | 70 | dependencies { 71 | implementation(fileTree(mapOf("dir" to "libs", "include" to arrayOf("*.jar")))) 72 | implementation(project(":common")) 73 | implementation(project(":presentation")) 74 | implementation(project(":domain")) 75 | 76 | implementation(libs.androidx.appcompat) 77 | implementation(libs.hilt.android) 78 | implementation(libs.androidx.work.runtime) 79 | ksp(libs.hilt.android.compiler) 80 | } 81 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/edit/components/KeyInputDialog.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.edit.components 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.OutlinedTextField 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.tooling.preview.Preview 15 | import com.wa2c.android.cifsdocumentsprovider.presentation.R 16 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.CommonDialog 17 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.DialogButton 18 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.Theme 19 | 20 | /** 21 | * Key input dialog. 22 | */ 23 | @Composable 24 | fun KeyInputDialog( 25 | onInput: (String) -> Unit, 26 | onDismiss: () -> Unit, 27 | ) { 28 | var key by remember { mutableStateOf("") } 29 | 30 | CommonDialog( 31 | title = stringResource(id = R.string.edit_key_title) + "\n" + stringResource(id = R.string.edit_key_input_import_text), 32 | confirmButtons = listOf( 33 | DialogButton(label = stringResource(id = R.string.general_accept)) { 34 | onInput(key) 35 | } 36 | ), 37 | dismissButton = DialogButton(label = stringResource(id = R.string.general_close)) { 38 | onDismiss() 39 | }, 40 | onDismiss = onDismiss 41 | ) { 42 | Column { 43 | OutlinedTextField( 44 | value = key, 45 | onValueChange = { 46 | key = it 47 | }, 48 | modifier = Modifier 49 | .fillMaxSize() 50 | ) 51 | } 52 | } 53 | } 54 | 55 | @Preview( 56 | name = "Preview", 57 | group = "Group", 58 | uiMode = Configuration.UI_MODE_NIGHT_YES, 59 | showBackground = true, 60 | ) 61 | @Composable 62 | private fun KeyInputDialogPreview() { 63 | Theme.AppTheme { 64 | KeyInputDialog( 65 | onInput = {}, 66 | onDismiss = {}, 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/settings/components/SettingsSingleChoiceItem.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.settings.components 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.MutableState 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.res.stringResource 9 | import androidx.compose.ui.tooling.preview.Preview 10 | import com.wa2c.android.cifsdocumentsprovider.presentation.R 11 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.DialogButton 12 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.OptionItem 13 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.SingleChoiceDialog 14 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.Theme 15 | 16 | /** 17 | * Settings single choice item 18 | */ 19 | @Composable 20 | internal fun SettingsSingleChoiceItem( 21 | title: String, 22 | items: List>, 23 | selectedItem: MutableState, 24 | ) { 25 | val showDialog = remember { mutableStateOf(false) } 26 | 27 | SettingsItem(text = title) { 28 | showDialog.value = true 29 | } 30 | 31 | if (showDialog.value) { 32 | SingleChoiceDialog( 33 | items = items.map { it.label }, 34 | selectedIndex = items.indexOfFirst { it.value == selectedItem.value }, 35 | title = title, 36 | dismissButton = DialogButton(label = stringResource(id = R.string.general_close)) { 37 | showDialog.value = false 38 | }, 39 | onDismiss = { showDialog.value = false } 40 | ) { index, _ -> 41 | selectedItem.value = items[index].value 42 | showDialog.value = false 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Preview 49 | */ 50 | @Preview( 51 | name = "Preview", 52 | group = "Group", 53 | uiMode = Configuration.UI_MODE_NIGHT_YES, 54 | showBackground = true, 55 | ) 56 | @Composable 57 | private fun SettingsSingleChoiceItemPreview() { 58 | Theme.AppTheme { 59 | SettingsSingleChoiceItem( 60 | title = "Single Choice Item", 61 | items = listOf(OptionItem("1", "Item1"), OptionItem("2","Item2"), OptionItem("3","Item3")), 62 | selectedItem = remember { mutableStateOf("Item1") }, 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /data/storage/smbj/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/storage/smbj/SmbjProxyFileCallbackSafe.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.smbj 2 | 3 | import android.os.ProxyFileDescriptorCallback 4 | import android.system.ErrnoException 5 | import com.hierynomus.smbj.share.File 6 | import com.wa2c.android.cifsdocumentsprovider.common.utils.logD 7 | import com.wa2c.android.cifsdocumentsprovider.common.values.AccessMode 8 | import com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.utils.checkAccessMode 9 | import com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.utils.processFileIo 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.Job 13 | import kotlin.coroutines.CoroutineContext 14 | 15 | /** 16 | * Proxy File Callback for SMBJ (Normal IO) 17 | */ 18 | class SmbjProxyFileCallbackSafe( 19 | private val file: File, 20 | private val accessMode: AccessMode, 21 | private val onFileRelease: suspend () -> Unit, 22 | ) : ProxyFileDescriptorCallback(), CoroutineScope { 23 | 24 | override val coroutineContext: CoroutineContext = Dispatchers.IO + Job() 25 | 26 | /** File size */ 27 | private val fileSize: Long by lazy { 28 | processFileIo(coroutineContext) { file.fileInformation.standardInformation.endOfFile } 29 | } 30 | 31 | @Throws(ErrnoException::class) 32 | override fun onGetSize(): Long { 33 | return fileSize 34 | } 35 | 36 | @Throws(ErrnoException::class) 37 | override fun onRead(offset: Long, size: Int, data: ByteArray): Int { 38 | return processFileIo(coroutineContext) { 39 | // if End-Of-File (-1) then return 0 bytes read 40 | maxOf(0,file.read(data, offset, 0, size)) 41 | } 42 | } 43 | 44 | @Throws(ErrnoException::class) 45 | override fun onWrite(offset: Long, size: Int, data: ByteArray): Int { 46 | return processFileIo(coroutineContext) { 47 | checkAccessMode(accessMode) 48 | file.write(data, offset, 0, size).toInt() 49 | } 50 | } 51 | 52 | @Throws(ErrnoException::class) 53 | override fun onFsync() { 54 | // Nothing to do 55 | } 56 | 57 | @Throws(ErrnoException::class) 58 | override fun onRelease() { 59 | logD("onRelease: ${file.uncPath}") 60 | processFileIo(coroutineContext) { 61 | logD("release begin") 62 | onFileRelease() 63 | logD("release end") 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /data/data/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/db/ConnectionSettingDao.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.db 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import androidx.room.Transaction 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.first 10 | 11 | @Dao 12 | interface ConnectionSettingDao { 13 | 14 | @Query("SELECT count(id) FROM ${ConnectionSettingEntity.TABLE_NAME}") 15 | suspend fun getCount(): Int 16 | 17 | @Query("SELECT coalesce(max(sort_order), 0) FROM ${ConnectionSettingEntity.TABLE_NAME}") 18 | suspend fun getMaxSortOrder(): Int 19 | 20 | @Query("SELECT * FROM ${ConnectionSettingEntity.TABLE_NAME} WHERE id = :id") 21 | suspend fun getEntity(id: String): ConnectionSettingEntity? 22 | 23 | @Query("SELECT * FROM ${ConnectionSettingEntity.TABLE_NAME} WHERE instr(:uri, uri) > 0 ORDER BY sort_order" ) 24 | suspend fun getEntityByUri(uri: String): ConnectionSettingEntity? 25 | 26 | @Query("SELECT * FROM ${ConnectionSettingEntity.TABLE_NAME} ORDER BY sort_order") 27 | fun getList(): Flow> 28 | 29 | @Query("SELECT * FROM ${ConnectionSettingEntity.TABLE_NAME} WHERE type IN (:types) ORDER BY sort_order") 30 | suspend fun getTypedList(types: Collection): List 31 | 32 | @Insert(onConflict = OnConflictStrategy.REPLACE) 33 | suspend fun insert(entity: ConnectionSettingEntity) 34 | 35 | @Insert(onConflict = OnConflictStrategy.REPLACE) 36 | suspend fun insertAll(entities: List) 37 | 38 | @Query("DELETE FROM ${ConnectionSettingEntity.TABLE_NAME} WHERE id = :id") 39 | suspend fun delete(id: String) 40 | 41 | @Query("DELETE FROM ${ConnectionSettingEntity.TABLE_NAME}") 42 | suspend fun deleteAll() 43 | 44 | @Query("UPDATE ${ConnectionSettingEntity.TABLE_NAME} SET sort_order = :sortOrder WHERE id = :id") 45 | suspend fun updateSortOrder(id: String, sortOrder: Int) 46 | 47 | @Transaction 48 | suspend fun replace(entities: List) { 49 | deleteAll() 50 | insertAll(entities) 51 | } 52 | 53 | @Transaction 54 | suspend fun move(fromPosition: Int, toPosition: Int) { 55 | val list = getList().first().toMutableList() 56 | list.add(toPosition, list.removeAt(fromPosition)) 57 | list.forEachIndexed { index, entity -> 58 | updateSortOrder(entity.id, index + 1) 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /data/data/schemas/com.wa2c.android.cifsdocumentsprovider.data.db.AppDatabase/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 2, 5 | "identityHash": "9e5e08f53616a96caa0b7c9b94cccc01", 6 | "entities": [ 7 | { 8 | "tableName": "connection_setting", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `uri` TEXT NOT NULL, `data` TEXT NOT NULL, `sort_order` INTEGER NOT NULL, `modified_date` INTEGER NOT NULL, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "name", 19 | "columnName": "name", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "type", 25 | "columnName": "type", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "uri", 31 | "columnName": "uri", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "data", 37 | "columnName": "data", 38 | "affinity": "TEXT", 39 | "notNull": true 40 | }, 41 | { 42 | "fieldPath": "sortOrder", 43 | "columnName": "sort_order", 44 | "affinity": "INTEGER", 45 | "notNull": true 46 | }, 47 | { 48 | "fieldPath": "modifiedDate", 49 | "columnName": "modified_date", 50 | "affinity": "INTEGER", 51 | "notNull": true 52 | } 53 | ], 54 | "primaryKey": { 55 | "autoGenerate": false, 56 | "columnNames": [ 57 | "id" 58 | ] 59 | }, 60 | "indices": [ 61 | { 62 | "name": "index_connection_setting_sort_order", 63 | "unique": false, 64 | "columnNames": [ 65 | "sort_order" 66 | ], 67 | "orders": [], 68 | "createSql": "CREATE INDEX IF NOT EXISTS `index_connection_setting_sort_order` ON `${TABLE_NAME}` (`sort_order`)" 69 | } 70 | ], 71 | "foreignKeys": [] 72 | } 73 | ], 74 | "views": [], 75 | "setupQueries": [ 76 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 77 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9e5e08f53616a96caa0b7c9b94cccc01')" 78 | ] 79 | } 80 | } -------------------------------------------------------------------------------- /data/data/schemas/com.wa2c.android.cifsdocumentsprovider.data.db.AppDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "13e876ad6de42cfdcf3472b14fb65b84", 6 | "entities": [ 7 | { 8 | "tableName": "connection_setting", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `uri` TEXT NOT NULL, `data` TEXT NOT NULL, `sort_order` INTEGER NOT NULL, `modified_date` INTEGER NOT NULL, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "name", 19 | "columnName": "name", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "type", 25 | "columnName": "type", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "uri", 31 | "columnName": "uri", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "data", 37 | "columnName": "data", 38 | "affinity": "TEXT", 39 | "notNull": true 40 | }, 41 | { 42 | "fieldPath": "sortOrder", 43 | "columnName": "sort_order", 44 | "affinity": "INTEGER", 45 | "notNull": true 46 | }, 47 | { 48 | "fieldPath": "modifiedDate", 49 | "columnName": "modified_date", 50 | "affinity": "INTEGER", 51 | "notNull": true 52 | } 53 | ], 54 | "primaryKey": { 55 | "columnNames": [ 56 | "id" 57 | ], 58 | "autoGenerate": false 59 | }, 60 | "indices": [ 61 | { 62 | "name": "index_connection_setting_uri_sort_order", 63 | "unique": false, 64 | "columnNames": [ 65 | "uri", 66 | "sort_order" 67 | ], 68 | "orders": [], 69 | "createSql": "CREATE INDEX IF NOT EXISTS `index_connection_setting_uri_sort_order` ON `${TABLE_NAME}` (`uri`, `sort_order`)" 70 | } 71 | ], 72 | "foreignKeys": [] 73 | } 74 | ], 75 | "views": [], 76 | "setupQueries": [ 77 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 78 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '13e876ad6de42cfdcf3472b14fb65b84')" 79 | ] 80 | } 81 | } -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_notification.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 21 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /domain/src/main/java/com/wa2c/android/cifsdocumentsprovider/domain/model/DocumentId.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.domain.model 2 | 3 | import android.os.Parcelable 4 | import com.wa2c.android.cifsdocumentsprovider.common.utils.appendChild 5 | import com.wa2c.android.cifsdocumentsprovider.common.values.DOCUMENT_ID_DELIMITER 6 | import com.wa2c.android.cifsdocumentsprovider.common.values.URI_SEPARATOR 7 | import com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.StorageConnection 8 | import com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.StorageFile 9 | import kotlinx.parcelize.Parcelize 10 | 11 | /** 12 | * Document ID 13 | * [Document ID format: :] 14 | */ 15 | @Parcelize 16 | data class DocumentId( 17 | val connectionId: String, 18 | val path: String, 19 | val legacyId: String? = null, 20 | ) : Parcelable { 21 | 22 | val idText: String 23 | get() = if (isRoot) "" else connectionId + DOCUMENT_ID_DELIMITER + path 24 | 25 | val isRoot: Boolean 26 | get() = connectionId.isEmpty() 27 | 28 | val isPathRoot: Boolean 29 | get() = path.isEmpty() || path == URI_SEPARATOR.toString() 30 | 31 | fun appendChild(child: String, isDirectory: Boolean = false): DocumentId? { 32 | return fromIdText(idText.appendChild(child, isDirectory)) 33 | } 34 | 35 | override fun toString(): String { 36 | return idText 37 | } 38 | 39 | companion object { 40 | 41 | val ROOT_DOCUMENT_ID_TEXT = "/" 42 | 43 | val ROOT = DocumentId("", "") 44 | 45 | fun isInvalidDocumentId(connectionId: String): Boolean { 46 | return connectionId.contains(DOCUMENT_ID_DELIMITER) || connectionId.contains(URI_SEPARATOR) 47 | } 48 | 49 | /** 50 | * Create from connection and file. 51 | */ 52 | fun fromConnection(connection: StorageConnection, file: StorageFile): DocumentId? { 53 | val relativePath = connection.getRelativePath(file.uri) 54 | return fromConnection(connection.id, relativePath) 55 | } 56 | 57 | /** 58 | * Root document ID text 59 | */ 60 | fun fromIdText(documentIdText: String?): DocumentId? { 61 | val connectionId = documentIdText?.substringBefore(DOCUMENT_ID_DELIMITER, documentIdText) 62 | val path = documentIdText?.substringAfter(DOCUMENT_ID_DELIMITER, "") 63 | return fromConnection(connectionId, path) 64 | } 65 | 66 | /** 67 | * Create from connection ID and path. 68 | */ 69 | fun fromConnection(connectionId: String?, path: String? = null, legacyId: String? = null): DocumentId? { 70 | val id = connectionId ?: "" 71 | if (isInvalidDocumentId(id)) return null 72 | return DocumentId(id, path ?: "", legacyId) 73 | } 74 | 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /presentation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.ksp) 5 | alias(libs.plugins.hilt.android) 6 | alias(libs.plugins.kotlin.compose) 7 | alias(libs.plugins.kotlin.parcelize) 8 | alias(libs.plugins.aboutlibraries) 9 | } 10 | 11 | val applicationId: String by rootProject.extra 12 | val javaVersion: JavaVersion by rootProject.extra 13 | val androidCompileSdk: Int by rootProject.extra 14 | val androidMinSdk: Int by rootProject.extra 15 | 16 | android { 17 | compileSdk = androidCompileSdk 18 | namespace = "${applicationId}.presentation" 19 | 20 | defaultConfig { 21 | minSdk = androidMinSdk 22 | 23 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 24 | consumerProguardFiles("consumer-rules.pro") 25 | } 26 | 27 | compileOptions { 28 | sourceCompatibility = javaVersion 29 | targetCompatibility = javaVersion 30 | } 31 | 32 | kotlin { 33 | jvmToolchain { 34 | languageVersion.set(JavaLanguageVersion.of(javaVersion.majorVersion)) 35 | } 36 | } 37 | buildFeatures { 38 | compose = true 39 | } 40 | } 41 | 42 | //aboutLibraries { 43 | // configPath = "config" 44 | //} 45 | 46 | dependencies { 47 | implementation(project(":common")) 48 | implementation(project(":domain")) 49 | 50 | 51 | // App 52 | 53 | implementation(libs.androidx.core.ktx) 54 | implementation(libs.androidx.appcompat) 55 | implementation(libs.kotlinx.coroutines.android) 56 | implementation(libs.hilt.android) 57 | ksp(libs.hilt.android.compiler) 58 | 59 | // UI 60 | 61 | // Compose 62 | val composeBom = platform(libs.androidx.compose.bom) 63 | implementation(composeBom) 64 | testImplementation(composeBom) 65 | androidTestImplementation(composeBom) 66 | implementation(libs.androidx.ui) 67 | implementation(libs.androidx.material3) 68 | implementation(libs.androidx.ui.tooling.preview) 69 | debugImplementation(libs.androidx.ui.tooling) 70 | implementation(libs.reorderable) 71 | implementation(libs.accompanist.systemuicontroller) 72 | // Lifecycle 73 | implementation(libs.androidx.lifecycle.runtime.compose) 74 | implementation(libs.androidx.lifecycle.viewmodel.compose) 75 | // Navigation 76 | implementation(libs.androidx.navigation.compose) 77 | implementation(libs.androidx.hilt.navigation.compose) 78 | // Worker 79 | implementation(libs.androidx.work.runtime) 80 | implementation(libs.guava) // to solve dependencies conflict 81 | 82 | // Util 83 | 84 | // OSS License 85 | implementation(libs.aboutlibraries) 86 | implementation(libs.aboutlibraries.compose) 87 | 88 | // Test 89 | 90 | testImplementation(libs.junit) 91 | androidTestImplementation(libs.androidx.junit) 92 | } 93 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/common/LoadingIconButton.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.common 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.LinearEasing 5 | import androidx.compose.animation.core.LinearOutSlowInEasing 6 | import androidx.compose.animation.core.RepeatMode 7 | import androidx.compose.animation.core.infiniteRepeatable 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.IconButton 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.LaunchedEffect 13 | import androidx.compose.runtime.mutableFloatStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.rotate 17 | import androidx.compose.ui.graphics.vector.ImageVector 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.res.vectorResource 20 | import com.wa2c.android.cifsdocumentsprovider.presentation.R 21 | 22 | @Composable 23 | fun LoadingIconButton( 24 | imageVector: ImageVector = ImageVector.vectorResource(id = R.drawable.ic_reload), 25 | contentDescription: String? = stringResource(id = R.string.host_reload_button), 26 | isLoading: Boolean, 27 | onClick: () -> Unit, 28 | ) { 29 | IconButton( 30 | onClick = { onClick() } 31 | ) { 32 | val currentRotation = remember { mutableFloatStateOf(0f) } 33 | val rotation = remember { Animatable(currentRotation.floatValue) } 34 | 35 | Icon( 36 | imageVector = imageVector, 37 | contentDescription = contentDescription, 38 | modifier = Modifier 39 | .rotate(rotation.value) 40 | ) 41 | 42 | // Loading animation 43 | LaunchedEffect(isLoading) { 44 | if (isLoading) { 45 | // Infinite repeatable rotation when is playing 46 | rotation.animateTo( 47 | targetValue = currentRotation.floatValue + 360f, 48 | animationSpec = infiniteRepeatable( 49 | animation = tween(3000, easing = LinearEasing), 50 | repeatMode = RepeatMode.Restart 51 | ) 52 | ) { 53 | currentRotation.floatValue = value 54 | } 55 | } else { 56 | // Slow down rotation on pause 57 | rotation.animateTo( 58 | targetValue = 0f, 59 | animationSpec = tween( 60 | durationMillis = 0, 61 | easing = LinearOutSlowInEasing 62 | ) 63 | ) { 64 | currentRotation.floatValue = 0f 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /data/storage/smbj/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/storage/smbj/SmbjProxyFileCallback.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.smbj 2 | 3 | import android.os.ProxyFileDescriptorCallback 4 | import android.system.ErrnoException 5 | import com.hierynomus.smbj.share.File 6 | import com.wa2c.android.cifsdocumentsprovider.common.utils.logD 7 | import com.wa2c.android.cifsdocumentsprovider.common.values.AccessMode 8 | import com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.utils.BackgroundBufferReader 9 | import com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.utils.checkAccessMode 10 | import com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.utils.processFileIo 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.Job 14 | import kotlin.coroutines.CoroutineContext 15 | 16 | /** 17 | * Proxy File Callback for SMBJ (Buffering IO) 18 | */ 19 | class SmbjProxyFileCallback( 20 | private val file: File, 21 | private val accessMode: AccessMode, 22 | private val onFileRelease: suspend () -> Unit, 23 | ) : ProxyFileDescriptorCallback(), CoroutineScope { 24 | 25 | override val coroutineContext: CoroutineContext = Dispatchers.IO + Job() 26 | 27 | /** File size */ 28 | private val fileSize: Long by lazy { 29 | processFileIo(coroutineContext) { file.fileInformation.standardInformation.endOfFile } 30 | } 31 | 32 | private val readerLazy = lazy { 33 | BackgroundBufferReader(fileSize) { start, array, off, len -> 34 | file.read(array, start, off, len) 35 | } 36 | } 37 | 38 | private val reader: BackgroundBufferReader get() = readerLazy.value 39 | 40 | @Throws(ErrnoException::class) 41 | override fun onGetSize(): Long { 42 | return fileSize 43 | } 44 | 45 | @Throws(ErrnoException::class) 46 | override fun onRead(offset: Long, size: Int, data: ByteArray): Int { 47 | return processFileIo(coroutineContext) { 48 | reader.readBuffer(offset, size, data) 49 | } 50 | } 51 | 52 | @Throws(ErrnoException::class) 53 | override fun onWrite(offset: Long, size: Int, data: ByteArray): Int { 54 | return processFileIo(coroutineContext) { 55 | checkAccessMode(accessMode) 56 | file.writeAsync(data, offset, 0, size) 57 | size 58 | } 59 | } 60 | 61 | @Throws(ErrnoException::class) 62 | override fun onFsync() { 63 | // Nothing to do 64 | } 65 | 66 | @Throws(ErrnoException::class) 67 | override fun onRelease() { 68 | logD("onRelease: ${file.uncPath}") 69 | processFileIo(coroutineContext) { 70 | logD("release begin") 71 | if (readerLazy.isInitialized()) { 72 | reader.close() 73 | } 74 | onFileRelease() 75 | logD("release end") 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/edit/components/InputCheck.kt: -------------------------------------------------------------------------------- 1 | import android.content.res.Configuration 2 | import android.view.KeyEvent 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.selection.toggleable 7 | import androidx.compose.material3.Checkbox 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.focus.FocusManager 12 | import androidx.compose.ui.input.key.KeyEventType 13 | import androidx.compose.ui.input.key.onPreviewKeyEvent 14 | import androidx.compose.ui.input.key.type 15 | import androidx.compose.ui.platform.LocalFocusManager 16 | import androidx.compose.ui.semantics.Role 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.Theme 19 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.moveFocusOnEnter 20 | 21 | /** 22 | * Input check 23 | */ 24 | @Composable 25 | fun InputCheck( 26 | title: String, 27 | value: Boolean, 28 | focusManager: FocusManager, 29 | enabled: Boolean = true, 30 | onChange: (value: Boolean) -> Unit, 31 | ) { 32 | Row( 33 | Modifier 34 | .toggleable( 35 | value = value, 36 | role = Role.Checkbox, 37 | onValueChange = { 38 | onChange(!value) 39 | } 40 | ) 41 | .padding(Theme.Sizes.M) 42 | .fillMaxWidth() 43 | .moveFocusOnEnter(focusManager) 44 | .onPreviewKeyEvent { 45 | when (it.nativeKeyEvent.keyCode) { 46 | KeyEvent.KEYCODE_SPACE -> { 47 | if (it.type == KeyEventType.KeyUp) { 48 | onChange(!value) 49 | } 50 | true 51 | } 52 | 53 | else -> { 54 | false 55 | } 56 | } 57 | } 58 | ) { 59 | Checkbox( 60 | checked = value, 61 | enabled = enabled, 62 | onCheckedChange = null, 63 | ) 64 | Text( 65 | text = title, 66 | Modifier 67 | .weight(1f) 68 | .padding(start = Theme.Sizes.S) 69 | ) 70 | } 71 | } 72 | 73 | @Preview( 74 | name = "Preview", 75 | group = "Group", 76 | uiMode = Configuration.UI_MODE_NIGHT_YES, 77 | showBackground = true, 78 | ) 79 | @Composable 80 | private fun InputCheckPreview() { 81 | Theme.AppTheme { 82 | InputCheck( 83 | title = "Title", 84 | value = true, 85 | focusManager = LocalFocusManager.current, 86 | enabled = true, 87 | ) { 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/common/CommonSingleChoiceDialog.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.common 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.selection.selectable 9 | import androidx.compose.material3.RadioButton 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.tooling.preview.Preview 15 | import androidx.compose.ui.unit.dp 16 | 17 | /** 18 | * Single choice dialog 19 | */ 20 | @Composable 21 | fun SingleChoiceDialog( 22 | items: List, 23 | selectedIndex: Int = 0, 24 | title: String? = null, 25 | confirmButtons: List? = null, 26 | dismissButton: DialogButton? = null, 27 | onDismiss: (() -> Unit)? = null, 28 | result: (Int, String) -> Unit 29 | ) { 30 | if (items.isEmpty()) return 31 | CommonDialog( 32 | title = title, 33 | confirmButtons = confirmButtons, 34 | dismissButton = dismissButton, 35 | onDismiss = onDismiss, 36 | ) { 37 | SingleChoiceView(items, selectedIndex, result) 38 | } 39 | } 40 | 41 | @Composable 42 | private fun SingleChoiceView( 43 | items: List, 44 | selectedIndex: Int, 45 | onSelectItem: (Int, String) -> Unit, 46 | ) { 47 | Column( 48 | Modifier.fillMaxWidth() 49 | ) { 50 | items.forEachIndexed { index, text -> 51 | Row( 52 | Modifier 53 | .fillMaxWidth() 54 | .selectable( 55 | selected = (index == selectedIndex), 56 | onClick = { onSelectItem(index, text) } 57 | ) 58 | .padding(vertical = 4.dp) 59 | ) { 60 | RadioButton( 61 | selected = (index == selectedIndex), 62 | onClick = { 63 | onSelectItem(index, text) 64 | }, 65 | modifier = Modifier 66 | .align(alignment = Alignment.CenterVertically), 67 | ) 68 | Text( 69 | text = text, 70 | modifier = Modifier 71 | .align(alignment = Alignment.CenterVertically), 72 | ) 73 | } 74 | } 75 | } 76 | } 77 | 78 | @Preview( 79 | name = "Preview", 80 | group = "Group", 81 | uiMode = Configuration.UI_MODE_NIGHT_YES, 82 | showBackground = true, 83 | ) 84 | @Composable 85 | private fun SingleChoiceDialogPreview() { 86 | Theme.AppTheme { 87 | SingleChoiceDialog( 88 | items = listOf("Item1", "Item2", "Item3"), 89 | selectedIndex = 1, 90 | title = "Title", 91 | ) { _, _ -> } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/worker/ProviderWorker.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.worker 2 | 3 | import android.content.Context 4 | import android.os.Handler 5 | import android.os.Looper 6 | import androidx.lifecycle.coroutineScope 7 | import androidx.work.CoroutineWorker 8 | import androidx.work.WorkerParameters 9 | import com.wa2c.android.cifsdocumentsprovider.common.utils.logD 10 | import com.wa2c.android.cifsdocumentsprovider.domain.repository.StorageRepository 11 | import com.wa2c.android.cifsdocumentsprovider.presentation.ext.collectIn 12 | import com.wa2c.android.cifsdocumentsprovider.presentation.provideStorageRepository 13 | import kotlinx.coroutines.CancellationException 14 | import kotlinx.coroutines.CompletableDeferred 15 | import kotlinx.coroutines.launch 16 | 17 | /** 18 | * Provider Worker (for keep DocumentsProvider) 19 | */ 20 | class ProviderWorker( 21 | private val context: Context, 22 | params: WorkerParameters 23 | ) : CoroutineWorker(context, params) { 24 | 25 | private val providerNotification: ProviderNotification by lazy { ProviderNotification(context) } 26 | private val storageRepository: StorageRepository by lazy { provideStorageRepository(context) } 27 | private val lifecycleOwner = WorkerLifecycleOwner() 28 | private var deferredUntilCompleted = CompletableDeferred() 29 | private val handler = Handler(Looper.getMainLooper()) 30 | 31 | override suspend fun doWork(): Result { 32 | logD("ProviderWorker begin") 33 | 34 | try { 35 | lifecycleOwner.start() 36 | lifecycleOwner.lifecycle.coroutineScope.launch { 37 | val completeRunnable = Runnable { 38 | // don't use list here, 5 seconds later the real list could be different 39 | if (storageRepository.openUriList.value.isEmpty()) deferredUntilCompleted.complete(Unit) 40 | else providerNotification.updateNotification(storageRepository.openUriList.value) 41 | } 42 | storageRepository.openUriList.collectIn(lifecycleOwner) { list -> 43 | providerNotification.updateNotification(list) 44 | handler.removeCallbacks(completeRunnable) 45 | if (list.isEmpty()) { 46 | // Check again after grace period, if list is empty cancel work. 47 | // otherwise, sometimes notification gets canceled as soon as opened 48 | handler.postDelayed(completeRunnable, 5000) 49 | } 50 | } 51 | } 52 | 53 | setForeground(providerNotification.getNotificationInfo(storageRepository.openUriList.value)) 54 | deferredUntilCompleted.await() // wait until the uri list is empty. 55 | } catch (e: CancellationException) { 56 | // ignored 57 | } finally { 58 | lifecycleOwner.stop() 59 | } 60 | 61 | logD("ProviderWorker end") 62 | return Result.success() 63 | } 64 | 65 | companion object { 66 | const val WORKER_NAME = "ProviderWorker" 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /data/storage/manager/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/storage/manager/SshKeyManager.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.data.storage.manager 2 | 3 | import android.content.Context 4 | import com.jcraft.jsch.JSch 5 | import com.jcraft.jsch.JSchUnknownHostKeyException 6 | import com.jcraft.jsch.KeyPair 7 | import com.wa2c.android.cifsdocumentsprovider.common.utils.logW 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import java.io.File 10 | import javax.inject.Inject 11 | import javax.inject.Singleton 12 | 13 | @Singleton 14 | class SshKeyManager @Inject constructor( 15 | @ApplicationContext private val context: Context, 16 | ) { 17 | 18 | /** JSch */ 19 | private val jsch: JSch by lazy { 20 | // Support ssh-rsa type keys 21 | JSch.setConfig("server_host_key", JSch.getConfig("server_host_key") + ",ssh-rsa") 22 | JSch.setConfig("PubkeyAcceptedAlgorithms", JSch.getConfig("PubkeyAcceptedAlgorithms") + ",ssh-rsa") 23 | // Initialize 24 | JSch().apply { 25 | val file = File(context.filesDir, KNOWN_HOSTS_FILE) 26 | if (!file.exists()) file.createNewFile() 27 | setKnownHosts(file.absolutePath) 28 | } 29 | } 30 | 31 | /** Known host file path */ 32 | val knownHostPath: String 33 | get() = jsch.hostKeyRepository.knownHostsRepositoryID 34 | 35 | /** Known host list. */ 36 | val knownHostList: List 37 | get() = jsch.hostKeyRepository.hostKey.map { 38 | HostKeyEntity(it.host, it.type, it.key) 39 | } 40 | 41 | /** 42 | * Check key file. 43 | */ 44 | fun checkKeyFile(keyData: ByteArray) { 45 | val k = KeyPair.load(jsch, keyData, null) 46 | k.dispose() 47 | } 48 | 49 | /** 50 | * Add known host. 51 | */ 52 | fun addKnownHost( 53 | host: String, 54 | port: Int?, 55 | username: String, 56 | ) { 57 | val session = jsch.getSession(username, host, port ?: 22) 58 | try { 59 | try { session.connect() } catch (e: JSchUnknownHostKeyException) { logW(e) } // session connected on JSchUnknownHostKeyException 60 | val hostKey = session.hostKey 61 | val userInfo = session.userInfo 62 | jsch.hostKeyRepository.add(hostKey, userInfo) 63 | } finally { 64 | session.disconnect() 65 | } 66 | } 67 | 68 | /** 69 | * Delete known host. 70 | */ 71 | fun deleteKnownHost( 72 | host: String, 73 | type: String, 74 | ) { 75 | jsch.hostKeyRepository.remove(host, type) 76 | jsch.getSession(host).disconnect() 77 | 78 | val file = File(context.filesDir, KNOWN_HOSTS_FILE) 79 | jsch.setKnownHosts(file.absolutePath) 80 | } 81 | 82 | /** 83 | * Known host key entity. 84 | */ 85 | data class HostKeyEntity( 86 | /** Host */ 87 | val host: String, 88 | /** Key type */ 89 | val type: String, 90 | /** Key */ 91 | val key: String, 92 | ) 93 | 94 | companion object { 95 | private const val KNOWN_HOSTS_FILE = "known_hosts" 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ui/settings/components/SettingsInputNumberItem.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ui.settings.components 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.heightIn 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.text.KeyboardOptions 10 | import androidx.compose.material3.LocalTextStyle 11 | import androidx.compose.material3.OutlinedTextField 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.MutableState 15 | import androidx.compose.runtime.mutableIntStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.text.input.ImeAction 20 | import androidx.compose.ui.text.input.KeyboardType 21 | import androidx.compose.ui.text.style.TextAlign 22 | import androidx.compose.ui.tooling.preview.Preview 23 | import androidx.compose.ui.unit.dp 24 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.DividerThin 25 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.common.Theme 26 | 27 | /** 28 | * Settings input number item. 29 | */ 30 | @Composable 31 | internal fun SettingsInputNumberItem( 32 | text: String, 33 | value: MutableState, 34 | maxValue: Int = 999, 35 | minValue: Int = 1, 36 | ) { 37 | Row( 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | .heightIn(64.dp) 41 | .padding(horizontal = Theme.Sizes.M, vertical = Theme.Sizes.S) 42 | ) { 43 | Text( 44 | text = text, 45 | modifier = Modifier 46 | .align(alignment = Alignment.CenterVertically) 47 | .weight(weight = 1f, fill = true), 48 | ) 49 | OutlinedTextField( 50 | value = value.value.toString(), 51 | keyboardOptions = KeyboardOptions( 52 | keyboardType = KeyboardType.Number, 53 | imeAction = ImeAction.Next, 54 | ), 55 | maxLines = 1, 56 | textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.End), 57 | modifier = Modifier 58 | .width(80.dp), 59 | onValueChange = { 60 | val number = it.toIntOrNull() ?: minValue 61 | value.value = number.coerceIn(minValue, maxValue) 62 | } 63 | ) 64 | } 65 | DividerThin() 66 | } 67 | 68 | /** 69 | * Preview 70 | */ 71 | @Preview( 72 | name = "Preview", 73 | group = "Group", 74 | uiMode = Configuration.UI_MODE_NIGHT_YES, 75 | showBackground = true, 76 | ) 77 | @Composable 78 | private fun SettingsInputNumberItemPreview() { 79 | Theme.AppTheme { 80 | SettingsInputNumberItem( 81 | text = "Settings Input Number Item", 82 | value = remember { mutableIntStateOf(9999) }, 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/ext/UiExt.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.ext 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.text.format.DateUtils 7 | import android.text.format.Formatter 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.lifecycle.LifecycleOwner 10 | import androidx.lifecycle.lifecycleScope 11 | import androidx.lifecycle.repeatOnLifecycle 12 | import com.wa2c.android.cifsdocumentsprovider.domain.model.SendData 13 | import com.wa2c.android.cifsdocumentsprovider.domain.model.SendDataState 14 | import com.wa2c.android.cifsdocumentsprovider.presentation.ui.MainActivity 15 | import kotlinx.coroutines.CoroutineScope 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.Job 18 | import kotlinx.coroutines.flow.Flow 19 | import kotlinx.coroutines.flow.SharingStarted 20 | import kotlinx.coroutines.flow.StateFlow 21 | import kotlinx.coroutines.flow.map 22 | import kotlinx.coroutines.flow.stateIn 23 | import kotlinx.coroutines.launch 24 | import kotlin.coroutines.CoroutineContext 25 | 26 | /** 27 | * Main Coroutine Scope 28 | */ 29 | class MainCoroutineScope: CoroutineScope { 30 | private val job = Job() 31 | 32 | override val coroutineContext: CoroutineContext 33 | get() = Dispatchers.Main + job 34 | } 35 | 36 | 37 | /** 38 | * Collect flow 39 | */ 40 | fun Flow.collectIn( 41 | lifecycleOwner: LifecycleOwner, 42 | state: Lifecycle.State = Lifecycle.State.STARTED, 43 | observer: (T) -> Unit = { }, 44 | ) { 45 | lifecycleOwner.lifecycleScope.launch { 46 | lifecycleOwner.repeatOnLifecycle(state) { 47 | this@collectIn.collect { observer(it) } 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * StateFlow map 54 | */ 55 | fun StateFlow.mapState( 56 | viewModelScope: CoroutineScope, 57 | transform: (data: T) -> K 58 | ): StateFlow { 59 | return map { 60 | transform(it) 61 | }.stateIn(viewModelScope, SharingStarted.Eagerly, transform(this.value)) 62 | } 63 | 64 | /** 65 | * Summary Text 66 | * ex. 10% [10MB/100MB] 1MB/s (1:00) 67 | */ 68 | fun SendData.getSummaryText(context: Context): String { 69 | return when (state) { 70 | SendDataState.PROGRESS -> { 71 | val sendSize = " (${Formatter.formatShortFileSize(context, progressSize)}/${Formatter.formatShortFileSize(context, size)})" 72 | val sendSpeed = "${Formatter.formatShortFileSize(context, bps)}/s (${DateUtils.formatElapsedTime(elapsedTime / 1000)})" 73 | "$progress% $sendSize $sendSpeed" 74 | } 75 | else -> { 76 | context.getString(state.labelRes) 77 | } 78 | } 79 | } 80 | 81 | fun Context.createAuthenticatePendingIntent(id: String): PendingIntent { 82 | return PendingIntent.getActivity( 83 | this, 84 | 100, 85 | Intent(this, MainActivity::class.java).also { 86 | it.putExtra("STORAGE_ID", id) 87 | }, 88 | PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, 89 | ) 90 | } 91 | 92 | fun Intent.getStorageId(): String? { 93 | return getStringExtra("STORAGE_ID") 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 54 | 57 | 58 | 59 | 63 | 64 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /data/storage/jcifsng/src/main/java/com/wa2c/android/cifsdocumentsprovider/data/storage/jcifsng/JCifsNgProxyFileCallbackSafe.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | package com.wa2c.android.cifsdocumentsprovider.data.storage.jcifsng 18 | 19 | import android.os.ProxyFileDescriptorCallback 20 | import android.system.ErrnoException 21 | import com.wa2c.android.cifsdocumentsprovider.common.values.AccessMode 22 | import com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.utils.checkAccessMode 23 | import com.wa2c.android.cifsdocumentsprovider.data.storage.interfaces.utils.processFileIo 24 | import jcifs.smb.SmbFile 25 | import jcifs.smb.SmbRandomAccessFile 26 | import kotlinx.coroutines.* 27 | import kotlin.coroutines.CoroutineContext 28 | 29 | /** 30 | * Proxy File Callback for jCIFS-ng (Normal IO) 31 | */ 32 | internal class JCifsNgProxyFileCallbackSafe( 33 | private val smbFile: SmbFile, 34 | private val accessMode: AccessMode, 35 | private val onFileRelease: suspend () -> Unit, 36 | ) : ProxyFileDescriptorCallback(), CoroutineScope { 37 | 38 | override val coroutineContext: CoroutineContext = Dispatchers.IO + Job() 39 | 40 | /** File size */ 41 | private val fileSize: Long by lazy { 42 | processFileIo(coroutineContext) { access.length() } 43 | } 44 | 45 | private val accessLazy: Lazy = lazy { 46 | processFileIo(coroutineContext) { 47 | smbFile.openRandomAccess(accessMode.smbMode) 48 | } 49 | } 50 | 51 | private val access: SmbRandomAccessFile get() = accessLazy.value 52 | 53 | @Throws(ErrnoException::class) 54 | override fun onGetSize(): Long { 55 | return fileSize 56 | } 57 | 58 | @Throws(ErrnoException::class) 59 | override fun onRead(offset: Long, size: Int, data: ByteArray): Int { 60 | return processFileIo(coroutineContext) { 61 | access.seek(offset) 62 | // if End-Of-File (-1) then return 0 bytes read 63 | maxOf(0, access.read(data, 0, size)) 64 | } 65 | } 66 | 67 | @Throws(ErrnoException::class) 68 | override fun onWrite(offset: Long, size: Int, data: ByteArray): Int { 69 | return processFileIo(coroutineContext) { 70 | checkAccessMode(accessMode) 71 | access.seek(offset) 72 | access.write(data, 0, size) 73 | size 74 | } 75 | } 76 | 77 | @Throws(ErrnoException::class) 78 | override fun onFsync() { 79 | // Nothing to do 80 | } 81 | 82 | @Throws(ErrnoException::class) 83 | override fun onRelease() { 84 | processFileIo(coroutineContext) { 85 | if (accessLazy.isInitialized()) access.close() 86 | onFileRelease() 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 16 | 22 | 28 | 33 | 38 | 43 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/wa2c/android/cifsdocumentsprovider/presentation/worker/ProviderNotification.kt: -------------------------------------------------------------------------------- 1 | package com.wa2c.android.cifsdocumentsprovider.presentation.worker 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.content.Context 7 | import android.content.pm.ServiceInfo 8 | import android.net.Uri 9 | import android.os.Build 10 | import androidx.core.app.NotificationCompat 11 | import androidx.work.ForegroundInfo 12 | import com.wa2c.android.cifsdocumentsprovider.common.utils.getFileName 13 | import com.wa2c.android.cifsdocumentsprovider.common.values.NOTIFICATION_CHANNEL_ID_PROVIDER 14 | import com.wa2c.android.cifsdocumentsprovider.common.values.NOTIFICATION_ID_PROVIDER 15 | import com.wa2c.android.cifsdocumentsprovider.presentation.R 16 | 17 | /** 18 | * Provider Notification 19 | */ 20 | class ProviderNotification( 21 | private val context: Context, 22 | ) { 23 | /** Notification manager */ 24 | private val notificationManager: NotificationManager by lazy { 25 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 26 | } 27 | 28 | init { 29 | createChannel() 30 | } 31 | 32 | /** 33 | * Create notification channel 34 | */ 35 | private fun createChannel() { 36 | if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID_PROVIDER) != null) return 37 | NotificationChannel( 38 | NOTIFICATION_CHANNEL_ID_PROVIDER, 39 | context.getString(R.string.notification_channel_name_provider), 40 | NotificationManager.IMPORTANCE_LOW 41 | ).apply { 42 | enableLights(false) 43 | enableVibration(false) 44 | setShowBadge(true) 45 | lockscreenVisibility = Notification.VISIBILITY_PRIVATE 46 | }.let { 47 | notificationManager.createNotificationChannel(it) 48 | } 49 | } 50 | 51 | /** 52 | * Create notification 53 | */ 54 | private fun createNotification(list: List = emptyList()): Notification { 55 | return NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_PROVIDER) 56 | .setAutoCancel(false) 57 | .setOngoing(true) 58 | .setSmallIcon(R.drawable.ic_notification) 59 | .setContentTitle(context.getString(R.string.notification_title_provider)) 60 | .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) 61 | .setStyle( 62 | NotificationCompat.InboxStyle().also { style -> 63 | list.map { Uri.parse(it).getFileName(context) }.filter { it.isNotBlank() }.forEach { style.addLine(it) } 64 | } 65 | ) 66 | .build() 67 | } 68 | 69 | /** 70 | * Create CoroutineWorker foreground info 71 | */ 72 | fun getNotificationInfo(list: List): ForegroundInfo { 73 | val notification = createNotification(list) 74 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 75 | ForegroundInfo(NOTIFICATION_ID_PROVIDER, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) 76 | } else { 77 | ForegroundInfo(NOTIFICATION_ID_PROVIDER, notification) 78 | } 79 | } 80 | 81 | /** 82 | * Update notification 83 | */ 84 | fun updateNotification(list: List) { 85 | if (list.isEmpty()) return 86 | val notification = createNotification(list) 87 | notificationManager.notify(NOTIFICATION_ID_PROVIDER, notification) 88 | } 89 | 90 | } 91 | --------------------------------------------------------------------------------