├── 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 |
4 |
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 |
5 |
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 |
5 |
6 |
7 |
8 |
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 |
7 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
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 |
--------------------------------------------------------------------------------