├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md ├── build.gradle ├── crowdin.yml ├── proguard-rules.pro ├── schemas ├── com.pydio.android.cells.db.accounts.AccountDB │ ├── 1.json │ └── 2.json ├── com.pydio.android.cells.db.auth.AuthDB │ └── 1.json ├── com.pydio.android.cells.db.nodes.TreeNodeDB │ ├── 2.json │ ├── 3.json │ ├── 4.json │ └── 5.json └── com.pydio.android.cells.db.runtime.RuntimeDB │ └── 2.json └── src ├── androidTest └── java │ └── com │ └── pydio │ └── android │ ├── cells │ ├── CheckModuleTest.kt │ └── db │ │ ├── AccountDBTest.kt │ │ └── RoomTestModule.kt │ └── legacy │ └── v2 │ ├── FileLoader.java │ ├── LegacyDBTest.java │ ├── LegacyMigrationTest.kt │ └── LegacyModelTest.java ├── main ├── AndroidManifest.xml ├── assets │ ├── CellsLogo.svg │ └── PydioAndroidSplash.svg ├── java │ └── com │ │ └── pydio │ │ └── android │ │ ├── cells │ │ ├── AppConstants.kt │ │ ├── AppKeys.java │ │ ├── AppNames.java │ │ ├── CellsApp.kt │ │ ├── MainActivity.kt │ │ ├── MigrateActivity.kt │ │ ├── SampleActivity.kt │ │ ├── db │ │ │ ├── CellsConverters.kt │ │ │ ├── accounts │ │ │ │ ├── AccountDB.kt │ │ │ │ ├── AccountDao.kt │ │ │ │ ├── RAccount.kt │ │ │ │ ├── RSession.kt │ │ │ │ ├── RSessionView.kt │ │ │ │ ├── RWorkspace.kt │ │ │ │ ├── SessionDao.kt │ │ │ │ ├── SessionViewDao.kt │ │ │ │ └── WorkspaceDao.kt │ │ │ ├── auth │ │ │ │ ├── AuthDB.kt │ │ │ │ ├── LegacyCredentialsDao.kt │ │ │ │ ├── OAuthStateDao.kt │ │ │ │ ├── RLegacyCredentials.kt │ │ │ │ ├── ROAuthState.kt │ │ │ │ ├── RToken.kt │ │ │ │ └── TokenDao.kt │ │ │ ├── nodes │ │ │ │ ├── LiveOfflineRootDao.kt │ │ │ │ ├── LocalFileDao.kt │ │ │ │ ├── OfflineRootDao.kt │ │ │ │ ├── RLiveOfflineRoot.kt │ │ │ │ ├── RLocalFile.kt │ │ │ │ ├── ROfflineRoot.kt │ │ │ │ ├── RTransfer.kt │ │ │ │ ├── RTransferCancellation.kt │ │ │ │ ├── RTreeNode.kt │ │ │ │ ├── TransferDao.kt │ │ │ │ ├── TreeNodeDB.kt │ │ │ │ └── TreeNodeDao.kt │ │ │ ├── preferences │ │ │ │ └── CellsPreferences.kt │ │ │ └── runtime │ │ │ │ ├── JobDao.kt │ │ │ │ ├── LogDao.kt │ │ │ │ ├── RJob.kt │ │ │ │ ├── RJobCancellation.kt │ │ │ │ ├── RLog.kt │ │ │ │ ├── RNetworkInfo.kt │ │ │ │ └── RuntimeDB.kt │ │ ├── di │ │ │ └── Modules.kt │ │ ├── services │ │ │ ├── AccountService.kt │ │ │ ├── AppCredentialService.kt │ │ │ ├── AuthService.kt │ │ │ ├── ConnectionService.kt │ │ │ ├── CoroutineService.kt │ │ │ ├── CredentialWatcher.kt │ │ │ ├── ErrorService.kt │ │ │ ├── FileService.kt │ │ │ ├── JobService.kt │ │ │ ├── NetworkService.kt │ │ │ ├── NodeService.kt │ │ │ ├── OfflineService.kt │ │ │ ├── P8TransferService.kt │ │ │ ├── PasswordStore.kt │ │ │ ├── PollService.kt │ │ │ ├── PreferencesService.kt │ │ │ ├── S3TransferService.kt │ │ │ ├── SessionFactory.kt │ │ │ ├── TokenStore.kt │ │ │ ├── TransferService.kt │ │ │ ├── TreeNodeRepository.kt │ │ │ ├── WorkerService.kt │ │ │ ├── models │ │ │ │ ├── CellsCancellation.kt │ │ │ │ ├── ConnectionState.kt │ │ │ │ └── SessionState.kt │ │ │ └── workers │ │ │ │ └── OfflineSyncWorker.kt │ │ ├── transfer │ │ │ ├── CellsAuthProvider.kt │ │ │ ├── CellsS3Client.kt │ │ │ ├── CellsSigner.java │ │ │ ├── CellsTransferListener.kt │ │ │ ├── FileDownloader.kt │ │ │ ├── TreeDiff.kt │ │ │ ├── WorkspaceDiff.kt │ │ │ ├── glide │ │ │ │ ├── CellsFileFetcher.kt │ │ │ │ ├── CellsGlideModule.kt │ │ │ │ ├── CellsModelLoader.kt │ │ │ │ ├── CellsModelLoaderFactory.kt │ │ │ │ └── GlideUtils.kt │ │ │ └── internal │ │ │ │ └── UrlHttpClient.java │ │ ├── ui │ │ │ ├── MainApp.kt │ │ │ ├── WithDrawer.kt │ │ │ ├── WithNavGraph.kt │ │ │ ├── account │ │ │ │ ├── AccountList.kt │ │ │ │ ├── AccountListVM.kt │ │ │ │ └── AccountsScreen.kt │ │ │ ├── browse │ │ │ │ ├── BrowseDestinations.kt │ │ │ │ ├── BrowseHelper.kt │ │ │ │ ├── BrowseNavGraph.kt │ │ │ │ ├── BrowseNavigation.kt │ │ │ │ ├── composables │ │ │ │ │ ├── BookmarkListItem.kt │ │ │ │ │ ├── NodeAction.kt │ │ │ │ │ ├── NodeActionDialogs.kt │ │ │ │ │ ├── NodeItem.kt │ │ │ │ │ ├── NodeMoreMenuData.kt │ │ │ │ │ ├── NodeWithActions.kt │ │ │ │ │ ├── NodesMoreMenuData.kt │ │ │ │ │ └── TreeNodeLargeCard.kt │ │ │ │ ├── menus │ │ │ │ │ ├── BookmarkMenu.kt │ │ │ │ │ ├── BrowseMoreMenu.kt │ │ │ │ │ ├── CreateOrImportMenu.kt │ │ │ │ │ ├── FilterTransfersByMenu.kt │ │ │ │ │ ├── MoreMenuState.kt │ │ │ │ │ ├── OfflineMenu.kt │ │ │ │ │ ├── RecycleMenu.kt │ │ │ │ │ ├── SearchMenu.kt │ │ │ │ │ ├── SortByMenu.kt │ │ │ │ │ └── TransferMoreMenu.kt │ │ │ │ ├── models │ │ │ │ │ ├── AccountHomeVM.kt │ │ │ │ │ ├── BookmarksVM.kt │ │ │ │ │ ├── CarouselVM.kt │ │ │ │ │ ├── FilterTransferByMenuVM.kt │ │ │ │ │ ├── FolderVM.kt │ │ │ │ │ ├── NodeActionsVM.kt │ │ │ │ │ ├── OfflineVM.kt │ │ │ │ │ ├── SingleTransferVM.kt │ │ │ │ │ ├── SortByMenuVM.kt │ │ │ │ │ ├── TransfersVM.kt │ │ │ │ │ └── TreeNodeVM.kt │ │ │ │ └── screens │ │ │ │ │ ├── AccountHome.kt │ │ │ │ │ ├── Bookmarks.kt │ │ │ │ │ ├── Carousel.kt │ │ │ │ │ ├── Folder.kt │ │ │ │ │ ├── NoAccount.kt │ │ │ │ │ ├── OfflineRoots.kt │ │ │ │ │ └── Transfers.kt │ │ │ ├── core │ │ │ │ ├── AbstractCellsVM.kt │ │ │ │ ├── UiConstants.kt │ │ │ │ ├── Utils.kt │ │ │ │ ├── composables │ │ │ │ │ ├── Buttons.kt │ │ │ │ │ ├── ConnectionStatus.kt │ │ │ │ │ ├── Descriptions.kt │ │ │ │ │ ├── Forms.kt │ │ │ │ │ ├── InternetBanner.kt │ │ │ │ │ ├── Node.kt │ │ │ │ │ ├── NodeThumb.kt │ │ │ │ │ ├── Preferences.kt │ │ │ │ │ ├── Thumb.kt │ │ │ │ │ ├── Titles.kt │ │ │ │ │ ├── TopBars.kt │ │ │ │ │ ├── animations │ │ │ │ │ │ ├── LinearProgress.kt │ │ │ │ │ │ └── LoadingAnimation.kt │ │ │ │ │ ├── dialogs │ │ │ │ │ │ ├── AskForConfirmation.kt │ │ │ │ │ │ ├── AskForFolderName.kt │ │ │ │ │ │ ├── AskForNewName.kt │ │ │ │ │ │ ├── CellsAlertDialog.kt │ │ │ │ │ │ └── DummyDialog.kt │ │ │ │ │ ├── lists │ │ │ │ │ │ ├── BrowseUpItem.kt │ │ │ │ │ │ ├── GenericItems.kt │ │ │ │ │ │ ├── GenericList.kt │ │ │ │ │ │ └── GridLargeCard.kt │ │ │ │ │ ├── menus │ │ │ │ │ │ ├── BottomSheet.kt │ │ │ │ │ │ └── menu.kt │ │ │ │ │ └── modal │ │ │ │ │ │ ├── ModalBottomSheet.kt │ │ │ │ │ │ ├── Strings.kt │ │ │ │ │ │ └── Swipeable.kt │ │ │ │ ├── nav │ │ │ │ │ ├── AccountHeader.kt │ │ │ │ │ ├── AppDrawer.kt │ │ │ │ │ ├── AppPermanentDrawer.kt │ │ │ │ │ ├── CellsNavigation.kt │ │ │ │ │ ├── DefaultTopAppBar.kt │ │ │ │ │ └── IntentFactory.kt │ │ │ │ └── screens │ │ │ │ │ ├── AuthScreen.kt │ │ │ │ │ └── WhiteScreen.kt │ │ │ ├── data │ │ │ │ └── Previews.kt │ │ │ ├── login │ │ │ │ ├── LoginDestinations.kt │ │ │ │ ├── LoginHelper.kt │ │ │ │ ├── LoginNavGraph.kt │ │ │ │ ├── LoginNavigation.kt │ │ │ │ ├── models │ │ │ │ │ └── LoginVM.kt │ │ │ │ ├── nav │ │ │ │ │ ├── NavigationState.kt │ │ │ │ │ ├── RouteNavigator.kt │ │ │ │ │ └── StateViewModel.kt │ │ │ │ └── screens │ │ │ │ │ ├── AskServerUrl.kt │ │ │ │ │ ├── DefaultLoginPage.kt │ │ │ │ │ ├── LaunchOAuthFlow.kt │ │ │ │ │ ├── P8Credentials.kt │ │ │ │ │ ├── SkipVerify.kt │ │ │ │ │ └── Starting.kt │ │ │ ├── migration │ │ │ │ ├── MigrationHost.kt │ │ │ │ └── MigrationVM.kt │ │ │ ├── models │ │ │ │ ├── AppState.kt │ │ │ │ ├── BrowseRemoteVM.kt │ │ │ │ ├── DownloadVM.kt │ │ │ │ ├── ErrorMessage.kt │ │ │ │ ├── GenericItem.kt │ │ │ │ └── TreeNodeItem.kt │ │ │ ├── search │ │ │ │ ├── HitList.kt │ │ │ │ ├── Search.kt │ │ │ │ ├── SearchHelper.kt │ │ │ │ └── SearchVM.kt │ │ │ ├── share │ │ │ │ ├── ShareDestinations.kt │ │ │ │ ├── ShareHelper.kt │ │ │ │ ├── ShareNavGraph.kt │ │ │ │ ├── ShareNavigation.kt │ │ │ │ ├── composables │ │ │ │ │ ├── TargetAccountList.kt │ │ │ │ │ ├── TransferListBottomSheet.kt │ │ │ │ │ └── TransferListItem.kt │ │ │ │ ├── models │ │ │ │ │ ├── MonitorUploadsVM.kt │ │ │ │ │ └── ShareVM.kt │ │ │ │ └── screens │ │ │ │ │ ├── SelectFolder.kt │ │ │ │ │ ├── SelectTargetAccount.kt │ │ │ │ │ └── UploadProgress.kt │ │ │ ├── system │ │ │ │ ├── SystemNavGraph.kt │ │ │ │ ├── SystemNavigation.kt │ │ │ │ ├── models │ │ │ │ │ ├── HouseKeepingVM.kt │ │ │ │ │ ├── JobListVM.kt │ │ │ │ │ ├── LandingVM.kt │ │ │ │ │ ├── LogListVM.kt │ │ │ │ │ ├── PreLaunchVM.kt │ │ │ │ │ ├── PrefReadOnlyVM.kt │ │ │ │ │ └── SettingsVM.kt │ │ │ │ └── screens │ │ │ │ │ ├── About.kt │ │ │ │ │ ├── HouseKeeping.kt │ │ │ │ │ ├── Jobs.kt │ │ │ │ │ ├── Logs.kt │ │ │ │ │ ├── Migration.kt │ │ │ │ │ ├── Settings.kt │ │ │ │ │ └── Splash.kt │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Icons.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Typography.kt │ │ └── utils │ │ │ ├── AndroidCustomEncoder.java │ │ │ ├── BackOffTicker.kt │ │ │ ├── DateUtils.kt │ │ │ ├── FileUtils.kt │ │ │ ├── NavigationUtils.kt │ │ │ ├── NodeUtils.kt │ │ │ └── Utils.kt │ │ └── legacy │ │ └── v2 │ │ ├── AccountRecord.java │ │ ├── LegacyAccountRecord.java │ │ ├── MigrationServiceV2.kt │ │ ├── V2MainDB.kt │ │ ├── V2SyncDB.kt │ │ └── WatchInfo.java ├── res-overrides │ ├── drawable │ │ ├── arrow_back_ios_new_24px.xml │ │ ├── audio_file_40px.xml │ │ ├── cells_logo_night.xml │ │ ├── cloud_download_24px.xml │ │ ├── delete_24px.xml │ │ ├── description_40px.xml │ │ ├── draft_40px.xml │ │ ├── folder_24px.xml │ │ ├── folder_shared_24px.xml │ │ ├── folder_zip_40px.xml │ │ ├── ic_baseline_arrow_back_ios_new_24.xml │ │ ├── ic_baseline_filter_list_24.xml │ │ ├── image_40px.xml │ │ ├── more_vert_24px.xml │ │ ├── picture_as_pdf_40px.xml │ │ ├── qr_code_40px.xml │ │ ├── splashscreen_icon_night.xml │ │ ├── splashscreen_icon_night_v31.xml │ │ ├── splashscreen_icon_v31.xml │ │ └── video_file_40px.xml │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_foreground.webp │ │ └── ic_launcher_round.webp │ ├── values-land │ │ └── dimens.xml │ ├── values-night-v31 │ │ └── themes.xml │ ├── values-night │ │ ├── colors.xml │ │ └── themes.xml │ ├── values-v31 │ │ └── themes.xml │ └── values │ │ └── ic_launcher_background.xml └── res │ ├── drawable │ ├── aa_200_arrow_back_ios_new_24px.xml │ ├── aa_200_folder_48px.xml │ ├── aa_200_folder_shared_48px.xml │ ├── aa_200_folder_special_48px.xml │ ├── aa_300_more_vert_40px.xml │ ├── aa_new_star_40px.xml │ ├── branding_image.xml │ ├── branding_image_nobg.xml │ ├── cells_logo.xml │ ├── empty.xml │ ├── file_cells_logo.xml │ ├── file_code_outline.xml │ ├── file_document_outline.xml │ ├── file_excel_outline.xml │ ├── file_image_outline.xml │ ├── file_outline.xml │ ├── file_pdf_box.xml │ ├── file_powerpoint_outline.xml │ ├── file_trash_outline.xml │ ├── file_word_outline.xml │ ├── file_zip_outline.xml │ ├── ic_baseline_check_24.xml │ ├── ic_baseline_link_24.xml │ ├── ic_baseline_star_border_24.xml │ ├── ic_baseline_wifi_protected_setup_24.xml │ ├── ic_outline_audio_file_24.xml │ ├── ic_outline_download_done_24.xml │ ├── ic_outline_running_with_errors_24.xml │ ├── ic_outline_video_file_24.xml │ ├── ic_round_warning_24.xml │ ├── image_no_thumb_small.xml │ ├── loading.xml │ ├── loading_img.xml │ ├── multiple_action.xml │ ├── pydio_logo.xml │ ├── splash_animation.xml │ ├── splash_no_logo.xml │ ├── splashscreen_branding_image.xml │ ├── splashscreen_branding_image_night.xml │ └── splashscreen_icon.xml │ ├── font │ └── roboto.ttf │ ├── mipmap │ ├── cells.png │ ├── ic_launcher_foreground.png │ └── loading.png │ ├── values-cs │ ├── arrays.xml │ └── strings.xml │ ├── values-de │ ├── arrays.xml │ └── strings.xml │ ├── values-es-rES │ ├── arrays.xml │ └── strings.xml │ ├── values-fr │ ├── arrays.xml │ └── strings.xml │ ├── values-it │ ├── arrays.xml │ └── strings.xml │ ├── values-ja │ ├── arrays.xml │ └── strings.xml │ ├── values-pt-rBR │ ├── arrays.xml │ └── strings.xml │ ├── values-ru │ ├── arrays.xml │ └── strings.xml │ ├── values │ ├── arrays.xml │ ├── colors.xml │ ├── dimens.xml │ ├── integers.xml │ ├── strings.xml │ ├── styles.xml │ ├── themes.xml │ ├── types.xml │ └── values.xml │ └── xml │ ├── provider_paths.xml │ └── searchable.xml └── test ├── java └── com │ └── pydio │ └── android │ └── cells │ ├── dummy │ ├── ExampleUnitTest.java │ ├── HelloAppTest.kt │ ├── HelloApplication.kt │ ├── HelloMessageData.kt │ ├── HelloModule.kt │ └── HelloService.kt │ ├── integration │ ├── BasicConnectionTest.java │ ├── TestClient.java │ └── app │ │ └── CellsClientTest.java │ └── test │ ├── DebugNodeHandler.java │ ├── LocalTestUtils.java │ └── TestClientFactory.java └── resources ├── accounts ├── cells-http.properties ├── cells-https.properties ├── cells-selfsigned.properties ├── default.properties └── p8.properties └── images ├── Logo1.png ├── Logo2.png └── Logo3.png /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /release/ 3 | /src/androidTest/resources/legacy/basic-setup 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | *Pydio Cells and the Android client are a free and open source project. We are very glad to welcome 4 | your contribution. To make the process as seamless as possible, we recommend you read this 5 | contribution guide*. 6 | 7 | [TODO] but the process to build the app is basically explained in the [packaging project](https://github.com/pydio/cells-android-app). 8 | 9 | > You wanna help? Already say "Hello" in the [forum](https://forum.pydio.com)!! 10 | > 11 | > It might motivate us to finish this page... 12 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /src/main/res/values/strings.xml 3 | translation: /src/main/res/values-%locale%/strings.xml 4 | languages_mapping: 5 | locale: 6 | cs: cs 7 | de: de 8 | es-ES: es-rES 9 | fr: fr 10 | it: it 11 | ja: ja 12 | pt-BR: pt-rBR 13 | ru: ru 14 | - source: /src/main/res/values/arrays.xml 15 | translation: /src/main/res/values-%locale%/arrays.xml 16 | languages_mapping: 17 | locale: 18 | cs: cs 19 | de: de 20 | es-ES: es-rES 21 | fr: fr 22 | it: it 23 | ja: ja 24 | pt-BR: pt-rBR 25 | ru: ru 26 | -------------------------------------------------------------------------------- /src/androidTest/java/com/pydio/android/cells/CheckModuleTest.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import com.pydio.android.cells.services.NetworkService 5 | import com.pydio.cells.utils.Log 6 | import org.junit.Test 7 | import org.koin.core.component.inject 8 | import org.koin.test.AutoCloseKoinTest 9 | 10 | /** Test that Koin configuration is valid by starting the full android test context */ 11 | class ModuleCheckTest : AutoCloseKoinTest() { 12 | 13 | private val logTag = "ModuleCheckTest" 14 | 15 | private val networkService by inject() 16 | 17 | // Check is implicitly done: the whole App is started 18 | // and to-be-injected objects have already been instantiated. 19 | // If there is a problem, an error should be thrown during @Before phase. 20 | @Test 21 | fun simplyStartApplicationContext() { 22 | val context = InstrumentationRegistry.getInstrumentation().targetContext 23 | Log.i(logTag, "Got a context: ${context.packageName}") 24 | // Log.i(logTag, "Network status: ${networkService.networkStatus}") 25 | } 26 | } 27 | 28 | // @Test 29 | // fun myModuleCheck() = checkModules { 30 | // modules(dbTestModule) 31 | // } 32 | 33 | // @Test 34 | // fun dryRun() { 35 | // val context = InstrumentationRegistry.getInstrumentation().targetContext 36 | // startKoin { 37 | // printLogger(Level.DEBUG) 38 | // androidContext(context) 39 | // modules(dbTestModule) 40 | // }.checkModules() 41 | // } 42 | -------------------------------------------------------------------------------- /src/androidTest/java/com/pydio/android/cells/db/RoomTestModule.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db 2 | 3 | import androidx.room.Room 4 | import com.pydio.android.cells.db.accounts.AccountDB 5 | import org.koin.android.ext.koin.androidContext 6 | import org.koin.dsl.module 7 | 8 | /** 9 | * In-Memory Room V2MainDB definition 10 | */ 11 | val dbTestModule = module { 12 | single { 13 | // In-Memory database config 14 | Room.inMemoryDatabaseBuilder(androidContext().applicationContext, AccountDB::class.java) 15 | .allowMainThreadQueries() 16 | .build() 17 | } 18 | } -------------------------------------------------------------------------------- /src/androidTest/java/com/pydio/android/legacy/v2/FileLoader.java: -------------------------------------------------------------------------------- 1 | package com.pydio.android.legacy.v2; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | /** 9 | * Simply retrieves a File from the androidTest/resources folder. 10 | */ 11 | public class FileLoader { 12 | 13 | public static File getResourceAsFile(String resourcePath) { 14 | try { 15 | InputStream in = FileLoader.class.getResourceAsStream(resourcePath); 16 | if (in == null) { 17 | return null; 18 | } 19 | 20 | File tempFile = File.createTempFile(String.valueOf(in.hashCode()), ".tmp"); 21 | tempFile.deleteOnExit(); 22 | 23 | try (FileOutputStream out = new FileOutputStream(tempFile)) { 24 | byte[] buffer = new byte[1024]; 25 | int bytesRead; 26 | while ((bytesRead = in.read(buffer)) != -1) { 27 | out.write(buffer, 0, bytesRead); 28 | } 29 | } 30 | return tempFile; 31 | } catch (IOException e) { 32 | e.printStackTrace(); 33 | return null; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/AppKeys.java: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells; 2 | 3 | /** 4 | * Centralises application-wide keys. 5 | */ 6 | public interface AppKeys { 7 | 8 | /* Parameter keys for compose routes */ 9 | String STATE_ID = "state-id"; 10 | String STATE_IDS = "state-ids"; 11 | String UID = "uid"; 12 | String SKIP_VERIFY = "skip-verify"; 13 | String LOGIN_CONTEXT = "login-context"; 14 | String QUERY_CONTEXT = "query-context"; 15 | 16 | /* Intent keys */ 17 | // OAuth Code Flow 18 | String QUERY_KEY_CODE = "code"; 19 | String QUERY_KEY_STATE = "state"; 20 | // String EXTRA_STATE = AppNames.KEY_PREFIX_ + "state"; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/MigrateActivity.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.navigation.compose.rememberNavController 9 | import com.pydio.android.cells.ui.migration.MigrationHost 10 | import com.pydio.android.cells.ui.migration.MigrationVM 11 | import com.pydio.android.cells.ui.theme.UseCellsTheme 12 | import org.koin.androidx.viewmodel.ext.android.viewModel 13 | 14 | /** 15 | * We use a distinct activity for migration so that we are sure that 16 | * the legacy repositories and objects are not loaded we are on the "happy path" a.k.a 17 | * when no migration is required 18 | */ 19 | class MigrateActivity : ComponentActivity() { 20 | 21 | private val logTag = "MigrateActivity" 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | Log.d(logTag, "onCreate: launching migration process") 25 | super.onCreate(savedInstanceState) 26 | 27 | val migrateActivity = this 28 | setContent { 29 | UseCellsTheme { 30 | val navController = rememberNavController() 31 | val migrationVM by viewModel() 32 | val afterMigration: () -> Unit = { 33 | val intent = Intent(migrateActivity, MainActivity::class.java) 34 | intent.action = Intent.ACTION_MAIN 35 | intent.addCategory(Intent.CATEGORY_LAUNCHER) 36 | startActivity(intent) 37 | migrateActivity.finish() 38 | } 39 | 40 | MigrationHost( 41 | navController = navController, 42 | migrationVM = migrationVM, 43 | afterMigration 44 | ) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/CellsConverters.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db 2 | 3 | import androidx.annotation.Keep 4 | import androidx.room.TypeConverter 5 | import com.google.gson.Gson 6 | import com.google.gson.reflect.TypeToken 7 | import com.pydio.cells.api.ServerURL 8 | import com.pydio.cells.transport.ServerURLImpl 9 | import java.util.Properties 10 | 11 | /** Enable serialization / deserialization to store complex objects in the DB using JSON */ 12 | class CellsConverters { 13 | 14 | @TypeConverter 15 | @Keep 16 | fun toProperties(value: String): Properties { 17 | val newType = TypeToken.get(Properties::class.javaObjectType) 18 | return Gson().fromJson(value, newType) 19 | } 20 | 21 | @TypeConverter 22 | fun fromProperties(meta: Properties): String { 23 | return Gson().toJson(meta) 24 | } 25 | 26 | @TypeConverter 27 | fun fromServerURL(url: ServerURL): String { 28 | return url.toJson() 29 | } 30 | 31 | @TypeConverter 32 | @Keep 33 | fun toServerURL(value: String): ServerURL { 34 | return ServerURLImpl.fromJson(value) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/accounts/AccountDB.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.accounts 2 | 3 | import androidx.room.AutoMigration 4 | import androidx.room.Database 5 | import androidx.room.RoomDatabase 6 | 7 | @Database( 8 | entities = [ 9 | RAccount::class, 10 | RSession::class, 11 | RWorkspace::class, 12 | ], 13 | views = [RSessionView::class], 14 | version = 2, 15 | exportSchema = true, 16 | autoMigrations = [ 17 | AutoMigration(from = 1, to = 2) 18 | ], 19 | ) 20 | abstract class AccountDB : RoomDatabase() { 21 | 22 | abstract fun accountDao(): AccountDao 23 | 24 | abstract fun sessionDao(): SessionDao 25 | 26 | abstract fun sessionViewDao(): SessionViewDao 27 | 28 | abstract fun workspaceDao(): WorkspaceDao 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/accounts/AccountDao.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.accounts 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.Update 8 | 9 | @Dao 10 | interface AccountDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.REPLACE) 13 | fun insert(account: RAccount) 14 | 15 | @Update 16 | fun update(account: RAccount) 17 | 18 | @Query("DELETE FROM accounts WHERE account_id = :accountID") 19 | fun forgetAccount(accountID: String) 20 | 21 | @Query("SELECT * FROM accounts WHERE account_id = :accountID LIMIT 1") 22 | fun getAccount(accountID: String): RAccount? 23 | 24 | @Query("SELECT * FROM accounts WHERE username = :username AND url = :url LIMIT 1") 25 | fun getAccount(username: String, url: String): RAccount? 26 | 27 | @Query("SELECT * FROM accounts WHERE url = :url ") 28 | fun getAccountByUrl(url: String): List 29 | 30 | @Query("SELECT * FROM accounts") 31 | fun getAccounts(): List 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/accounts/RSession.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.accounts 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.pydio.android.cells.AppNames 7 | import com.pydio.cells.transport.StateID 8 | import java.net.URL 9 | 10 | @Entity(tableName = "sessions") 11 | data class RSession( 12 | 13 | @PrimaryKey 14 | @ColumnInfo(name = "account_id") val accountID: String, 15 | 16 | // foreground, background, paused, new 17 | @ColumnInfo(name = "lifecycle_state") var lifecycleState: String, 18 | 19 | @ColumnInfo(name = "dir_name") val dirName: String, 20 | 21 | @ColumnInfo(name = "db_name") val dbName: String, 22 | 23 | // We duplicate this info to ease implementation: we can do this because we won't have tons of accounts. 24 | @ColumnInfo(name = "is_legacy") val isRemoteLegacy: Boolean, 25 | 26 | // Simple flag to make the UI more explicit for the end user. 27 | // This flag is set to false when we have internet but cannot ping the server. 28 | // It should not be used to prevent trying to reach the remote server: we should try to ping 29 | // at each iteration, even when set to false. 30 | @ColumnInfo(name = "is_reachable", defaultValue = "true") var isReachable: Boolean, 31 | ) { 32 | 33 | companion object { 34 | 35 | fun newInstance(account: RAccount, index: Int): RSession { 36 | var cleanUrl = URL(account.url).host 37 | var cleanDbName = "nodes.$cleanUrl" 38 | 39 | if (index > 0) { 40 | cleanUrl += "-$index" 41 | cleanDbName += "-$index" 42 | } 43 | 44 | return RSession( 45 | accountID = account.accountId, 46 | lifecycleState = AppNames.SESSION_STATE_NEW, 47 | dirName = cleanUrl, 48 | dbName = cleanDbName, 49 | isRemoteLegacy = account.isLegacy, 50 | isReachable = true 51 | ) 52 | } 53 | } 54 | 55 | fun account(): StateID = StateID.fromId(accountID) 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/accounts/SessionDao.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.accounts 2 | 3 | import androidx.room.* 4 | import com.pydio.android.cells.AppNames 5 | 6 | @Dao 7 | interface SessionDao { 8 | 9 | @Insert(onConflict = OnConflictStrategy.REPLACE) 10 | fun insert(session: RSession) 11 | 12 | @Update 13 | fun update(session: RSession) 14 | 15 | @Query("DELETE FROM sessions WHERE account_id = :accountID") 16 | fun forgetSession(accountID: String) 17 | 18 | @Query("SELECT * FROM sessions WHERE account_id = :accountID LIMIT 1") 19 | fun getSession(accountID: String): RSession? 20 | 21 | @Query("SELECT * FROM sessions") 22 | fun getSessions(): List 23 | 24 | @Query("SELECT * FROM sessions WHERE lifecycle_state = :lifecycleState LIMIT 1") 25 | fun getForegroundSession(lifecycleState: String = AppNames.LIFECYCLE_STATE_FOREGROUND): RSession? 26 | 27 | /** 28 | * Convenience method to insure we reset all sessions to be in the background before 29 | * putting one live. 30 | */ 31 | @Query("SELECT * FROM sessions WHERE lifecycle_state = :lifecycleState") 32 | fun listAllForegroundSessions(lifecycleState: String = AppNames.LIFECYCLE_STATE_FOREGROUND): List 33 | 34 | @Query("SELECT * FROM sessions WHERE lifecycle_state = :lifecycleState") 35 | fun listAllBackgroundSessions(lifecycleState: String = AppNames.LIFECYCLE_STATE_BACKGROUND): List 36 | 37 | @Query("SELECT * FROM sessions WHERE dir_name = :dirName") 38 | fun getWithDirName(dirName: String): List 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/accounts/SessionViewDao.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.accounts 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | @Dao 8 | interface SessionViewDao { 9 | 10 | // Suspend functions to be called from the Io dispatcher 11 | @Query("SELECT * FROM RSessionView where account_id = :accountID") 12 | fun getSession(accountID: String): RSessionView? 13 | 14 | @Query("SELECT * FROM RSessionView where lifecycle_state = :state LIMIT 1") 15 | fun getActiveSession(state: String): RSessionView? 16 | 17 | @Query("SELECT * FROM RSessionView") 18 | fun getSessions(): List 19 | 20 | @Query("SELECT * FROM RSessionView where is_legacy = 0") 21 | fun getCellsSessions(): List 22 | 23 | // Exposes Reactive Flows 24 | 25 | @Query("SELECT * FROM RSessionView") 26 | fun getLiveSessions(): Flow> 27 | 28 | @Query("SELECT * FROM RSessionView where account_id = :accountID") 29 | fun getSessionFlow(accountID: String): Flow 30 | 31 | @Query("SELECT * FROM RSessionView where lifecycle_state = :state LIMIT 1") 32 | fun getActiveSessionFlow(state: String): Flow 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/accounts/WorkspaceDao.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.accounts 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.Update 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | @Dao 11 | interface WorkspaceDao { 12 | 13 | @Insert(onConflict = OnConflictStrategy.REPLACE) 14 | fun insert(workspace: RWorkspace) 15 | 16 | @Update 17 | fun update(workspace: RWorkspace) 18 | 19 | @Query("SELECT * FROM workspaces WHERE encoded_state = :encodedState") 20 | fun getWorkspace(encodedState: String): RWorkspace? 21 | 22 | @Query("SELECT * FROM workspaces WHERE encoded_state like :accountId || '%' ORDER BY sort_name") 23 | fun getWorkspaces(accountId: String): List 24 | 25 | @Query("DELETE FROM workspaces WHERE encoded_state = :encodedState") 26 | fun forgetWorkspace(encodedState: String) 27 | 28 | @Query("DELETE FROM workspaces WHERE encoded_state like :accountID || '%'") 29 | fun forgetAccount(accountID: String) 30 | 31 | @Query("SELECT * FROM workspaces WHERE encoded_state like :encodedParentStateID || '%' ORDER BY slug") 32 | fun getWsForDiff(encodedParentStateID: String): List 33 | 34 | @Query("SELECT * FROM workspaces WHERE encoded_state LIKE :accountId || '%' AND sort_name LIKE '8%' ORDER BY sort_name") 35 | fun getCellsFlow(accountId: String): Flow> 36 | 37 | @Query("SELECT * FROM workspaces WHERE encoded_state LIKE :accountId || '%' AND sort_name NOT LIKE '8%' ORDER BY sort_name") 38 | fun getNotCellsFlow(accountId: String): Flow> 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/auth/AuthDB.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.auth 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | 6 | @Database( 7 | entities = [ 8 | RToken::class, 9 | RLegacyCredentials::class, 10 | ROAuthState::class, 11 | ], 12 | version = 1, 13 | exportSchema = true, 14 | ) 15 | 16 | abstract class AuthDB : RoomDatabase() { 17 | 18 | abstract fun tokenDao(): TokenDao 19 | 20 | abstract fun legacyCredentialsDao(): LegacyCredentialsDao 21 | 22 | abstract fun authStateDao(): OAuthStateDao 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/auth/LegacyCredentialsDao.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.auth 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | import androidx.room.Update 8 | 9 | @Dao 10 | interface LegacyCredentialsDao { 11 | 12 | @Insert 13 | fun insert(credentials: RLegacyCredentials) 14 | 15 | @Update 16 | fun update(credentials: RLegacyCredentials) 17 | 18 | @Delete 19 | fun delete(credentials: RLegacyCredentials) 20 | 21 | @Query("DELETE FROM legacy_credentials WHERE account_id = :accountID") 22 | fun forgetPassword(accountID: String) 23 | 24 | @Query("SELECT * FROM legacy_credentials WHERE account_id = :accountID LIMIT 1") 25 | fun getCredential(accountID: String): RLegacyCredentials? 26 | 27 | @Query("SELECT * FROM legacy_credentials") 28 | fun getAll(): List 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/auth/OAuthStateDao.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.auth 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | 8 | @Dao 9 | interface OAuthStateDao { 10 | 11 | @Insert(onConflict = OnConflictStrategy.REPLACE) 12 | fun insert(roAuthState: ROAuthState) 13 | 14 | @Query("SELECT * FROM oauth_states WHERE oauth_state = :state LIMIT 1") 15 | fun get(state: String): ROAuthState? 16 | 17 | @Query("DELETE FROM oauth_states WHERE oauth_state = :state") 18 | fun delete(state: String) 19 | 20 | @Query("DELETE FROM oauth_states") 21 | fun deleteAll() 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/auth/RLegacyCredentials.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.auth 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "legacy_credentials") 8 | data class RLegacyCredentials( 9 | 10 | @PrimaryKey 11 | @ColumnInfo(name = "account_id") val accountID: String, 12 | 13 | @ColumnInfo(name = "password") val password: String, 14 | ) 15 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/auth/ROAuthState.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.auth 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import androidx.room.TypeConverters 7 | import com.pydio.android.cells.db.CellsConverters 8 | import com.pydio.cells.api.ServerURL 9 | 10 | /** 11 | * Stores the OAuth state that is used as unique identifier during the Credentials flow 12 | * and the corresponding {@code ServerURL} 13 | */ 14 | @Entity(tableName = "oauth_states") 15 | @TypeConverters(CellsConverters::class) 16 | data class ROAuthState( 17 | 18 | @PrimaryKey 19 | @ColumnInfo(name = "oauth_state") val state: String, 20 | 21 | @ColumnInfo(name = "server_url") val serverURL: ServerURL, 22 | 23 | @ColumnInfo(name = "start_ts") val startTimestamp: Long, 24 | 25 | @ColumnInfo(name = "next") val loginContext: String?, 26 | ) 27 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/auth/TokenDao.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.auth 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import androidx.room.Update 7 | 8 | @Dao 9 | interface TokenDao { 10 | 11 | @Insert 12 | fun insert(token: RToken) 13 | 14 | @Update 15 | fun update(token: RToken) 16 | 17 | @Query("SELECT * FROM tokens WHERE account_id = :accountID LIMIT 1") 18 | fun getToken(accountID: String): RToken? 19 | 20 | @Query("DELETE FROM tokens WHERE account_id = :accountID") 21 | fun deleteToken(accountID: String) 22 | 23 | @Query("DELETE FROM tokens") 24 | fun deleteAllToken() 25 | 26 | @Query("SELECT * FROM tokens") 27 | fun getAll(): List 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/nodes/LiveOfflineRootDao.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.nodes 2 | 3 | import androidx.room.Dao 4 | import androidx.room.RawQuery 5 | import androidx.sqlite.db.SupportSQLiteQuery 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | @Dao 9 | interface LiveOfflineRootDao { 10 | 11 | @RawQuery(observedEntities = [RLiveOfflineRoot::class]) 12 | fun offlineRootQueryF(query: SupportSQLiteQuery): Flow> 13 | 14 | // @Query("SELECT * FROM RLiveOfflineRoot WHERE uuid = :uuid LIMIT 1") 15 | // fun getByUuid(uuid: String): RLiveOfflineRoot? 16 | // 17 | // @Query("SELECT * FROM RLiveOfflineRoot ORDER BY sort_name") 18 | // fun getAll(): List 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/nodes/LocalFileDao.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.nodes 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.Update 8 | 9 | @Dao 10 | interface LocalFileDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.REPLACE) 13 | fun insert(localFile: RLocalFile) 14 | 15 | @Update 16 | fun update(localFile: RLocalFile) 17 | 18 | @Query("DELETE FROM local_files WHERE encoded_state = :stateId") 19 | fun delete(stateId: String) 20 | 21 | @Query("DELETE FROM local_files WHERE encoded_state = :stateId and type = :type") 22 | fun delete(stateId: String, type: String) 23 | 24 | @Query("SELECT * FROM local_files WHERE encoded_state = :encodedState and type = :type LIMIT 1") 25 | fun getFile(encodedState: String, type: String): RLocalFile? 26 | 27 | @Query("SELECT * FROM local_files WHERE encoded_state = :encodedState") 28 | fun getFiles(encodedState: String): List 29 | 30 | @Query("SELECT * FROM local_files WHERE encoded_state like :encodedState || '%'") 31 | fun getFilesUnder(encodedState: String): List 32 | 33 | @Query("DELETE FROM tree_nodes WHERE encoded_state like :encodedState || '%'") 34 | fun deleteUnder(encodedState: String) 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/nodes/OfflineRootDao.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.nodes 2 | 3 | import androidx.room.* 4 | import com.pydio.android.cells.AppNames 5 | 6 | @Dao 7 | interface OfflineRootDao { 8 | 9 | @Insert(onConflict = OnConflictStrategy.REPLACE) 10 | fun insert(treeNode: ROfflineRoot) 11 | 12 | @Update 13 | fun update(treeNode: ROfflineRoot) 14 | 15 | @Query("DELETE FROM offline_roots WHERE encoded_state = :stateId") 16 | fun delete(stateId: String) 17 | 18 | @Query("SELECT * FROM offline_roots WHERE encoded_state = :encodedState LIMIT 1") 19 | fun get(encodedState: String): ROfflineRoot? 20 | 21 | @Query("SELECT * FROM offline_roots WHERE uuid = :uuid LIMIT 1") 22 | fun getByUuid(uuid: String): ROfflineRoot? 23 | 24 | @Query("SELECT * FROM offline_roots WHERE status != :lostStatus ORDER BY sort_name") 25 | fun getAllActive(lostStatus: String? = AppNames.OFFLINE_STATUS_LOST): List 26 | 27 | @Query("SELECT * FROM offline_roots ORDER BY sort_name") 28 | fun getAll(): List 29 | 30 | // @Query("SELECT * FROM offline_roots ORDER BY sort_name") 31 | // fun getAllLive(): Flow> 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/nodes/RLocalFile.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.nodes 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import com.pydio.android.cells.AppNames 6 | import com.pydio.android.cells.utils.currentTimestamp 7 | import com.pydio.cells.transport.StateID 8 | import java.io.File 9 | 10 | @Entity( 11 | tableName = "local_files", 12 | primaryKeys = [ 13 | "encoded_state", 14 | "type" 15 | ], 16 | ) 17 | data class RLocalFile( 18 | 19 | @ColumnInfo(name = "encoded_state") val encodedState: String, 20 | 21 | // Thumb, Preview, File 22 | @ColumnInfo(name = "type") val type: String, 23 | 24 | // Might be the file (for thumbs, preview) or the rel path from base dir (for files) 25 | // e.g. common-files/test/my-image.jpg 26 | @ColumnInfo(name = "file") val file: String, 27 | 28 | // The e-tag of the **main** file: it is used to detect when the file is probably out-dated 29 | // and thus that we need to re-trigger the download of the corresponding file. 30 | @ColumnInfo(name = "etag") var etag: String?, 31 | 32 | @ColumnInfo(name = "size") var size: Long = -1L, 33 | 34 | @ColumnInfo(name = "remote_mod_ts") var remoteTS: Long, 35 | 36 | @ColumnInfo(name = "local_mod_ts") var localTS: Long = -1L, 37 | ) { 38 | 39 | fun getStateID(): StateID { 40 | return StateID.fromId(encodedState) 41 | } 42 | 43 | fun getAccountID(): StateID { 44 | return getStateID().account() 45 | } 46 | 47 | companion object { 48 | // private val logTag = "RLocalFile" 49 | 50 | fun fromFile(stateID: StateID, type: String, file: File, eTag: String?, remoteTS: Long) 51 | : RLocalFile { 52 | val filename = if (type == AppNames.LOCAL_FILE_TYPE_FILE) { 53 | stateID.path.substring(1) // we remove the leading / for easier later use 54 | } else { 55 | file.name 56 | } 57 | 58 | return RLocalFile( 59 | encodedState = stateID.id, 60 | type = type, 61 | file = filename, 62 | etag = eTag, 63 | size = file.length(), 64 | remoteTS = remoteTS, 65 | localTS = currentTimestamp() 66 | ) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/nodes/ROfflineRoot.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.nodes 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.pydio.android.cells.AppNames 7 | import com.pydio.cells.transport.StateID 8 | 9 | @Entity(tableName = "offline_roots") 10 | data class ROfflineRoot( 11 | 12 | @PrimaryKey 13 | @ColumnInfo(name = "encoded_state") var encodedState: String, 14 | 15 | @ColumnInfo(name = "uuid") val uuid: String, 16 | 17 | @ColumnInfo(name = "status") var status: String, 18 | 19 | @ColumnInfo(name = "local_mod_ts") var localModificationTS: Long = 0L, 20 | 21 | @ColumnInfo(name = "last_check_ts") var lastCheckTS: Long = 0L, 22 | 23 | @ColumnInfo(name = "message") var message: String?, 24 | 25 | @ColumnInfo(name = "sort_name") var sortName: String? = null, 26 | 27 | // Can be: internal or external, optionally with an index 28 | // TODO: we only support storage in the app files dir for the time being. 29 | @ColumnInfo(name = "storage") var storage: String = AppNames.OFFLINE_STORAGE_INTERNAL, 30 | ) { 31 | 32 | fun getStateID(): StateID { 33 | return StateID.fromId(encodedState) 34 | } 35 | 36 | companion object { 37 | fun fromTreeNode(treeNode: RTreeNode): ROfflineRoot { 38 | return ROfflineRoot( 39 | encodedState = treeNode.encodedState, 40 | uuid = treeNode.uuid, 41 | status = AppNames.OFFLINE_STATUS_NEW, 42 | localModificationTS = 0, 43 | lastCheckTS = 0, 44 | message = null, 45 | sortName = treeNode.sortName, 46 | ) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/nodes/RTransferCancellation.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.nodes 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.pydio.android.cells.utils.currentTimestamp 7 | 8 | @Entity(tableName = "transfer_cancellation") 9 | data class RTransferCancellation( 10 | @PrimaryKey 11 | // Unique ID of the transfer to stop 12 | @ColumnInfo(name = "transfer_id") val transferId: Long, 13 | // Corresponding target state (mainly for logging purposes) 14 | @ColumnInfo(name = "encoded_state") val encodedState: String, 15 | // Owner of the cancellation event (user, worker or system) 16 | @ColumnInfo(name = "owner") val owner: String, 17 | // Timestamp for the cancellation event 18 | @ColumnInfo(name = "request_ts") val requestTimestamp: Long, 19 | // By default only job children are cancelled, not the ancestor 20 | @ColumnInfo(name = "also_stop_ancestors") val alsoStopAncestors: Boolean, 21 | ) { 22 | 23 | companion object { 24 | fun cancel( 25 | transferId: Long, 26 | encodedState: String, 27 | owner: String, 28 | alsoStopAncestors: Boolean = false 29 | ): RTransferCancellation { 30 | return RTransferCancellation( 31 | transferId = transferId, 32 | encodedState = encodedState, 33 | owner = owner, 34 | alsoStopAncestors = alsoStopAncestors, 35 | requestTimestamp = currentTimestamp(), 36 | ) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/runtime/LogDao.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.runtime 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | @Dao 9 | interface LogDao { 10 | 11 | @Insert 12 | fun insert(log: RLog): Long 13 | 14 | @Query("SELECT * FROM logs ORDER BY log_id DESC") 15 | fun getLiveLogs(): Flow> 16 | 17 | @Query("SELECT * FROM logs ORDER BY timestamp DESC LIMIT 100") 18 | fun getLatest(): List 19 | 20 | @Query("SELECT * FROM logs WHERE level <= :levelId ORDER BY timestamp DESC LIMIT 100") 21 | fun getByLevelAtLeast(levelId: Int): List 22 | 23 | @Query("SELECT * FROM logs WHERE level = :levelId ORDER BY timestamp DESC LIMIT 100") 24 | fun getByLevel(levelId: Int): List 25 | 26 | @Query("DELETE FROM logs ") 27 | fun clearLogs() 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/runtime/RJobCancellation.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.runtime 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.pydio.android.cells.utils.currentTimestamp 7 | 8 | @Entity(tableName = "job_cancellation") 9 | data class RJobCancellation( 10 | 11 | @PrimaryKey 12 | @ColumnInfo(name = "job_id") 13 | var jobId: Long, 14 | 15 | @ColumnInfo(name = "request_ts") val requestTimestamp: Long, 16 | 17 | // TODO How do we pass "Cancel parent parameter"? 18 | ) { 19 | 20 | companion object { 21 | fun cancel(jobId: Long): RJobCancellation { 22 | return RJobCancellation( 23 | jobId = jobId, 24 | requestTimestamp = currentTimestamp(), 25 | ) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/runtime/RLog.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.runtime 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.pydio.android.cells.AppNames 7 | import com.pydio.android.cells.utils.currentTimestamp 8 | 9 | @Entity(tableName = "logs") 10 | data class RLog( 11 | 12 | @PrimaryKey(autoGenerate = true) 13 | @ColumnInfo(name = "log_id") 14 | var logId: Long = 0L, 15 | 16 | @ColumnInfo(name = "timestamp") val timestamp: Long, 17 | @ColumnInfo(name = "level") val level: Int, 18 | @ColumnInfo(name = "tag") val tag: String?, 19 | @ColumnInfo(name = "message") val message: String? = null, 20 | @ColumnInfo(name = "caller_id") val callerId: String? = null, 21 | ) { 22 | 23 | fun getLevelString(): String = when (level) { 24 | 1 -> AppNames.FATAL 25 | 2 -> AppNames.ERROR 26 | 3 -> AppNames.WARNING 27 | 4 -> AppNames.INFO 28 | 5 -> AppNames.DEBUG 29 | 6 -> AppNames.TRACE 30 | else -> "unknown level: $level" 31 | } 32 | 33 | companion object { 34 | 35 | fun create(level: String, tag: String?, message: String, callerId: String?): RLog { 36 | return RLog( 37 | level = when (level) { 38 | AppNames.FATAL -> 1 39 | AppNames.ERROR -> 2 40 | AppNames.WARNING -> 3 41 | AppNames.INFO -> 4 42 | AppNames.DEBUG -> 5 43 | AppNames.TRACE -> 6 44 | else -> 99 45 | }, 46 | tag = tag, 47 | message = message, 48 | callerId = callerId, 49 | timestamp = currentTimestamp(), 50 | ) 51 | } 52 | 53 | fun info(tag: String?, message: String, callerId: String?): RLog { 54 | return create(AppNames.INFO, tag, message, callerId) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/runtime/RNetworkInfo.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.runtime 2 | 3 | //import androidx.room.ColumnInfo 4 | //import androidx.room.Entity 5 | //import androidx.room.PrimaryKey 6 | //import com.pydio.android.cells.AppNames 7 | //import com.pydio.android.cells.utils.currentTimestamp 8 | // 9 | ///** 10 | // * Experimental class to try a cache of the network status. Will probably disappear soon: 11 | // * Do not rely on it. 12 | // */ 13 | //@Entity(tableName = "network_info") 14 | //data class RNetworkInfo( 15 | // 16 | // @PrimaryKey(autoGenerate = true) 17 | // var id: Long = 0L, 18 | // 19 | // // unknown, no_internet, metered, ok 20 | // @ColumnInfo(name = "status") var status: String = AppNames.NETWORK_TYPE_UNKNOWN, 21 | // 22 | // @ColumnInfo(name = "last_checked") var lastCheckedTS: Long = -1L, 23 | // 24 | // @ColumnInfo(name = "last_response_code") var lastResponseCode: Int = 200, 25 | // 26 | // @ColumnInfo(name = "last_response_msg") var lastResponseMsg: String? = null, 27 | // 28 | // ) { 29 | // 30 | // fun isOnline() = status == AppNames.NETWORK_TYPE_UNMETERED 31 | // 32 | // fun isOffline() = status == AppNames.NETWORK_TYPE_UNAVAILABLE 33 | // 34 | // companion object { 35 | // fun create( 36 | // status: String?, 37 | // lastResponseCode: Int, 38 | // lastResponseMsg: String? 39 | // ): RNetworkInfo { 40 | // return RNetworkInfo( 41 | // status = status ?: AppNames.NETWORK_TYPE_UNKNOWN, 42 | // lastResponseCode = lastResponseCode, 43 | // lastResponseMsg = lastResponseMsg, 44 | // lastCheckedTS = currentTimestamp(), 45 | // ) 46 | // } 47 | // } 48 | //} 49 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/db/runtime/RuntimeDB.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.db.runtime 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.migration.Migration 6 | import androidx.sqlite.db.SupportSQLiteDatabase 7 | 8 | @Database( 9 | entities = [ 10 | RJob::class, 11 | RJobCancellation::class, 12 | RLog::class, 13 | ], 14 | version = 2, 15 | exportSchema = true 16 | ) 17 | 18 | abstract class RuntimeDB : RoomDatabase() { 19 | 20 | abstract fun jobDao(): JobDao 21 | 22 | abstract fun logDao(): LogDao 23 | 24 | companion object { 25 | val MIGRATION_1_2 = object : Migration(1, 2) { 26 | override fun migrate(db: SupportSQLiteDatabase) { 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/services/CoroutineService.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.services 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.MainCoroutineDispatcher 6 | import kotlinx.coroutines.SupervisorJob 7 | 8 | class CoroutineService( 9 | val uiDispatcher: MainCoroutineDispatcher, 10 | val ioDispatcher: CoroutineDispatcher, 11 | val cpuDispatcher: CoroutineDispatcher, 12 | ) { 13 | 14 | private val cellsSupervisorJob = SupervisorJob() 15 | 16 | val cellsUiScope = CoroutineScope(uiDispatcher + cellsSupervisorJob) 17 | val cellsCpuScope = CoroutineScope(cpuDispatcher + cellsSupervisorJob) 18 | val cellsIoScope = CoroutineScope(ioDispatcher + cellsSupervisorJob) 19 | 20 | } -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/services/PasswordStore.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.services 2 | 3 | import com.pydio.android.cells.db.auth.LegacyCredentialsDao 4 | import com.pydio.android.cells.db.auth.RLegacyCredentials 5 | import com.pydio.cells.api.Store 6 | 7 | class PasswordStore(private val dao: LegacyCredentialsDao) : Store { 8 | 9 | override fun put(id: String, password: String) { 10 | val cred = RLegacyCredentials(accountID = id, password = password) 11 | if (dao.getCredential(id) == null) { 12 | dao.insert(cred) 13 | } else { 14 | dao.update(cred) 15 | } 16 | } 17 | 18 | override fun get(id: String): String? { 19 | return dao.getCredential(id)?.password 20 | } 21 | 22 | override fun remove(id: String) { 23 | dao.forgetPassword(id) 24 | } 25 | 26 | override fun clear() { 27 | TODO("Not yet implemented") 28 | } 29 | 30 | override fun getAll(): MutableMap { 31 | val allCredentials: MutableMap = HashMap() 32 | for (cred in dao.getAll()) { 33 | allCredentials[cred.accountID] = cred.password 34 | } 35 | return allCredentials 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/services/TokenStore.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.services 2 | 3 | import com.pydio.android.cells.db.auth.RToken 4 | import com.pydio.android.cells.db.auth.TokenDao 5 | import com.pydio.cells.api.Store 6 | import com.pydio.cells.transport.auth.Token 7 | 8 | class TokenStore(private val dao: TokenDao) : Store { 9 | 10 | override fun put(id: String, token: Token) { 11 | val rToken = RToken.fromToken(id, token) 12 | if (dao.getToken(id) == null) { 13 | dao.insert(rToken) 14 | } else { 15 | dao.update(rToken) 16 | } 17 | } 18 | 19 | override fun get(id: String): Token? { 20 | val rToken = dao.getToken(id) 21 | if (rToken != null) { 22 | return rToken.toToken() 23 | } 24 | return null 25 | } 26 | 27 | override fun remove(id: String) { 28 | dao.deleteToken(id) 29 | } 30 | 31 | override fun clear() { 32 | dao.deleteAllToken() 33 | } 34 | 35 | override fun getAll(): MutableMap { 36 | val allCredentials: MutableMap = HashMap() 37 | for (rToken in dao.getAll()) { 38 | allCredentials[rToken.accountId] = rToken.toToken() 39 | } 40 | return allCredentials 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/services/models/CellsCancellation.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.services.models 2 | 3 | import kotlinx.coroutines.CancellationException 4 | 5 | class CellsCancellation : CancellationException() -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/services/models/ConnectionState.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.services.models 2 | 3 | import com.pydio.android.cells.LoadingState 4 | import com.pydio.android.cells.ServerConnection 5 | 6 | data class ConnectionState(val loading: LoadingState, val serverConnection: ServerConnection) -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/services/models/SessionState.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.services.models 2 | 3 | import com.pydio.android.cells.LoginStatus 4 | import com.pydio.android.cells.NetworkStatus 5 | import com.pydio.android.cells.db.accounts.RSessionView 6 | import com.pydio.cells.transport.StateID 7 | 8 | data class SessionState( 9 | val accountID: StateID, 10 | val isServerReachable: Boolean, 11 | val networkStatus: NetworkStatus, 12 | val loginStatus: LoginStatus, 13 | val isServerLegacy: Boolean = false 14 | ) { 15 | companion object { 16 | fun from(view: RSessionView, status: NetworkStatus): SessionState { 17 | return SessionState( 18 | accountID = view.getStateID(), 19 | isServerReachable = view.isReachable, 20 | networkStatus = status, 21 | loginStatus = LoginStatus.fromId(view.authStatus), 22 | isServerLegacy = view.isLegacy 23 | ) 24 | } 25 | 26 | val NONE: SessionState = SessionState( 27 | accountID = StateID.NONE, 28 | networkStatus = NetworkStatus.UNKNOWN, 29 | isServerReachable = false, 30 | loginStatus = LoginStatus.Undefined 31 | ) 32 | } 33 | } 34 | 35 | fun SessionState.isOK(): Boolean { 36 | // TODO rather also rely on server connection to also take prefs limit in account 37 | return isServerReachable && networkStatus == NetworkStatus.OK && loginStatus.isConnected() 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/transfer/CellsAuthProvider.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.transfer 2 | 3 | import android.util.Log 4 | import com.amazonaws.auth.AWSCredentials 5 | import com.amazonaws.auth.AWSCredentialsProvider 6 | import com.amazonaws.auth.BasicAWSCredentials 7 | import com.pydio.cells.api.SDKException 8 | import com.pydio.cells.transport.CellsTransport 9 | import com.pydio.cells.transport.StateID 10 | 11 | class CellsAuthProvider( 12 | private val transport: CellsTransport, 13 | private val accountID: StateID 14 | ) : AWSCredentialsProvider { 15 | 16 | private val defaultGatewaySecret = "gatewaysecret" 17 | private val logTag = "CellsAuthProvider" 18 | 19 | override fun getCredentials(): AWSCredentials { 20 | return BasicAWSCredentials(transport.accessToken, defaultGatewaySecret) 21 | } 22 | 23 | override fun refresh() { 24 | Log.i(logTag, "Explicit token refresh request for $accountID") 25 | try { 26 | transport.requestTokenRefresh() 27 | } catch (se: SDKException) { 28 | Log.e(logTag, "Unexpected error while requesting token refresh for $accountID") 29 | Log.e(logTag, "#${se.code}: ${se.message}") 30 | se.printStackTrace() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/transfer/glide/CellsGlideModule.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.transfer.glide 2 | 3 | import android.content.Context 4 | import com.bumptech.glide.module.AppGlideModule 5 | import com.bumptech.glide.Glide 6 | import com.bumptech.glide.Registry 7 | import com.bumptech.glide.annotation.GlideModule 8 | import com.pydio.android.cells.transfer.glide.CellsModelLoaderFactory 9 | import java.nio.ByteBuffer 10 | 11 | @GlideModule 12 | class CellsGlideModule : AppGlideModule() { 13 | override fun registerComponents(context: Context, glide: Glide, registry: Registry) { 14 | registry.prepend(String::class.java, ByteBuffer::class.java, CellsModelLoaderFactory()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/transfer/glide/CellsModelLoader.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.transfer.glide 2 | 3 | import com.bumptech.glide.load.Options 4 | import com.bumptech.glide.load.model.ModelLoader 5 | import com.bumptech.glide.signature.ObjectKey 6 | import com.pydio.cells.utils.Log 7 | import java.nio.ByteBuffer 8 | 9 | /** 10 | * Loads a local file from an encoded state ID with a type. 11 | * If the file is not found locally, it is downloaded. 12 | * 13 | * Note that the model also contains the remote node eTAG: 14 | * if the remote image has changed, the eTAG is impacted and thus the model changes, 15 | * triggering cache invalidation for this image in Glide's layers. 16 | */ 17 | class CellsModelLoader : ModelLoader { 18 | 19 | private val logTag = "CellsModelLoader" 20 | override fun buildLoadData( 21 | model: String, 22 | width: Int, 23 | height: Int, 24 | options: Options 25 | ): ModelLoader.LoadData { 26 | return ModelLoader.LoadData(ObjectKey(model), CellsFileFetcher(model)) 27 | } 28 | 29 | override fun handles(model: String): Boolean { 30 | // TODO better validation? 31 | return try { 32 | val res = decodeModel(model) 33 | res.second.isNotEmpty() 34 | } catch (e: Exception) { 35 | Log.e(logTag, "Unexpected exception while handling $model: ${e.message}.") 36 | false 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/transfer/glide/CellsModelLoaderFactory.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.transfer.glide 2 | 3 | import com.bumptech.glide.load.model.ModelLoader 4 | import com.bumptech.glide.load.model.ModelLoaderFactory 5 | import com.bumptech.glide.load.model.MultiModelLoaderFactory 6 | import java.nio.ByteBuffer 7 | 8 | class CellsModelLoaderFactory : ModelLoaderFactory { 9 | override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { 10 | return CellsModelLoader() 11 | } 12 | 13 | override fun teardown() { 14 | // Do nothing. 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/transfer/glide/GlideUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.transfer.glide 2 | 3 | import com.pydio.android.cells.db.nodes.RTreeNode 4 | import com.pydio.cells.transport.StateID 5 | 6 | fun encodeModel(rTreeNode: RTreeNode, type: String): String { 7 | return encodeModel(rTreeNode.encodedState, rTreeNode.etag, type) 8 | } 9 | 10 | // Enable encoded model for RLiveOfflineRoot objects. 11 | fun encodeModel(encodedState: String, eTag: String?, type: String): String { 12 | // We pre-pend the model with the eTag, so that it changes when the image changes 13 | return (eTag ?: "none") + ":" + type + ":" + encodedState 14 | } 15 | 16 | fun encodeModel(type: String, stateID: StateID, eTag: String?, metaHash: Int): String { 17 | // We pre-pend the model with both eTag and meta hash, so that it changes when the image changes 18 | return (eTag ?: "none") + "$metaHash" + ":" + type + ":" + stateID.id 19 | } 20 | 21 | 22 | fun decodeModel(encoded: String): Pair { 23 | // We remove the eTag + meta hash prefix that we do not use (only serves to find modifs, see above) 24 | val model = encoded.substring(encoded.indexOf(":") + 1) 25 | val type = model.substring(0, model.indexOf(":")) 26 | val encodedState = model.substring(model.indexOf(":") + 1) 27 | // Log.d("decodeModel", "Decoded: ${StateID.fromId(encodedState)} - $type") 28 | return Pair(StateID.fromId(encodedState), type) 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/MainApp.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui 2 | 3 | import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass 4 | import androidx.compose.runtime.Composable 5 | import com.pydio.android.cells.ui.models.AppState 6 | import com.pydio.cells.transport.StateID 7 | 8 | @Composable 9 | fun MainApp( 10 | initialAppState: AppState, 11 | processSelectedTarget: (StateID?) -> Unit, 12 | emitActivityResult: (Int) -> Unit, 13 | widthSizeClass: WindowWidthSizeClass, 14 | ) { 15 | NavHostWithDrawer( 16 | initialAppState = initialAppState, 17 | processSelectedTarget = processSelectedTarget, 18 | emitActivityResult = emitActivityResult, 19 | widthSizeClass = widthSizeClass, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/browse/BrowseDestinations.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.browse 2 | 3 | import com.pydio.android.cells.AppKeys 4 | import com.pydio.android.cells.ui.core.encodeStateForRoute 5 | import com.pydio.cells.transport.StateID 6 | 7 | sealed class BrowseDestinations(val route: String) { 8 | 9 | companion object { 10 | protected const val logTag = "BrowseDestinations" 11 | protected const val PREFIX = "browse" 12 | } 13 | 14 | data object Open : BrowseDestinations("${PREFIX}/open/{${AppKeys.STATE_ID}}") { 15 | fun createRoute(stateID: StateID): String { 16 | return "${PREFIX}/open/${encodeStateForRoute(stateID)}" 17 | 18 | } 19 | 20 | fun isCurrent(route: String?): Boolean = route?.startsWith("${PREFIX}/open/") ?: false 21 | } 22 | 23 | data object OpenCarousel : BrowseDestinations("${PREFIX}/carousel/{${AppKeys.STATE_ID}}") { 24 | fun createRoute(stateID: StateID) = "${PREFIX}/carousel/${encodeStateForRoute(stateID)}" 25 | } 26 | 27 | data object Bookmarks : BrowseDestinations("${PREFIX}/bookmarks/{${AppKeys.STATE_ID}}") { 28 | fun createRoute(stateID: StateID) = "${PREFIX}/bookmarks/${encodeStateForRoute(stateID)}" 29 | fun isCurrent(route: String?): Boolean = route?.startsWith("${PREFIX}/bookmarks/") ?: false 30 | } 31 | 32 | data object OfflineRoots : BrowseDestinations("${PREFIX}/offline-roots/{${AppKeys.STATE_ID}}") { 33 | fun createRoute(stateID: StateID) = 34 | "${PREFIX}/offline-roots/${encodeStateForRoute(stateID)}" 35 | 36 | fun isCurrent(route: String?): Boolean = 37 | route?.startsWith("${PREFIX}/offline-roots/") ?: false 38 | } 39 | 40 | data object Transfers : BrowseDestinations("${PREFIX}/transfers/{${AppKeys.STATE_ID}}") { 41 | fun createRoute(stateID: StateID) = "${PREFIX}/transfers/${encodeStateForRoute(stateID)}" 42 | fun isCurrent(route: String?): Boolean = route?.startsWith("${PREFIX}/transfers/") ?: false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/browse/BrowseNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.browse 2 | 3 | import androidx.navigation.NavHostController 4 | import com.pydio.cells.transport.StateID 5 | 6 | /** Simply expose navigation actions for the Browse subGraph */ 7 | class BrowseNavigationActions(private val navController: NavHostController) { 8 | 9 | // private val logTag = "BrowseNavigationActions" 10 | 11 | fun toBrowse(stateID: StateID) { 12 | val route = BrowseDestinations.Open.createRoute(stateID) 13 | // We don't want the single top flag when browsing otherwise the native back button does not work 14 | navController.navigate(route) 15 | } 16 | 17 | fun toOfflineRoots(stateID: StateID) { 18 | val route = BrowseDestinations.OfflineRoots.createRoute(stateID) 19 | navController.navigate(route) { 20 | launchSingleTop = true 21 | } 22 | } 23 | 24 | fun toBookmarks(stateID: StateID) { 25 | val route = BrowseDestinations.Bookmarks.createRoute(stateID) 26 | navController.navigate(route) { 27 | launchSingleTop = true 28 | } 29 | } 30 | 31 | fun toTransfers(stateID: StateID) { 32 | val route = BrowseDestinations.Transfers.createRoute(stateID) 33 | navController.navigate(route) { 34 | launchSingleTop = true 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/browse/composables/NodeAction.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.browse.composables 2 | 3 | sealed class NodeAction(val id: String) { 4 | 5 | data object UnSelectAll : NodeAction("deselect_all") 6 | data object DownloadToDevice : NodeAction("download_to_device") 7 | data object ConfirmDownloadOnMetered : NodeAction("confirm_dl_on_metered") 8 | data object DownloadMultipleToDevice : NodeAction("download_multiple_to_device") 9 | data object ImportFile : NodeAction("import_file") 10 | data object CreateFolder : NodeAction("create_folder") 11 | data object Rename : NodeAction("rename") 12 | data object SelectTargetFolder : NodeAction("select_target_folder") 13 | data object CopyTo : NodeAction("copy_to") 14 | data object MoveTo : NodeAction("move_to") 15 | data object Delete : NodeAction("delete") 16 | data object RestoreFromTrash : NodeAction("restore_from_trash") 17 | data object PermanentlyRemove : NodeAction("permanently_remove") 18 | data object EmptyRecycle : NodeAction("empty_recycle") 19 | data object CopyToClipboard : NodeAction("copy_to_Clipboard") 20 | data object CreateShare : NodeAction("create_share") 21 | data object ShareWith : NodeAction("share_with") 22 | data object ShowQRCode : NodeAction("show_qr_code") 23 | data object RemoveLink : NodeAction("remove_link") 24 | data object TakePicture : NodeAction("take_picture") 25 | data object ForceResync : NodeAction("force_re_sync") 26 | data object OpenInApp : NodeAction("open_in_app") 27 | data object OpenParentLocation : NodeAction("open_parent_location") 28 | data object SortBy : NodeAction("sort_by") 29 | data object AsList : NodeAction("as_list") 30 | data object AsGrid : NodeAction("as_grid") 31 | class ToggleOffline(val isChecked: Boolean) : NodeAction("toggle_offline") 32 | class ToggleBookmark(val isChecked: Boolean) : NodeAction("toggle_bookmark") 33 | 34 | // data object AsSmallerGrid : NodeAction("as_smaller_grid") 35 | } -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/browse/composables/TreeNodeLargeCard.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.browse.composables 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import com.pydio.android.cells.ui.core.composables.getNodeDesc 6 | import com.pydio.android.cells.ui.core.composables.getNodeTitle 7 | import com.pydio.android.cells.ui.core.composables.lists.LargeCard 8 | import com.pydio.android.cells.ui.core.composables.lists.LargeCardGenericIconThumb 9 | import com.pydio.android.cells.ui.core.composables.lists.LargeCardImageThumb 10 | import com.pydio.android.cells.ui.models.TreeNodeItem 11 | 12 | @Composable 13 | fun TreeNodeLargeCard( 14 | nodeItem: TreeNodeItem, 15 | more: (() -> Unit)?, 16 | modifier: Modifier = Modifier, 17 | isSelected: Boolean = false, 18 | ) { 19 | LargeCard( 20 | title = getNodeTitle(name = nodeItem.name, mime = nodeItem.mime), 21 | desc = getNodeDesc( 22 | nodeItem.remoteModTs, 23 | nodeItem.size, 24 | nodeItem.localModStatus 25 | ), 26 | modifier = modifier, 27 | isSelected = isSelected 28 | ) { 29 | if (nodeItem.hasThumb) { 30 | LargeCardImageThumb( 31 | stateID = nodeItem.defaultStateID(), 32 | eTag = nodeItem.eTag, 33 | metaHash = nodeItem.metaHash, 34 | title = getNodeTitle(name = nodeItem.name, mime = nodeItem.mime), 35 | mime = nodeItem.mime, 36 | openMoreMenu = more 37 | ) 38 | } else { 39 | LargeCardGenericIconThumb( 40 | title = getNodeTitle(name = nodeItem.name, mime = nodeItem.mime), 41 | mime = nodeItem.mime, 42 | sortName = nodeItem.sortName, 43 | more = more 44 | ) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/browse/menus/MoreMenuState.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.browse.menus 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import com.pydio.android.cells.ui.browse.composables.NodeMoreMenuType 5 | import com.pydio.android.cells.ui.core.composables.modal.ModalBottomSheetState 6 | import com.pydio.cells.transport.StateID 7 | 8 | class MoreMenuState @OptIn(ExperimentalMaterial3Api::class) constructor( 9 | val sheetState: ModalBottomSheetState, 10 | val type: NodeMoreMenuType, 11 | val stateID: StateID, 12 | val openMoreMenu: (NodeMoreMenuType, StateID) -> Unit, 13 | ) 14 | 15 | class SetMoreMenuState @OptIn(ExperimentalMaterial3Api::class) constructor( 16 | val sheetState: ModalBottomSheetState, 17 | val type: NodeMoreMenuType, 18 | val stateIDs: Set, 19 | val openMoreMenu: (NodeMoreMenuType, Set) -> Unit, 20 | val cancelSelection: () -> Unit, 21 | ) 22 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/browse/models/AccountHomeVM.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.browse.models 2 | 3 | import android.util.Log 4 | import com.pydio.android.cells.db.accounts.RSessionView 5 | import com.pydio.android.cells.db.accounts.RWorkspace 6 | import com.pydio.android.cells.services.AccountService 7 | import com.pydio.android.cells.ui.core.AbstractCellsVM 8 | import com.pydio.cells.api.SdkNames 9 | import com.pydio.cells.transport.StateID 10 | import kotlinx.coroutines.flow.Flow 11 | 12 | /** 13 | * Exposes the repository while showing an account Home. 14 | */ 15 | class AccountHomeVM( 16 | val accountID: StateID, 17 | accountService: AccountService, 18 | ) : AbstractCellsVM() { 19 | 20 | private val logTag = "AccountHomeVM" 21 | 22 | val currSession: Flow = accountService.getLiveSession(accountID) 23 | 24 | val wss: Flow> = 25 | accountService.getWsByTypeFlow(SdkNames.WS_TYPE_DEFAULT, accountID.id) 26 | 27 | val cells: Flow> = 28 | accountService.getWsByTypeFlow(SdkNames.WS_TYPE_CELL, accountID.id) 29 | 30 | init { 31 | Log.i(logTag, "Created") 32 | } 33 | 34 | override fun onCleared() { 35 | super.onCleared() 36 | Log.i(logTag, "Cleared") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/browse/models/CarouselVM.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.browse.models 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.viewModelScope 5 | import com.pydio.android.cells.db.nodes.RTreeNode 6 | import com.pydio.android.cells.services.AccountService 7 | import com.pydio.android.cells.ui.core.AbstractCellsVM 8 | import com.pydio.android.cells.utils.isPreViewable 9 | import com.pydio.cells.transport.StateID 10 | import kotlinx.coroutines.ExperimentalCoroutinesApi 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.flatMapLatest 13 | import kotlinx.coroutines.flow.flow 14 | import kotlinx.coroutines.flow.map 15 | import kotlinx.coroutines.launch 16 | 17 | /** Hold the state for the carousel component */ 18 | class CarouselVM( 19 | initialStateID: StateID, 20 | private val accountService: AccountService, 21 | ) : AbstractCellsVM() { 22 | 23 | private val logTag = "CarouselVM" 24 | 25 | private var _isRemoteLegacy = false 26 | val isRemoteLegacy: Boolean 27 | get() = _isRemoteLegacy 28 | 29 | // Observe current folder children 30 | @OptIn(ExperimentalCoroutinesApi::class) 31 | val allOrdered: Flow> = defaultOrderPair.flatMapLatest { (orderBy, direction) -> 32 | try { 33 | nodeService.listLiveChildren(initialStateID.parent(), "", orderBy, direction) 34 | } catch (e: Exception) { 35 | // This should never happen but it has been seen in prod 36 | // Adding a failsafe to avoid crash 37 | Log.e(logTag, "Could not list children of $initialStateID: ${e.message}") 38 | flow { listOf() } 39 | } 40 | } 41 | 42 | val preViewableItems: Flow> = allOrdered.map { childList -> 43 | childList.filter { item -> 44 | val preViewable = isPreViewable(item) 45 | preViewable 46 | } 47 | } 48 | 49 | init { 50 | viewModelScope.launch { 51 | _isRemoteLegacy = accountService.isLegacy(initialStateID) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/browse/models/FilterTransferByMenuVM.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.browse.models 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.pydio.android.cells.services.PreferencesKeys 7 | import com.pydio.android.cells.services.PreferencesService 8 | import kotlinx.coroutines.flow.map 9 | import kotlinx.coroutines.launch 10 | 11 | /** Gives access to the "Filter Transfer By Status" Preference for the transfer more menu */ 12 | class FilterTransferByMenuVM( 13 | private val prefs: PreferencesService, 14 | ) : ViewModel() { 15 | 16 | private val logTag = "FilterTransferByMenuVM" 17 | 18 | val transferFilter = prefs.cellsPreferencesFlow.map { cellsPreferences -> 19 | cellsPreferences.list.transferFilter 20 | } 21 | 22 | fun setFilterBy(newFilterByStatus: String) { 23 | viewModelScope.launch { 24 | try { 25 | prefs.setString(PreferencesKeys.TRANSFER_FILTER_BY_STATUS, newFilterByStatus) 26 | } catch (e: Exception) { 27 | Log.e(logTag, "Could not update filter by status preference: ${e.message}") 28 | // TODO forward to the end user. 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/browse/models/SingleTransferVM.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.browse.models 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import com.pydio.android.cells.db.nodes.RTransfer 6 | import com.pydio.android.cells.services.TransferService 7 | import com.pydio.cells.transport.StateID 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | /** Provides access to a RTransfer record given an account and a transfer ID*/ 11 | class SingleTransferVM( 12 | private val accountID: StateID, 13 | private val transferService: TransferService, 14 | ) : ViewModel() { 15 | 16 | private val logTag = "SingleTransferVM" 17 | 18 | fun getTransfer(transferID: Long): Flow = 19 | transferService.liveTransfer(accountID, transferID) 20 | 21 | init { 22 | Log.d(logTag, "after init for $accountID") 23 | } 24 | 25 | override fun onCleared() { 26 | super.onCleared() 27 | Log.d(logTag, "after clear for $accountID") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/browse/models/SortByMenuVM.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.browse.models 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.pydio.android.cells.ListType 7 | import com.pydio.android.cells.services.PreferencesService 8 | import kotlinx.coroutines.flow.map 9 | import kotlinx.coroutines.launch 10 | 11 | /** Gives access to the Sort By Preference for the more menu */ 12 | class SortByMenuVM( 13 | private val type: ListType, 14 | private val prefs: PreferencesService, 15 | ) : ViewModel() { 16 | 17 | private val logTag = "SortByMenuVM" 18 | 19 | val encodedOrder = prefs.cellsPreferencesFlow.map { cellsPreferences -> 20 | when (type) { 21 | ListType.TRANSFER -> cellsPreferences.list.transferOrder 22 | ListType.JOB -> cellsPreferences.list.jobOrder 23 | ListType.DEFAULT -> cellsPreferences.list.order 24 | } 25 | } 26 | 27 | fun setSortBy(newSortBy: String) { 28 | viewModelScope.launch { 29 | try { 30 | prefs.setOrder(type, newSortBy) 31 | } catch (e: Exception) { 32 | Log.e(logTag, "Could not update filter by status pref: ${e.message}") 33 | // TODO forward to the end user. 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/browse/screens/NoAccount.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.browse.screens 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.Add 7 | import androidx.compose.material3.FloatingActionButton 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.Scaffold 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.stringResource 14 | import com.pydio.android.cells.R 15 | import com.pydio.android.cells.ui.core.composables.DefaultTopBar 16 | 17 | @Composable 18 | fun NoAccount( 19 | openDrawer: () -> Unit, 20 | addAccount: () -> Unit, 21 | ) { 22 | 23 | Scaffold( 24 | topBar = { 25 | DefaultTopBar(stringResource(R.string.welcome_title), openDrawer = openDrawer) 26 | }, 27 | floatingActionButton = { 28 | FloatingActionButton( 29 | onClick = { addAccount() } 30 | ) { 31 | Icon( 32 | Icons.Filled.Add, 33 | contentDescription = stringResource(R.string.welcome_add_account_button) 34 | ) 35 | } 36 | }, 37 | ) { padding -> 38 | 39 | Column(modifier = Modifier.padding(padding)) { 40 | Text(text = stringResource(R.string.welcome_subtitle)) 41 | Text(text = stringResource(R.string.welcome_text)) 42 | Text(text = stringResource(R.string.welcome_no_account_instructions)) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/UiConstants.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core 2 | 3 | // see: https://kotlinlang.org/docs/enum-classes.html#working-with-enum-constants 4 | enum class ListLayout { 5 | LIST, SMALL_GRID, GRID, LARGE_GRID 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/composables/Buttons.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.composables 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.alpha 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.res.dimensionResource 14 | import androidx.compose.ui.res.stringResource 15 | import com.pydio.android.cells.R 16 | import com.pydio.android.cells.ui.theme.CellsIcons 17 | 18 | @Composable 19 | fun SwitchAccountButton( 20 | openAccounts: () -> Unit, 21 | modifier: Modifier 22 | ) { 23 | Surface( 24 | tonalElevation = dimensionResource(R.dimen.switch_account_btn_tonal_elevation), 25 | shadowElevation = dimensionResource(R.dimen.switch_account_btn_shadow_elevation), 26 | modifier = modifier 27 | .clickable { openAccounts() } 28 | .alpha(.7f) 29 | .clip(RoundedCornerShape(dimensionResource(R.dimen.glide_thumb_radius))) 30 | ) { 31 | Icon( 32 | imageVector = CellsIcons.SwitchAccount, 33 | contentDescription = stringResource(R.string.choose_account), 34 | modifier = Modifier 35 | .padding(all = dimensionResource(R.dimen.margin_xxsmall)) 36 | .size(dimensionResource(R.dimen.list_thumb_icon_size)) 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/composables/Node.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.composables 2 | 3 | import android.text.format.DateUtils 4 | import android.text.format.Formatter 5 | import android.webkit.MimeTypeMap 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.platform.LocalContext 8 | import androidx.compose.ui.res.stringResource 9 | import com.pydio.android.cells.R 10 | import com.pydio.android.cells.db.nodes.RTreeNode 11 | import com.pydio.android.cells.ui.core.getMessageFromLocalModifStatus 12 | import com.pydio.cells.api.SdkNames 13 | import java.io.File 14 | 15 | @Composable 16 | fun getNodeTitle(name: String, mime: String): String { 17 | return if (SdkNames.NODE_MIME_RECYCLE == mime) { 18 | stringResource(R.string.recycle_bin_label) 19 | } else { 20 | name 21 | } 22 | } 23 | 24 | @Composable 25 | fun getNodeDesc( 26 | item: RTreeNode, 27 | ): String { 28 | return getNodeDesc( 29 | remoteModificationTS = item.remoteModificationTS, 30 | size = item.size, 31 | localModificationStatus = item.localModificationStatus 32 | ) 33 | } 34 | 35 | @Composable 36 | fun getNodeDesc( 37 | remoteModificationTS: Long, 38 | size: Long, 39 | localModificationStatus: String?, 40 | ): String { 41 | return localModificationStatus?.let { getMessageFromLocalModifStatus(it) } 42 | ?: run { 43 | val timestamp = DateUtils.formatDateTime( 44 | LocalContext.current, 45 | remoteModificationTS * 1000L, 46 | DateUtils.FORMAT_ABBREV_RELATIVE 47 | ) 48 | val sizeStr = Formatter.formatShortFileSize( 49 | LocalContext.current, 50 | size 51 | ) 52 | "$timestamp • $sizeStr" 53 | } 54 | } 55 | 56 | 57 | fun betterMime(passedMime: String, sortName: String?): String { 58 | return if (passedMime == SdkNames.NODE_MIME_DEFAULT) { 59 | MimeTypeMap.getSingleton().getMimeTypeFromExtension(File("./$sortName").extension) 60 | ?: SdkNames.NODE_MIME_DEFAULT 61 | } else passedMime 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/composables/animations/LinearProgress.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.composables.animations 2 | 3 | import androidx.compose.animation.core.FastOutSlowInEasing 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.material3.LinearProgressIndicator 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableFloatStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Modifier 14 | 15 | @Composable 16 | fun SmoothLinearProgressIndicator( 17 | indicatorProgress: Float, 18 | modifier: Modifier = Modifier 19 | ) { 20 | var progress by remember { mutableFloatStateOf(0f) } 21 | val progressAnimDuration = 1000 // we update progress in the db every second 22 | val progressAnimation by animateFloatAsState( 23 | label = "Progress animation", 24 | targetValue = indicatorProgress, 25 | animationSpec = tween(durationMillis = progressAnimDuration, easing = FastOutSlowInEasing) 26 | ) 27 | LinearProgressIndicator( 28 | modifier = modifier, 29 | progress = { progressAnimation } 30 | ) 31 | LaunchedEffect(indicatorProgress) { 32 | progress = indicatorProgress 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/composables/animations/LoadingAnimation.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.composables.animations 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.infiniteRepeatable 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.wrapContentSize 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.mutableFloatStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.rotate 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.res.dimensionResource 21 | import androidx.compose.ui.res.stringResource 22 | import com.pydio.android.cells.R 23 | import com.pydio.android.cells.ui.theme.CellsIcons 24 | 25 | @Composable 26 | fun LoadingAnimation( 27 | modifier: Modifier = Modifier, 28 | circleColor: Color = MaterialTheme.colorScheme.primary.copy(alpha = .3f), 29 | animationDelay: Int = 2000 30 | ) { 31 | 32 | val rotation = remember { mutableFloatStateOf(0f) } // starting point 33 | 34 | val rotationAnimate = animateFloatAsState( 35 | targetValue = rotation.value, 36 | animationSpec = infiniteRepeatable( 37 | animation = tween( 38 | durationMillis = animationDelay 39 | ) 40 | ), 41 | label = "loading_anim_value", 42 | ) 43 | 44 | LaunchedEffect(Unit) { // Initialise target 45 | rotation.value = 360f 46 | } 47 | 48 | Box( 49 | modifier = modifier 50 | .padding(dimensionResource(R.dimen.list_thumb_padding)) 51 | .fillMaxSize() 52 | .wrapContentSize(Alignment.Center) 53 | ) { 54 | Icon( 55 | imageVector = CellsIcons.Refresh, 56 | contentDescription = stringResource(id = R.string.loading_message), 57 | tint = circleColor, 58 | modifier = Modifier.rotate(rotationAnimate.value) 59 | ) 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/composables/dialogs/AskForConfirmation.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.composables.dialogs 2 | 3 | import CellsAlertDialog 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | 7 | @Composable 8 | fun AskForConfirmation( 9 | icon: ImageVector? = null, 10 | title: String, 11 | desc: String, 12 | confirm: () -> Unit, 13 | dismiss: () -> Unit, 14 | ) { 15 | CellsAlertDialog(icon = icon, title = title, desc = desc, confirm = confirm, dismiss = dismiss) 16 | } -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/composables/dialogs/CellsAlertDialog.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.material3.AlertDialog 2 | import androidx.compose.material3.Text 3 | import androidx.compose.material3.TextButton 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.res.stringResource 7 | import androidx.compose.ui.window.DialogProperties 8 | import androidx.compose.ui.window.SecureFlagPolicy 9 | import com.pydio.android.cells.R 10 | import com.pydio.android.cells.ui.core.composables.DialogTitle 11 | 12 | 13 | @Composable 14 | fun CellsAlertDialog( 15 | icon: ImageVector? = null, 16 | title: String, 17 | desc: String, 18 | confirm: () -> Unit, 19 | dismiss: () -> Unit, 20 | ) { 21 | AlertDialog( 22 | title = { 23 | DialogTitle( 24 | text = title, 25 | icon = icon 26 | ) 27 | }, 28 | text = { Text(desc) }, 29 | confirmButton = { 30 | TextButton(onClick = confirm) { Text(stringResource(R.string.button_ok)) } 31 | }, 32 | dismissButton = { 33 | TextButton(onClick = dismiss) { Text(stringResource(R.string.button_cancel)) } 34 | }, 35 | onDismissRequest = { dismiss() }, 36 | properties = DialogProperties( 37 | dismissOnBackPress = true, 38 | dismissOnClickOutside = true, 39 | securePolicy = SecureFlagPolicy.Inherit 40 | ) 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/composables/lists/GenericItems.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.composables.lists 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.res.stringResource 6 | import com.pydio.android.cells.R 7 | import com.pydio.android.cells.ui.models.MultipleItem 8 | 9 | @Composable 10 | fun MultipleGridItem( 11 | item: MultipleItem, 12 | more: () -> Unit, 13 | isSelectionMode: Boolean, 14 | isSelected: Boolean, 15 | modifier: Modifier = Modifier 16 | ) { 17 | LargeCard( 18 | title = item.name, 19 | desc = getAppearsInDesc(item), 20 | modifier = modifier, 21 | isSelected = isSelected 22 | ) { 23 | if (item.hasThumb) { 24 | LargeCardImageThumb( 25 | stateID = item.defaultStateID(), 26 | eTag = item.eTag, 27 | metaHash = item.metaHash, 28 | title = item.name, 29 | mime = item.mime, 30 | openMoreMenu = if (!isSelectionMode) more else null 31 | ) 32 | } else { 33 | LargeCardGenericIconThumb( 34 | title = item.name, 35 | mime = item.mime, 36 | sortName = item.sortName, 37 | more = if (!isSelectionMode) more else null 38 | ) 39 | } 40 | } 41 | } 42 | 43 | @Composable 44 | fun getAppearsInDesc(item: MultipleItem): String { 45 | val suffix = item.appearsIn 46 | .joinToString(", ") { item.appearsInWorkspace[it.slug] ?: it.slug } 47 | return stringResource(R.string.appears_in_prefix, suffix) 48 | } -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/composables/menus/menu.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.composables.menus 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | 5 | interface IMenuItem { 6 | fun onClick() 7 | } 8 | 9 | class SimpleMenuItem( 10 | val icon: ImageVector, 11 | val title: String, 12 | val onClick: () -> Unit, 13 | val selected: Boolean = false, 14 | ) : IMenuItem { 15 | override fun onClick() { 16 | onClick() 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/composables/modal/Strings.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.composables.modal 2 | 3 | /* 4 | * Copyright 2021 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.Immutable 21 | import androidx.compose.ui.R 22 | import androidx.compose.ui.platform.LocalConfiguration 23 | import androidx.compose.ui.platform.LocalContext 24 | 25 | @Immutable 26 | @JvmInline 27 | value class Strings private constructor(@Suppress("unused") private val value: Int) { 28 | companion object { 29 | val NavigationMenu = Strings(0) 30 | val CloseDrawer = Strings(1) 31 | val CloseSheet = Strings(2) 32 | val DefaultErrorMessage = Strings(3) 33 | val ExposedDropdownMenu = Strings(4) 34 | val SliderRangeStart = Strings(5) 35 | val SliderRangeEnd = Strings(6) 36 | } 37 | } 38 | 39 | @Composable 40 | fun getString(string: Strings): String { 41 | LocalConfiguration.current 42 | val resources = LocalContext.current.resources 43 | return when (string) { 44 | Strings.NavigationMenu -> resources.getString(R.string.navigation_menu) 45 | Strings.CloseDrawer -> resources.getString(R.string.close_drawer) 46 | Strings.CloseSheet -> resources.getString(R.string.close_sheet) 47 | Strings.DefaultErrorMessage -> resources.getString(R.string.default_error_message) 48 | Strings.ExposedDropdownMenu -> resources.getString(R.string.dropdown_menu) 49 | Strings.SliderRangeStart -> resources.getString(R.string.range_start) 50 | Strings.SliderRangeEnd -> resources.getString(R.string.range_end) 51 | else -> "" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/nav/CellsNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.nav 2 | 3 | import androidx.navigation.NavHostController 4 | import com.pydio.android.cells.AppKeys 5 | import com.pydio.android.cells.ui.core.encodeStateForRoute 6 | import com.pydio.cells.transport.StateID 7 | 8 | /** 9 | * Main generic destinations used in Cells App 10 | */ 11 | sealed class CellsDestinations(val route: String) { 12 | 13 | data object Root : CellsDestinations("root") 14 | data object Home : CellsDestinations("home") 15 | data object Accounts : CellsDestinations("accounts") 16 | 17 | data object Search : 18 | CellsDestinations("search/{${AppKeys.QUERY_CONTEXT}}/{${AppKeys.STATE_ID}}") { 19 | 20 | fun createRoute(queryContext: String, stateID: StateID) = 21 | "search/${queryContext}/${encodeStateForRoute(stateID)}" 22 | 23 | fun isCurrent(route: String?): Boolean = 24 | route?.startsWith("search/") ?: false 25 | } 26 | 27 | data object Download : 28 | CellsDestinations("download/{${AppKeys.STATE_ID}}") { 29 | 30 | fun createRoute(stateID: StateID) = 31 | "download/${encodeStateForRoute(stateID)}" 32 | 33 | fun isCurrent(route: String?): Boolean = 34 | route?.startsWith("download/") ?: false 35 | } 36 | } 37 | 38 | class CellsNavigationActions(private val navController: NavHostController) { 39 | 40 | val navigateToAccounts: () -> Unit = { 41 | navController.navigate(CellsDestinations.Accounts.route) { 42 | // Remove other screens (TODO: Really?) 43 | // and only enable one copy for this destination 44 | popUpTo(CellsDestinations.Accounts.route) { 45 | // saveState = true 46 | } 47 | launchSingleTop = true 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/nav/DefaultTopAppBar.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.nav 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.material3.Icon 5 | import androidx.compose.material3.IconButton 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TopAppBar 9 | import androidx.compose.material3.TopAppBarDefaults 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.stringResource 13 | import com.pydio.android.cells.R 14 | import com.pydio.android.cells.ui.theme.CellsIcons 15 | 16 | @OptIn(ExperimentalMaterial3Api::class) 17 | @Composable 18 | fun DefaultTopAppBar( 19 | title: String, 20 | openDrawer: () -> Unit, 21 | modifier: Modifier = Modifier, 22 | isExpandedScreen: Boolean = false, 23 | showSearch: Boolean = false, 24 | ) { 25 | TopAppBar( 26 | title = { 27 | Text(title) 28 | }, 29 | navigationIcon = { 30 | if (!isExpandedScreen){ 31 | IconButton(onClick = openDrawer) { 32 | Icon( 33 | imageVector = CellsIcons.Menu, 34 | contentDescription = stringResource(R.string.open_drawer), 35 | ) 36 | } 37 | } 38 | }, 39 | actions = { 40 | if (showSearch) { 41 | IconButton(onClick = { /* TODO: Open search */ }) { 42 | Icon( 43 | imageVector = CellsIcons.Search, 44 | contentDescription = stringResource(R.string.search_label) 45 | ) 46 | } 47 | } 48 | }, 49 | colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), 50 | modifier = modifier 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/nav/IntentFactory.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.nav 2 | 3 | import android.content.Intent 4 | import android.content.res.Resources 5 | import android.net.Uri 6 | import com.pydio.android.cells.R 7 | import com.pydio.android.cells.utils.getOSCurrentVersion 8 | import com.pydio.android.cells.utils.getTimestampAsENString 9 | import com.pydio.cells.transport.ClientData 10 | 11 | fun openExternalURL(urlStr: String): Intent { 12 | val intent = Intent(Intent.ACTION_VIEW) 13 | intent.data = Uri.parse(urlStr) 14 | return intent 15 | } 16 | 17 | fun sendSupportEmail(resources: Resources): Intent { 18 | val data = ClientData.getInstance() 19 | val format = resources.getString(R.string.app_info) 20 | val appInfo = String.format( 21 | format, 22 | data.versionCode, 23 | data.version, 24 | getTimestampAsENString(data.lastUpdateTime), 25 | getOSCurrentVersion(), 26 | ) 27 | val summary = "\n\nPlease describe your problem (in English): \n" 28 | 29 | val intent = Intent(Intent.ACTION_SEND) 30 | .setType("text/plain") 31 | .putExtra(Intent.EXTRA_SUBJECT, resources.getString(R.string.support_email_subject)) 32 | .putExtra(Intent.EXTRA_TEXT, appInfo + summary) 33 | .putExtra(Intent.EXTRA_EMAIL, arrayOf(resources.getString(R.string.support_email))) 34 | 35 | return intent 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/core/screens/WhiteScreen.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.core.screens 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.wrapContentSize 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | 12 | @Composable 13 | fun WhiteScreen() { 14 | Box( 15 | modifier = Modifier 16 | .background(MaterialTheme.colorScheme.background) 17 | .fillMaxSize() 18 | .wrapContentSize(Alignment.Center) 19 | ) { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/login/LoginNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.login 2 | 3 | import androidx.navigation.NavHostController 4 | import com.pydio.cells.transport.StateID 5 | 6 | /** Simply expose navigation actions for the Login subGraph */ 7 | class LoginNavigation(private val navController: NavHostController) { 8 | 9 | fun start(stateID: StateID?) { 10 | val route = LoginDestinations.Starting.createRoute(stateID ?: StateID.NONE) 11 | navController.navigate(route) { 12 | launchSingleTop = true 13 | } 14 | } 15 | 16 | fun done(stateID: StateID?) { 17 | val route = LoginDestinations.Done.createRoute(stateID ?: StateID.NONE) 18 | navController.navigate(route) 19 | } 20 | 21 | fun askUrl() { 22 | val route = LoginDestinations.AskUrl.createRoute() 23 | navController.navigate(route) 24 | } 25 | 26 | fun skipVerify(stateID: StateID?) { 27 | val route = 28 | LoginDestinations.SkipVerify.createRoute(stateID ?: StateID.NONE) 29 | navController.navigate(route) 30 | } 31 | 32 | fun back() { 33 | navController.popBackStack() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/login/nav/NavigationState.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.login.nav 2 | 3 | import java.util.UUID 4 | 5 | /** 6 | * State that can be used to trigger navigation. 7 | */ 8 | sealed class NavigationState { 9 | 10 | object Idle : NavigationState() 11 | 12 | /** 13 | * @param id is used so that multiple instances of the same route will trigger multiple navigation calls. 14 | */ 15 | data class NavigateToRoute(val route: String, val id: String = UUID.randomUUID().toString()) : 16 | NavigationState() 17 | 18 | /** 19 | * @param staticRoute is the static route to pop to, without parameter replacements. 20 | */ 21 | data class PopToRoute(val staticRoute: String, val id: String = UUID.randomUUID().toString()) : 22 | NavigationState() 23 | 24 | data class NavigateUp(val id: String = UUID.randomUUID().toString()) : NavigationState() 25 | } -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/login/nav/RouteNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.login.nav 2 | 3 | import androidx.annotation.VisibleForTesting 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | import kotlinx.coroutines.flow.StateFlow 6 | 7 | /** 8 | * Navigator to use when initiating navigation from a ViewModel. 9 | */ 10 | interface RouteNavigator { 11 | fun onNavigated(state: NavigationState) 12 | fun navigateUp() 13 | fun popToRoute(route: String) 14 | fun navigateToRoute(route: String) 15 | 16 | val navigationState: StateFlow 17 | } 18 | 19 | class CellsRouteNavigator : RouteNavigator { 20 | 21 | /** 22 | * Using a single state here, not a list of states. As a result, if you quickly 23 | * update the state multiple times, the view will only receive and handle the latest state, 24 | * which is fine for my use case. 25 | */ 26 | override val navigationState: MutableStateFlow = 27 | MutableStateFlow(NavigationState.Idle) 28 | 29 | override fun onNavigated(state: NavigationState) { 30 | // clear navigation state, if state is the current state: 31 | navigationState.compareAndSet(state, NavigationState.Idle) 32 | } 33 | 34 | override fun popToRoute(route: String) = navigate(NavigationState.PopToRoute(route)) 35 | 36 | override fun navigateUp() = navigate(NavigationState.NavigateUp()) 37 | 38 | override fun navigateToRoute(route: String) = navigate(NavigationState.NavigateToRoute(route)) 39 | 40 | @VisibleForTesting 41 | fun navigate(state: NavigationState) { 42 | navigationState.value = state 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/login/nav/StateViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.login.nav 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | class StateViewModel( 6 | private val routeNavigator: RouteNavigator, 7 | ) : ViewModel(), RouteNavigator by routeNavigator { 8 | 9 | private val logTag = "LoginStateViewModel" 10 | 11 | fun navigateTo(route: String) { 12 | // Note the trick: this part of the class declaration: 13 | // RouteNavigator by routeNavigator 14 | // "Gives" all methods of a RouteNavigator to the HomeViewModel "For free" 15 | navigateToRoute(route) 16 | } 17 | 18 | fun navigateBack() { 19 | navigateUp() 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/login/screens/LaunchOAuthFlow.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.login.screens 2 | 3 | import android.content.res.Configuration 4 | import android.util.Log 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.LaunchedEffect 7 | import androidx.compose.runtime.collectAsState 8 | import androidx.compose.ui.platform.LocalContext 9 | import androidx.compose.ui.tooling.preview.Preview 10 | import com.pydio.android.cells.ui.core.screens.AuthScreen 11 | import com.pydio.android.cells.ui.login.LoginHelper 12 | import com.pydio.android.cells.ui.login.models.LoginVM 13 | import com.pydio.android.cells.ui.theme.UseCellsTheme 14 | import com.pydio.cells.transport.StateID 15 | 16 | @Composable 17 | fun LaunchOAuthFlow( 18 | stateID: StateID, 19 | skipVerify: Boolean, 20 | loginContext: String, 21 | loginVM: LoginVM, 22 | helper: LoginHelper, 23 | ) { 24 | val logTag = "LaunchAuthProcessing" 25 | 26 | val message = loginVM.message.collectAsState() 27 | val errMsg = loginVM.errorMessage.collectAsState() 28 | val context = LocalContext.current 29 | 30 | LaunchedEffect(key1 = stateID) { 31 | Log.i(logTag, "... Launch auth process for $stateID") 32 | helper.launchAuth(context, stateID, skipVerify, loginContext) 33 | } 34 | 35 | AuthScreen( 36 | isProcessing = errMsg.value.isNullOrEmpty(), 37 | message = message.value, 38 | errMsg = errMsg.value, 39 | cancel = helper::cancel 40 | ) 41 | } 42 | 43 | @Preview(name = "ProcessAuth Light") 44 | @Preview( 45 | uiMode = Configuration.UI_MODE_NIGHT_YES, 46 | showBackground = true, 47 | name = "ProcessAuth Dark" 48 | ) 49 | @Composable 50 | private fun ProcessAuthPreview() { 51 | UseCellsTheme { 52 | AuthScreen( 53 | isProcessing = true, 54 | message = "Getting credentials...", 55 | errMsg = null, 56 | cancel = {} 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/login/screens/Starting.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.login.screens 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.wrapContentSize 6 | import androidx.compose.material3.CircularProgressIndicator 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import com.pydio.android.cells.ui.theme.UseCellsTheme 13 | 14 | @Composable 15 | fun StartingLoginProcess() { 16 | Surface( 17 | modifier = Modifier 18 | .fillMaxSize() 19 | .wrapContentSize(Alignment.Center) 20 | ) { 21 | CircularProgressIndicator() 22 | } 23 | } 24 | 25 | 26 | @Preview(name = "Starting Login process Light") 27 | @Preview( 28 | uiMode = Configuration.UI_MODE_NIGHT_YES, 29 | showBackground = true, 30 | name = "Starting Login process Dark" 31 | ) 32 | @Composable 33 | private fun ProcessAuthPreview() { 34 | UseCellsTheme { 35 | StartingLoginProcess() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/models/AppState.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.models 2 | 3 | import com.pydio.cells.transport.StateID 4 | 5 | class AppState( 6 | stateID: StateID, 7 | val intentID: String?, 8 | val route: String?, 9 | val context: String?, 10 | ) { 11 | // We rather rely on the string version of the StateID so that AppState is serializable by default 12 | private val stateIDStr: String 13 | 14 | val stateID: StateID 15 | get() = StateID.fromId(stateIDStr) 16 | 17 | init { 18 | stateIDStr = stateID.id 19 | } 20 | 21 | companion object { 22 | val NONE = AppState( 23 | stateID = StateID.NONE, 24 | intentID = null, 25 | route = null, 26 | context = null 27 | ) 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/models/BrowseRemoteVM.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.models 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import com.pydio.android.cells.services.ConnectionService 6 | import com.pydio.android.cells.services.ErrorService 7 | import com.pydio.cells.transport.StateID 8 | import org.koin.core.component.KoinComponent 9 | 10 | class BrowseRemoteVM( 11 | private val connectionService: ConnectionService, 12 | private val errorService: ErrorService 13 | ) : ViewModel(), KoinComponent { 14 | 15 | private val logTag = "BrowseRemoteVM" 16 | 17 | val connectionState = connectionService.liveConnectionState 18 | val isLegacy = connectionService.isRemoteLegacy 19 | 20 | fun watch(newStateID: StateID, isForceRefresh: Boolean) { 21 | connectionService.setCurrentStateID(newStateID) 22 | if (isForceRefresh) { 23 | connectionService.forceRefresh() 24 | } 25 | } 26 | 27 | fun pause(oldID: StateID) { 28 | connectionService.pause(oldID) 29 | } 30 | 31 | init { 32 | Log.i(logTag, "... Main browse view model has been initialised") 33 | } 34 | 35 | override fun onCleared() { 36 | super.onCleared() 37 | Log.i(logTag, "Cleared") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/models/ErrorMessage.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.models 2 | 3 | import android.content.Context 4 | import androidx.annotation.StringRes 5 | import com.pydio.android.cells.R 6 | 7 | const val undefined = -1 8 | val unknownError = R.string.generic_error_message 9 | 10 | data class ErrorMessage( 11 | val defaultMessage: String?, 12 | @StringRes val id: Int, 13 | val formatArgs: List, 14 | ) 15 | 16 | fun toErrorMessage(context: Context, msg: ErrorMessage): String { 17 | return msg.defaultMessage ?: context.getString( 18 | msg.id, 19 | *msg.formatArgs.map { it }.toTypedArray() 20 | ) 21 | } 22 | 23 | fun fromException(e: Exception): ErrorMessage { 24 | return ErrorMessage(e.message, unknownError, listOf()) 25 | } 26 | 27 | fun fromMessage(msg: String): ErrorMessage { 28 | return ErrorMessage(msg, undefined, listOf()) 29 | } -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/search/SearchHelper.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.search 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.navigation.NavHostController 6 | import com.pydio.android.cells.ListContext 7 | import com.pydio.android.cells.ui.browse.BrowseDestinations 8 | import com.pydio.android.cells.ui.browse.BrowseHelper 9 | import com.pydio.cells.transport.StateID 10 | 11 | class SearchHelper( 12 | private val navController: NavHostController, 13 | private val searchVM: SearchVM, 14 | ) : BrowseHelper(navController, searchVM) { 15 | 16 | private val logTag = "SearchHelper" 17 | 18 | suspend fun openParentLocation(stateID: StateID) { 19 | val parent = stateID.parent() 20 | searchVM.getNode(parent)?.let { 21 | navController.navigate( 22 | BrowseDestinations.Open.createRoute(parent) 23 | ) 24 | } ?: run { 25 | if (searchVM.retrieveFolder(parent)) { 26 | navController.navigate(BrowseDestinations.Open.createRoute(parent)) 27 | } 28 | } 29 | } 30 | 31 | suspend fun open(context: Context, stateID: StateID) { 32 | Log.d(logTag, "... Calling open for $stateID") 33 | searchVM.getNode(stateID)?.let { 34 | if (it.isFolder()) { 35 | navController.navigate( 36 | BrowseDestinations.Open.createRoute(stateID) 37 | ) 38 | } else { 39 | super.open( 40 | context = context, 41 | stateID = stateID, 42 | callingContext = ListContext.SEARCH.id 43 | ) 44 | } 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/share/ShareDestinations.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.share 2 | 3 | import com.pydio.android.cells.AppKeys 4 | import com.pydio.android.cells.ui.core.encodeStateForRoute 5 | import com.pydio.cells.transport.StateID 6 | 7 | sealed class ShareDestinations(val route: String) { 8 | 9 | companion object { 10 | protected const val PREFIX = "share" 11 | fun isCurrent(route: String?): Boolean = route?.startsWith(PREFIX) ?: false 12 | } 13 | 14 | data object ChooseAccount : ShareDestinations("${PREFIX}/choose-account") { 15 | fun isCurrent(route: String?): Boolean = "${PREFIX}/choose-account" == route 16 | } 17 | 18 | data object OpenFolder : ShareDestinations("${PREFIX}/open/{${AppKeys.STATE_ID}}") { 19 | fun createRoute(stateID: StateID) = "${PREFIX}/open/${encodeStateForRoute(stateID)}" 20 | fun isCurrent(route: String?): Boolean = route?.startsWith("${PREFIX}/open/") ?: false 21 | } 22 | 23 | data object UploadInProgress : 24 | ShareDestinations("${PREFIX}/in-progress/{${AppKeys.STATE_ID}}/{${AppKeys.UID}}") { 25 | fun createRoute(stateID: StateID, jobID: Long) = 26 | "${PREFIX}/in-progress/${stateID.id}/${jobID}" 27 | 28 | fun isCurrent(route: String?): Boolean = 29 | route?.startsWith("${PREFIX}/in-progress/") ?: false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/share/ShareNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.share 2 | 3 | import androidx.navigation.NavHostController 4 | import com.pydio.android.cells.ui.browse.BrowseDestinations 5 | import com.pydio.cells.transport.StateID 6 | 7 | /** Simply expose navigation actions for the Share subGraph */ 8 | class ShareNavigation(private val navController: NavHostController) { 9 | 10 | // private val logTag = "ShareNavigation" 11 | 12 | fun toAccounts() { 13 | val route = ShareDestinations.ChooseAccount.route 14 | navController.navigate(route) { 15 | launchSingleTop = true 16 | } 17 | } 18 | 19 | fun toFolder(stateID: StateID) { 20 | val route = ShareDestinations.OpenFolder.createRoute(stateID) 21 | navController.navigate(route) 22 | } 23 | 24 | fun toTransfers(stateID: StateID, jobID: Long) { 25 | val route = ShareDestinations.UploadInProgress.createRoute(stateID, jobID) 26 | navController.navigate(route) { 27 | popUpTo(ShareDestinations.ChooseAccount.route) { inclusive = true } 28 | } 29 | } 30 | 31 | fun toParentLocation(stateID: StateID) { 32 | val route = BrowseDestinations.Open.createRoute(stateID) 33 | navController.navigate(route) { 34 | popUpTo(ShareDestinations.ChooseAccount.route) { inclusive = true } 35 | } 36 | } 37 | 38 | fun back() { 39 | navController.popBackStack() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/system/SystemNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.system 2 | 3 | import androidx.navigation.NavHostController 4 | import com.pydio.android.cells.AppKeys 5 | import com.pydio.android.cells.ui.core.encodeStateForRoute 6 | import com.pydio.cells.transport.StateID 7 | 8 | // private val logTag = "SystemNavigation" 9 | 10 | /** Defines the System and Settings destinations **/ 11 | sealed class SystemDestinations(val route: String) { 12 | 13 | companion object { 14 | protected const val PREFIX = "share" 15 | } 16 | 17 | data object About : SystemDestinations("about") 18 | data object Settings : SystemDestinations("settings") 19 | data object Logs : SystemDestinations("logs") 20 | data object Jobs : SystemDestinations("jobs") 21 | 22 | data object ClearCache : SystemDestinations("$PREFIX/clear-cache/{${AppKeys.STATE_ID}}") { 23 | fun createRoute(stateID: StateID) = "${PREFIX}/clear-cache/${encodeStateForRoute(stateID)}" 24 | fun isCurrent(route: String?): Boolean = 25 | route?.startsWith("${PREFIX}/clear-cache/") ?: false 26 | } 27 | } 28 | 29 | class SystemNavigationActions(navController: NavHostController) { 30 | 31 | val navigateToAbout: () -> Unit = { 32 | navController.navigate(SystemDestinations.About.route) { 33 | launchSingleTop = true 34 | } 35 | } 36 | 37 | val navigateToSettings: () -> Unit = { 38 | navController.navigate(SystemDestinations.Settings.route) { 39 | launchSingleTop = true 40 | } 41 | } 42 | 43 | val navigateToLogs: () -> Unit = { 44 | navController.navigate(SystemDestinations.Logs.route) { 45 | launchSingleTop = true 46 | } 47 | } 48 | 49 | val navigateToJobs: () -> Unit = { 50 | navController.navigate(SystemDestinations.Jobs.route) { 51 | launchSingleTop = true 52 | } 53 | } 54 | 55 | val navigateToClearCache: (StateID) -> Unit = { stateID -> 56 | navController.navigate(SystemDestinations.ClearCache.createRoute(stateID)) { 57 | launchSingleTop = true 58 | } 59 | } 60 | 61 | val back: () -> Unit = { 62 | navController.popBackStack() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/system/models/JobListVM.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.system.models 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.pydio.android.cells.services.JobService 6 | import kotlinx.coroutines.launch 7 | 8 | /** 9 | * Holds a list of recent jobs and provides cleaning features. 10 | */ 11 | class JobListVM( 12 | val jobService: JobService 13 | ) : ViewModel() { 14 | 15 | val jobs = jobService.listLiveJobs(true) 16 | 17 | fun clearTerminated() { 18 | viewModelScope.launch { 19 | jobService.clearTerminated() 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/system/models/LandingVM.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.system.models 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.pydio.android.cells.services.PreferencesService 7 | import com.pydio.cells.transport.ClientData 8 | import kotlinx.coroutines.launch 9 | import kotlin.properties.Delegates 10 | 11 | class LandingVM( 12 | private val prefs: PreferencesService, 13 | // private val jobService: JobService, 14 | // private val authService: AuthService, 15 | // private val accountService: AccountService, 16 | ) : ViewModel() { 17 | 18 | private val logTag = "LandingVM" 19 | private var oldVersion by Delegates.notNull() 20 | private val newVersion = ClientData.getInstance().versionCode.toInt() 21 | 22 | init { 23 | viewModelScope.launch { 24 | oldVersion = prefs.getInstalledVersion() 25 | } 26 | } 27 | 28 | override fun onCleared() { 29 | // useless: this does nothing 30 | // super.onCleared() 31 | Log.d(logTag, "... Cleared") 32 | } 33 | 34 | /** 35 | * Makes a first quick check for the happy path and returns true 36 | * only if code version is the same as the stored version 37 | * 38 | * Note: "false" means that we have to trigger the migrate activity that: 39 | * - performs advanced tests, 40 | * - does a migration (if necessary) 41 | * - updates the stored version number. 42 | * 43 | * Note: We also get "false" for fresh installs and go through migration. 44 | * It takes a few seconds more to start but subsequent starts are then faster: 45 | * they avoid instantiating legacy migration objects 46 | */ 47 | suspend fun noMigrationNeeded(): Boolean { 48 | val currInstalled = prefs.getInstalledVersion() 49 | return newVersion > 100 && newVersion == currInstalled 50 | } 51 | 52 | // suspend fun isAuthStateValid(state: String): Pair { 53 | // return authService.isAuthStateValid(state) 54 | // } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/system/models/LogListVM.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.system.models 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.pydio.android.cells.services.JobService 6 | import kotlinx.coroutines.launch 7 | 8 | /** 9 | * Holds a list of recent logs and provides various clean features (still to implement). 10 | */ 11 | class LogListVM( 12 | private val jobService: JobService 13 | ) : ViewModel() { 14 | 15 | val logs = jobService.listLogs() 16 | 17 | fun clearAllLogs() { 18 | viewModelScope.launch { 19 | jobService.clearAllLogs() 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/system/models/PrefReadOnlyVM.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.system.models 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.pydio.android.cells.services.PreferencesService 5 | import kotlinx.coroutines.flow.map 6 | 7 | /** Exposes preference state as cold flow for the various views */ 8 | class PrefReadOnlyVM( 9 | prefs: PreferencesService, 10 | ) : ViewModel() { 11 | 12 | private val cellsPreferences = prefs.cellsPreferencesFlow 13 | 14 | val showDebugTools = cellsPreferences.map { prefs -> prefs.showDebugTools } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/system/models/SettingsVM.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.system.models 2 | 3 | import androidx.datastore.preferences.core.Preferences 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.pydio.android.cells.ListType 7 | import com.pydio.android.cells.services.PreferencesService 8 | import com.pydio.android.cells.ui.core.ListLayout 9 | import kotlinx.coroutines.launch 10 | 11 | /** Expose methods used to perform house keeping on the App */ 12 | class SettingsVM( 13 | private val prefs: PreferencesService, 14 | // private val nodeService: NodeService 15 | ) : ViewModel() { 16 | 17 | // private val logTag = "SettingsVM" 18 | 19 | val cellsPreferences = prefs.cellsPreferencesFlow 20 | 21 | fun setShowRuntimeToolsFlag(show: Boolean) { 22 | viewModelScope.launch { 23 | prefs.setShowDebugToolsFlag(show) 24 | } 25 | } 26 | 27 | fun setDisablePollFlag(disablePoll: Boolean) { 28 | viewModelScope.launch { 29 | prefs.setDisablePollFlag(disablePoll) 30 | } 31 | } 32 | 33 | fun setDefaultOrder(order: String) { 34 | viewModelScope.launch { 35 | prefs.setOrder(ListType.DEFAULT, order) 36 | } 37 | } 38 | 39 | fun setListLayout(layoutStr: String) { 40 | // Log.e(logTag, "About to set list layout to $layoutStr -- ${ListLayout.GRID.name}") 41 | val layout: ListLayout = if (ListLayout.GRID.name == layoutStr) { 42 | ListLayout.GRID 43 | } else 44 | ListLayout.LIST 45 | viewModelScope.launch { 46 | prefs.setListLayout(layout) 47 | } 48 | } 49 | 50 | fun setBooleanFlag(key: Preferences.Key, flag: Boolean) { 51 | viewModelScope.launch { 52 | prefs.setBoolean(key, flag) 53 | } 54 | } 55 | 56 | fun setStringPref(key: Preferences.Key, strValue: String) { 57 | viewModelScope.launch { 58 | prefs.setString(key, strValue) 59 | } 60 | } 61 | 62 | fun setLongPref(key: Preferences.Key, value: Long) { 63 | viewModelScope.launch { 64 | prefs.setLong(key, value) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/system/screens/Splash.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.system.screens 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.layout.wrapContentWidth 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.ColorFilter 18 | import androidx.compose.ui.res.dimensionResource 19 | import androidx.compose.ui.res.painterResource 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.dp 22 | import com.pydio.android.cells.R 23 | 24 | @Composable 25 | fun Splash() { 26 | Column( 27 | modifier = Modifier 28 | .fillMaxSize() 29 | .background(MaterialTheme.colorScheme.primary) 30 | .padding(all = dimensionResource(R.dimen.margin)) 31 | .wrapContentWidth(Alignment.CenterHorizontally) 32 | ) { 33 | Surface( 34 | modifier = Modifier 35 | .fillMaxWidth() 36 | .weight(1f) 37 | ) { 38 | Image( 39 | painterResource(R.drawable.pydio_logo), 40 | colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), 41 | contentDescription = null, 42 | modifier = Modifier.size(240.dp) 43 | ) 44 | } 45 | Text( 46 | text = stringResource(id = R.string.copyright_string), 47 | color = MaterialTheme.colorScheme.onPrimary, 48 | modifier = Modifier 49 | .align(Alignment.End) 50 | .padding(all = dimensionResource(R.dimen.margin)) 51 | 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/ui/theme/Typography.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Defines the typography that have been customised for Cells 10 | val CellsTypography = Typography( 11 | // bodyLarge = TextStyle( 12 | // fontFamily = FontFamily.Default, 13 | // fontWeight = FontWeight.W500, 14 | // fontSize = 14.sp, 15 | // lineHeight = 24.sp, 16 | // letterSpacing = 0.5.sp 17 | // ), 18 | bodyMedium = TextStyle( 19 | fontFamily = FontFamily.Default, 20 | fontWeight = FontWeight.W400, 21 | fontSize = 13.sp, 22 | lineHeight = 18.sp, 23 | letterSpacing = 0.5.sp 24 | ) 25 | ) 26 | 27 | val CellsListTypography = Typography( 28 | bodyLarge = TextStyle( 29 | fontFamily = FontFamily.Default, 30 | fontWeight = FontWeight.W500, 31 | fontSize = 14.sp, 32 | lineHeight = 24.sp, 33 | letterSpacing = 0.5.sp 34 | ), 35 | bodyMedium = TextStyle( 36 | fontFamily = FontFamily.Default, 37 | fontWeight = FontWeight.W300, 38 | fontSize = 12.sp, 39 | lineHeight = 20.sp, 40 | letterSpacing = 0.5.sp 41 | ) 42 | ) -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/utils/AndroidCustomEncoder.java: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.utils; 2 | 3 | import android.os.Build; 4 | import android.util.Base64; 5 | 6 | import com.pydio.cells.api.CustomEncoder; 7 | 8 | import java.io.UnsupportedEncodingException; 9 | import java.net.URLDecoder; 10 | import java.net.URLEncoder; 11 | import java.nio.charset.StandardCharsets; 12 | 13 | public class AndroidCustomEncoder implements CustomEncoder { 14 | 15 | @Override 16 | public byte[] base64Decode(byte[] data) { 17 | return Base64.decode(data, Base64.DEFAULT | Base64.NO_WRAP | Base64.NO_PADDING | Base64.NO_CLOSE); 18 | } 19 | 20 | @Override 21 | public String base64Decode(String s) { 22 | byte[] data = s.getBytes(StandardCharsets.UTF_8); 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 24 | return new String(base64Decode(data), StandardCharsets.UTF_8); 25 | } else { 26 | return new String(base64Decode(data)); 27 | } 28 | } 29 | 30 | @Override 31 | public String utf8Encode(String value) { 32 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 33 | return URLEncoder.encode(value, StandardCharsets.UTF_8); 34 | } else { 35 | try { 36 | return URLEncoder.encode(value, "UTF-8"); 37 | } catch (UnsupportedEncodingException ex) { 38 | return value; 39 | } 40 | 41 | } 42 | } 43 | 44 | public String utf8Decode(String value) { 45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 46 | return URLDecoder.decode(value, StandardCharsets.UTF_8); 47 | } else { 48 | try { 49 | return URLDecoder.decode(value, "UTF-8"); 50 | } catch (UnsupportedEncodingException ex) { 51 | return value; 52 | } 53 | } 54 | } 55 | 56 | @Override 57 | public byte[] getUTF8Bytes(String str) { 58 | return str.getBytes(StandardCharsets.UTF_8); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/utils/BackOffTicker.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.utils 2 | 3 | import kotlin.math.min 4 | 5 | class BackOffTicker { 6 | private val backoffDuration = longArrayOf( 7 | 2, 3, 5, 10, 15, 20, 8 | 30, 30, 30, 30, 30, 30, 9 | 40, 60, 60, 120, 120, 300, 10 | 600, 600, 900, 900, 900, 1800, 3600 11 | ) 12 | private var currentBackoffIndex = 0 13 | 14 | /** Returns the next delay duration, in seconds */ 15 | fun getNextDelay(): Long { 16 | synchronized(this) { 17 | val nextDelay = backoffDuration[currentBackoffIndex] 18 | currentBackoffIndex = min(currentBackoffIndex + 1, backoffDuration.size - 1) 19 | return nextDelay 20 | } 21 | } 22 | 23 | fun getCurrentDelay(): Long { 24 | synchronized(this) { 25 | return backoffDuration[currentBackoffIndex] 26 | } 27 | } 28 | 29 | fun getCurrentIndex(): Int { 30 | synchronized(this) { 31 | return currentBackoffIndex 32 | } 33 | } 34 | 35 | fun resetIndex() { 36 | synchronized(this) { 37 | currentBackoffIndex = 0 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.utils 2 | 3 | import com.pydio.cells.api.SDKException 4 | import java.io.File 5 | import java.io.FileInputStream 6 | import java.io.IOException 7 | import java.math.BigInteger 8 | import java.security.MessageDigest 9 | import java.security.NoSuchAlgorithmException 10 | 11 | fun computeFileMd5(file: File): String { 12 | try { 13 | FileInputStream(file).use { inputStream -> 14 | val digest = MessageDigest.getInstance("MD5") 15 | val bytesBuffer = ByteArray(1024) 16 | var bytesRead: Int 17 | while (inputStream.read(bytesBuffer).also { bytesRead = it } != -1) { 18 | digest.update(bytesBuffer, 0, bytesRead) 19 | } 20 | val hashedBytes = BigInteger(1, digest.digest()) 21 | return hashedBytes.toString(16) 22 | } 23 | } catch (ex: NoSuchAlgorithmException) { 24 | // This should never happen 25 | throw SDKException( 26 | "Could not generate hash from file ${file.name}", ex 27 | ) 28 | } catch (ex: IOException) { 29 | throw SDKException( 30 | "Could not generate hash from file ${file.name}", ex 31 | ) 32 | } 33 | } 34 | 35 | fun formatBytesToMB(bytes: Long): String { 36 | val sizeInMB = bytes / (1024.0 * 1024.0) 37 | return String.format("%.1fMB", sizeInMB) 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/cells/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.utils 2 | 3 | import android.os.Build 4 | import android.util.Log 5 | import com.pydio.android.cells.AppNames 6 | import com.pydio.android.cells.ListType 7 | import com.pydio.cells.api.SDKException 8 | import java.io.File 9 | 10 | /* VARIOUS */ 11 | 12 | fun childFile(parPath: String, filename: String): File { 13 | return File(parPath + File.separator + filename) 14 | } 15 | 16 | /* CURRENT SYSTEM INFORMATION */ 17 | 18 | fun getOSCurrentVersion(): String { 19 | val release = java.lang.Double.parseDouble( 20 | java.lang.String(Build.VERSION.RELEASE).replaceAll("(\\d+[.]\\d+)(.*)", "$1") 21 | ) 22 | var codeName = "Unsupported" // Older as Jelly Bean 23 | if (release >= 4.1 && release < 4.4) codeName = "Jelly Bean" 24 | else if (release < 5) codeName = "Kit Kat" 25 | else if (release < 6) codeName = "Lollipop" 26 | else if (release < 7) codeName = "Marshmallow" 27 | else if (release < 8) codeName = "Nougat" 28 | else if (release < 9) codeName = "Oreo" 29 | else if (release < 10) codeName = "Pie" 30 | else if (release >= 10) codeName = 31 | "Android " + (release.toInt())//since API 29 no more candy code names 32 | return codeName + " v" + release + ", API Level: " + Build.VERSION.SDK_INT 33 | } 34 | 35 | fun logException(caller: String?, msg: String, e: Exception) { 36 | Log.e(caller, "$msg ${if (e is SDKException) "(Code #${e.code} )" else ""}") 37 | e.printStackTrace() 38 | } 39 | 40 | /* MANAGE PREFERENCES */ 41 | 42 | // fun parseOrder(encoded: String, type: ListType = ListType.DEFAULT): Pair { 43 | // Returns the default value for each list type if the passed encoded value is empty or not valid 44 | fun parseOrder(encoded: String?, type: ListType): Pair { 45 | var tokens = encoded?.split("||") ?: "".split("||") 46 | if (tokens.size != 2) { 47 | Log.w("parseOrder", "could not parse encoded order [$encoded]") 48 | val newDefault = when (type) { 49 | ListType.JOB -> AppNames.JOB_DEFAULT_ENCODED_ORDER 50 | ListType.TRANSFER -> AppNames.TRANSFER_DEFAULT_ENCODED_ORDER 51 | ListType.DEFAULT -> AppNames.DEFAULT_SORT_ENCODED 52 | } 53 | tokens = newDefault.split("||") 54 | } 55 | return Pair(tokens[0], tokens[1]) 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/legacy/v2/AccountRecord.java: -------------------------------------------------------------------------------- 1 | package com.pydio.android.legacy.v2; 2 | 3 | import com.pydio.cells.api.ui.WorkspaceNode; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * Additional object that defines what we store in the V2 MainDB 9 | * and can be easily serialized back and forth using gson. 10 | */ 11 | public class AccountRecord { 12 | 13 | String accountID; 14 | String username; 15 | String serverUrl; 16 | // TODO also accepted certificates here. 17 | boolean skipVerify; 18 | boolean legacy; 19 | 20 | // Also stores UI messages 21 | String serverLabel; 22 | String welcomeMessage; 23 | 24 | Map cachedWorkspaces; 25 | 26 | public String id() { 27 | return accountID; 28 | } 29 | 30 | public String getUsername() { 31 | return username; 32 | } 33 | 34 | public String url() { 35 | return serverUrl; 36 | } 37 | 38 | public boolean skipVerify() { 39 | return skipVerify; 40 | } 41 | 42 | public boolean isLegacy() { 43 | return legacy; 44 | } 45 | 46 | public void setWorkspaces(Map cachedWorkspaces) { 47 | this.cachedWorkspaces = cachedWorkspaces; 48 | } 49 | 50 | // public static AccountRecord fromServer(String username, Server server) { 51 | // AccountRecord record = new AccountRecord(); 52 | // record.username = username; 53 | // record.serverUrl = server.url(); 54 | // // Not very elegant. Improve. 55 | // StateID state = new StateID(username, server.url()); 56 | // record.accountID = state.getId(); 57 | // record.skipVerify = server.getServerURL().skipVerify(); 58 | // record.legacy = server.isLegacy(); 59 | // record.serverLabel = server.getLabel(); 60 | // record.welcomeMessage = server.getWelcomeMessage(); 61 | // return record; 62 | // } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/pydio/android/legacy/v2/LegacyAccountRecord.java: -------------------------------------------------------------------------------- 1 | package com.pydio.android.legacy.v2; 2 | 3 | import java.io.Serializable; 4 | import java.util.Properties; 5 | 6 | public class LegacyAccountRecord implements Serializable { 7 | 8 | public String ID; 9 | public Server server; 10 | public String user; 11 | 12 | public LegacyAccountRecord() { 13 | } 14 | 15 | public static class Server { 16 | public String host; 17 | public String scheme; 18 | public int port; 19 | public String path; 20 | public String version; 21 | public String versionName; 22 | public String iconURL; 23 | public String welcomeMessage; 24 | public String label; 25 | public String url; 26 | public boolean sslUnverified; 27 | public boolean legacy; 28 | public Properties properties; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/arrow_back_ios_new_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/audio_file_40px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/cells_logo_night.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/cloud_download_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/delete_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/description_40px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/draft_40px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/folder_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/folder_shared_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/folder_zip_40px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/ic_baseline_arrow_back_ios_new_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/ic_baseline_filter_list_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/image_40px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/more_vert_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/picture_as_pdf_40px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/qr_code_40px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/splashscreen_icon_night.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/splashscreen_icon_night_v31.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/splashscreen_icon_v31.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/res-overrides/drawable/video_file_40px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /src/main/res-overrides/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res-overrides/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /src/main/res-overrides/values-land/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 4 | 4 5 | 4 6 | -------------------------------------------------------------------------------- /src/main/res-overrides/values-night-v31/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/res-overrides/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #00BCD4 7 | #EE776C 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res-overrides/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/res-overrides/values-v31/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/res-overrides/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #434B65 4 | -------------------------------------------------------------------------------- /src/main/res/drawable/aa_200_arrow_back_ios_new_24px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/aa_200_folder_48px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/aa_200_folder_shared_48px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/aa_200_folder_special_48px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/aa_300_more_vert_40px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/aa_new_star_40px.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/cells_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/res/drawable/empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/res/drawable/file_cells_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/res/drawable/file_code_outline.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/file_document_outline.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/file_excel_outline.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/file_image_outline.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/file_outline.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/file_pdf_box.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/file_powerpoint_outline.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/file_trash_outline.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/file_word_outline.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/file_zip_outline.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_baseline_check_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_baseline_link_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_baseline_star_border_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_baseline_wifi_protected_setup_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_outline_audio_file_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_outline_download_done_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_outline_running_with_errors_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_outline_video_file_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/ic_round_warning_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/res/drawable/image_no_thumb_small.xml: -------------------------------------------------------------------------------- 1 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/res/drawable/loading.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/res/drawable/multiple_action.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 8 | 10 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/res/drawable/splash_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/res/drawable/splash_no_logo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/res/drawable/splashscreen_branding_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/res/drawable/splashscreen_branding_image_night.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/res/drawable/splashscreen_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/res/font/roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res/font/roboto.ttf -------------------------------------------------------------------------------- /src/main/res/mipmap/cells.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res/mipmap/cells.png -------------------------------------------------------------------------------- /src/main/res/mipmap/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res/mipmap/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src/main/res/mipmap/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/main/res/mipmap/loading.png -------------------------------------------------------------------------------- /src/main/res/values-cs/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Standard (A to Z, folders first) 5 | Název souboru (A až Z) 6 | Název souboru (Z až A) 7 | Upraveno (od nejstarších) 8 | Upraveno (od nejnovějších) 9 | Velikost (od nejmenších) 10 | Velikost (od největších) 11 | 12 | 13 | 14 | 15 | 16 | Seznam 17 | Mřížka 18 | 19 | 20 | Každý týden 21 | Každý den 22 | Každou hodinu 23 | Každých 15 minut 24 | 25 | 26 | Neomezené 27 | Ne v roamingu 28 | Připojeno (spustit jakmile je server dosažitelný, bez ohledu na typ sítě) 29 | 30 | 31 | Show all (no filter) 32 | New 33 | Processing 34 | Cancelled 35 | Done 36 | Error 37 | Timeout 38 | 39 | 40 | Standard (By creation order, newest first) 41 | Creation order (oldest first) 42 | Status (A to Z) 43 | Status (Z to A) 44 | Type (Download First) 45 | Type (Upload First) 46 | 47 | 48 | Standard (By creation order, newest first) 49 | Creation order (oldest first) 50 | Status (A to Z) 51 | Status (Z to A) 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/res/values-de/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Standard (A bis Z, Ordner zuerst) 5 | Name (A bis Z) 6 | Name (Z bis A) 7 | Geändert (älteste zuerst) 8 | Geändert (neueste zuerst) 9 | Größe (kleinste zuerst) 10 | Größe (größte zuerst) 11 | 12 | 13 | 14 | 15 | 16 | Liste 17 | Raster 18 | 19 | 20 | Jede Woche 21 | Jeden Tag 22 | Jede Stunde 23 | Alle 15 Minuten 24 | 25 | 26 | Ungemessenes 27 | Nicht bei Roaming 28 | Verbunden (sobald wir den Server erreichen können, unabhängig vom Netzwerktyp) 29 | 30 | 31 | Alle anzeigen (kein Filter) 32 | Neu 33 | In Bearbeitung 34 | Abgebrochen 35 | Erledigt 36 | Fehler 37 | Timeout 38 | 39 | 40 | Standard (neueste zuerst) 41 | Erstellung (älteste zuerst) 42 | Status (A bis Z) 43 | Status (Z bis A) 44 | Typ (zuerst herunterladen) 45 | Typ (zuerst hochladen) 46 | 47 | 48 | Standard (neueste zuerst) 49 | Erstellung (älteste zuerst) 50 | Status (A bis Z) 51 | Status (Z bis A) 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/res/values-it/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Standard (A to Z, folders first) 5 | File name (A to Z) 6 | File name (Z to A) 7 | Modified (oldest first) 8 | Modified (newest first) 9 | Size (smallest first) 10 | Size (largest first) 11 | 12 | 13 | 14 | 15 | 16 | List 17 | Grid 18 | 19 | 20 | Every week 21 | Every day 22 | Every hour 23 | Every 15 minutes 24 | 25 | 26 | Unmetered 27 | Not Roaming 28 | Connected (run as soon as we can reach the server, no matter the network type) 29 | 30 | 31 | Show all (no filter) 32 | New 33 | Processing 34 | Cancelled 35 | Done 36 | Error 37 | Timeout 38 | 39 | 40 | Standard (By creation order, newest first) 41 | Creation order (oldest first) 42 | Status (A to Z) 43 | Status (Z to A) 44 | Type (Download First) 45 | Type (Upload First) 46 | 47 | 48 | Standard (By creation order, newest first) 49 | Creation order (oldest first) 50 | Status (A to Z) 51 | Status (Z to A) 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/res/values-ja/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Standard (A to Z, folders first) 5 | File name (A to Z) 6 | File name (Z to A) 7 | Modified (oldest first) 8 | Modified (newest first) 9 | Size (smallest first) 10 | Size (largest first) 11 | 12 | 13 | 14 | 15 | 16 | List 17 | Grid 18 | 19 | 20 | Every week 21 | Every day 22 | Every hour 23 | Every 15 minutes 24 | 25 | 26 | Unmetered 27 | Not Roaming 28 | Connected (run as soon as we can reach the server, no matter the network type) 29 | 30 | 31 | Show all (no filter) 32 | New 33 | Processing 34 | Cancelled 35 | Done 36 | Error 37 | Timeout 38 | 39 | 40 | Standard (By creation order, newest first) 41 | Creation order (oldest first) 42 | Status (A to Z) 43 | Status (Z to A) 44 | Type (Download First) 45 | Type (Upload First) 46 | 47 | 48 | Standard (By creation order, newest first) 49 | Creation order (oldest first) 50 | Status (A to Z) 51 | Status (Z to A) 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/res/values-pt-rBR/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Padrão (A a Z, pastas primeiro) 5 | Nome do arquivo (A a Z) 6 | Nome do arquivo (Z a A) 7 | Modificado (mais antigo primeiro) 8 | Modificado (mais recente primeiro) 9 | Tamanho (menor primeiro) 10 | Tamanho (maior primeiro) 11 | 12 | 13 | 14 | 15 | 16 | Lista 17 | Grade 18 | 19 | 20 | Semanalmente 21 | Diariamente 22 | A cada hora 23 | A cada 15 minutos 24 | 25 | 26 | Não medida 27 | Não está em roaming 28 | Conectado (executado o mais rápido possível no servidor, independentemente do tipo de rede) 29 | 30 | 31 | Mostrar tudo (sem filtro) 32 | Nova 33 | Processando 34 | Cancelada 35 | Finalizada 36 | Com erro 37 | Tempo esgotado 38 | 39 | 40 | Padrão (por ordem de criação, mais novo primeiro) 41 | Data de criação (mais antigo primeiro) 42 | Status (A a Z) 43 | Status (Z a A) 44 | Tipo (Download primeiro) 45 | Tipo (Upload primeiro) 46 | 47 | 48 | Padrão (por ordem de criação, mais novo primeiro) 49 | Data de criação (mais antigo primeiro) 50 | Status (A a Z) 51 | Status (Z a A) 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/res/values-ru/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Standard (A to Z, folders first) 5 | File name (A to Z) 6 | File name (Z to A) 7 | Modified (oldest first) 8 | Modified (newest first) 9 | Size (smallest first) 10 | Size (largest first) 11 | 12 | 13 | 14 | 15 | 16 | List 17 | Grid 18 | 19 | 20 | Каждую неделю 21 | Каждый день 22 | Каждый час 23 | Каждые 15 минут 24 | 25 | 26 | Безлимит 27 | Не в роуминге 28 | Подключено (запустить, как только мы сможем получить доступ к серверу, независимо от типа сети) 29 | 30 | 31 | Show all (no filter) 32 | New 33 | Processing 34 | Cancelled 35 | Done 36 | Error 37 | Timeout 38 | 39 | 40 | Standard (By creation order, newest first) 41 | Creation order (oldest first) 42 | Status (A to Z) 43 | Status (Z to A) 44 | Type (Download First) 45 | Type (Upload First) 46 | 47 | 48 | Standard (By creation order, newest first) 49 | Creation order (oldest first) 50 | Status (A to Z) 51 | Status (Z to A) 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #444C66 10 | #EE776C 11 | 12 | #FFE500 13 | #FF9800 14 | #FF5722 15 | #F44336 16 | #4CAF50 17 | #2196F3 18 | #3F51B5 19 | #5C5F61 20 | 21 | #000000 22 | #FFFFFF 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/res/values/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 150 4 | 300 5 | -------------------------------------------------------------------------------- /src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/res/values/types.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/res/values/values.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | cells 4 | Pydio 5 | 6 | 7 | AndroidClient 8 | 9 | cells-mobile 10 | cells 11 | 12 | https://pydio.com 13 | © Abstrium SAS 2023 14 | App instance details: \n\n\tPydio Version #%1$s: %2$s \n\tInstalled on %3$s \n\tAndroid OS: %4$s \n 15 | android@pydio.com 16 | Pydio In-App Support Request 17 | -------------------------------------------------------------------------------- /src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/test/java/com/pydio/android/cells/dummy/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.dummy; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /src/test/java/com/pydio/android/cells/dummy/HelloAppTest.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.dummy 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Rule 5 | import org.junit.Test 6 | import org.koin.test.KoinTest 7 | import org.koin.test.KoinTestRule 8 | import org.koin.test.inject 9 | 10 | class HelloAppTest : KoinTest { 11 | 12 | val model by inject() 13 | val service by inject() 14 | 15 | @get:Rule 16 | val koinTestRule = KoinTestRule.create { 17 | printLogger() 18 | modules(helloModule) 19 | } 20 | 21 | @Test 22 | fun `unit test`() { 23 | val helloApp = HelloApplication() 24 | helloApp.sayHello() 25 | 26 | assertEquals(service, helloApp.helloService) 27 | assertEquals("Hey, ${model.message}", service.hello()) 28 | } 29 | } -------------------------------------------------------------------------------- /src/test/java/com/pydio/android/cells/dummy/HelloApplication.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.dummy 2 | 3 | import org.koin.core.component.KoinComponent 4 | import org.koin.core.component.inject 5 | import org.koin.core.context.startKoin 6 | 7 | /** 8 | * HelloApplication - Application Class 9 | * use HelloService 10 | */ 11 | class HelloApplication : KoinComponent { 12 | 13 | // Inject HelloService 14 | val helloService: HelloService by inject() 15 | 16 | // display our data 17 | fun sayHello() = println(helloService.hello()) 18 | } 19 | 20 | /** 21 | * run app from here 22 | */ 23 | fun main() { 24 | startKoin { 25 | printLogger() 26 | modules(helloModule) 27 | } 28 | HelloApplication().sayHello() 29 | } -------------------------------------------------------------------------------- /src/test/java/com/pydio/android/cells/dummy/HelloMessageData.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.dummy 2 | 3 | /** 4 | * A class to hold our message data 5 | */ 6 | data class HelloMessageData(val message : String = "Hello Koin!") -------------------------------------------------------------------------------- /src/test/java/com/pydio/android/cells/dummy/HelloModule.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.dummy 2 | 3 | import org.koin.dsl.module 4 | 5 | // Koin module 6 | val helloModule = module { 7 | single { HelloMessageData() } 8 | single { HelloServiceImpl(get()) } 9 | } 10 | 11 | /* 12 | val helloModule = module { 13 | single() 14 | single() bind HelloService::class 15 | } 16 | */ -------------------------------------------------------------------------------- /src/test/java/com/pydio/android/cells/dummy/HelloService.kt: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.dummy 2 | 3 | /** 4 | * Hello Service - interface 5 | */ 6 | interface HelloService { 7 | fun hello(): String 8 | } 9 | 10 | 11 | // service class with injected helloModel instance 12 | /** 13 | * Hello Service Impl 14 | * Will use HelloMessageData data 15 | */ 16 | class HelloServiceImpl(private val helloMessageData: HelloMessageData) : HelloService { 17 | 18 | override fun hello() = "Hey, ${helloMessageData.message}" 19 | } -------------------------------------------------------------------------------- /src/test/java/com/pydio/android/cells/integration/TestClient.java: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.integration; 2 | 3 | import com.pydio.cells.api.Server; 4 | import com.pydio.cells.client.CellsClient; 5 | import com.pydio.cells.utils.Log; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.InputStreamReader; 10 | import java.net.URL; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.Properties; 14 | 15 | /** 16 | * Utility class to centralise setup of a Cells client for testing purposes. 17 | */ 18 | public class TestClient { 19 | 20 | private final static String logTag = TestClient.class.getSimpleName(); 21 | 22 | private Server server; 23 | private CellsClient cellsClient; 24 | 25 | private Path workingDirPath; 26 | 27 | private String serverURL, login, pwd, workspace, skipVerify; 28 | 29 | public void setup() { 30 | 31 | URL url = TestClient.class.getResource("/accounts/default.properties"); 32 | workingDirPath = Paths.get(url.getPath()).getParent(); 33 | 34 | Properties p = new Properties(); 35 | try (InputStream is = TestClient.class.getResourceAsStream("/accounts/default.properties")) { 36 | p.load(new InputStreamReader(is)); 37 | serverURL = p.getProperty("serverURL"); 38 | login = p.getProperty("login"); 39 | pwd = p.getProperty("pwd"); 40 | workspace = p.getProperty("defaultWorkspace"); 41 | skipVerify = p.getProperty("skipVerify", "false"); 42 | } catch (IOException e) { 43 | Log.e(logTag, "could not retrieve configuration file, cause: " + e.getMessage()); 44 | } 45 | } 46 | 47 | public CellsClient getCellsClient() { 48 | return cellsClient; 49 | } 50 | 51 | public String getDefaultWorkspace() { 52 | return workspace; 53 | } 54 | 55 | public Path getWorkingDir() { 56 | return workingDirPath; 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/test/java/com/pydio/android/cells/test/DebugNodeHandler.java: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.test; 2 | 3 | import com.pydio.cells.api.callbacks.NodeHandler; 4 | import com.pydio.cells.api.ui.Node; 5 | import com.pydio.cells.utils.Log; 6 | 7 | public class DebugNodeHandler implements NodeHandler { 8 | 9 | private final String caller; 10 | 11 | public DebugNodeHandler(String caller) { 12 | this.caller = caller; 13 | } 14 | 15 | private int i = 0; 16 | 17 | @Override 18 | public void onNode(Node node) { 19 | Log.i(caller, "#" + (++i) + " " + node.getName()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/java/com/pydio/android/cells/test/LocalTestUtils.java: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.test; 2 | 3 | import com.pydio.cells.utils.tests.TestUtils; 4 | 5 | public class LocalTestUtils { 6 | 7 | public static String getRunID() { 8 | return TestUtils.randomString(4); 9 | } 10 | 11 | } 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/test/java/com/pydio/android/cells/test/TestClientFactory.java: -------------------------------------------------------------------------------- 1 | package com.pydio.android.cells.test; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.pydio.android.cells.transfer.CellsS3Client; 6 | import com.pydio.cells.api.CustomEncoder; 7 | import com.pydio.cells.api.Server; 8 | import com.pydio.cells.api.Store; 9 | import com.pydio.cells.api.Transport; 10 | import com.pydio.cells.client.CellsClient; 11 | import com.pydio.cells.client.ClientFactory; 12 | import com.pydio.cells.transport.CellsTransport; 13 | import com.pydio.cells.transport.auth.CredentialService; 14 | import com.pydio.cells.utils.JavaCustomEncoder; 15 | import com.pydio.cells.utils.MemoryStore; 16 | import com.pydio.cells.utils.tests.TestCredentialService; 17 | 18 | public class TestClientFactory extends ClientFactory { 19 | 20 | public TestClientFactory() { 21 | this(new TestCredentialService(new MemoryStore<>(), new MemoryStore<>()), 22 | new MemoryStore<>(), new MemoryStore<>()); 23 | } 24 | 25 | public TestClientFactory(CredentialService credentialService, Store serverStore, Store transportStore) { 26 | super(credentialService, serverStore, transportStore); 27 | } 28 | 29 | @Override 30 | protected CellsClient getCellsClient(@NonNull CellsTransport transport) { 31 | return new CellsClient(transport, new CellsS3Client(transport)); 32 | } 33 | 34 | @Override 35 | public CustomEncoder getEncoder() { 36 | return new JavaCustomEncoder(); 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/test/resources/accounts/cells-http.properties: -------------------------------------------------------------------------------- 1 | # Adapt and set the below flag to false to test with such a server 2 | skipServer=true 3 | serverURL=http://localhost:8080 4 | username=admin 5 | pwd=admin 6 | defaultWorkspace=common-files 7 | -------------------------------------------------------------------------------- /src/test/resources/accounts/cells-https.properties: -------------------------------------------------------------------------------- 1 | # Adapt and set the below flag to false to test with such a server 2 | skipServer=true 3 | serverURL=https://cells.example.com 4 | username=admin 5 | pwd=admin 6 | defaultWorkspace=common-files 7 | -------------------------------------------------------------------------------- /src/test/resources/accounts/cells-selfsigned.properties: -------------------------------------------------------------------------------- 1 | # Adapt and set the below flag to false to test with such a server 2 | skipServer=true 3 | serverURL=https://localhost:8080 4 | username=admin 5 | pwd=admin 6 | defaultWorkspace=common-files 7 | skipVerify=true 8 | -------------------------------------------------------------------------------- /src/test/resources/accounts/default.properties: -------------------------------------------------------------------------------- 1 | # Server under test => Adapt to your own setup. 2 | skipServer=true 3 | serverURL=https://localhost:8080 4 | username=admin 5 | pwd=admin 6 | defaultWorkspace=common-files 7 | skipVerify=true -------------------------------------------------------------------------------- /src/test/resources/accounts/p8.properties: -------------------------------------------------------------------------------- 1 | # Adapt and set the below flag to false to test with such a server 2 | skipServer=true 3 | serverURL=https://p8.example.com 4 | username=admin 5 | pwd=admin 6 | defaultWorkspace=default 7 | -------------------------------------------------------------------------------- /src/test/resources/images/Logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/test/resources/images/Logo1.png -------------------------------------------------------------------------------- /src/test/resources/images/Logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/test/resources/images/Logo2.png -------------------------------------------------------------------------------- /src/test/resources/images/Logo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydio/cells-android-client/e37ba07af264cba3f5c8ea6d98f1297cfcd04d6f/src/test/resources/images/Logo3.png --------------------------------------------------------------------------------