diff --git a/changelog/unreleased/3293 b/changelog/unreleased/3293 new file mode 100644 index 00000000000..69c4f2c9fa0 --- /dev/null +++ b/changelog/unreleased/3293 @@ -0,0 +1,7 @@ +Enhancement: Replace picker to select camera folder with native one + +The custom picker to select the camera folder was replaced with the native one. Now, it is ready for +scoped storage and some problems to select a folder in the SD Card were fixed. Also, a new field to show +the last synchronization timestamp was added. + +https://github.com/owncloud/android/issues/2899 https://github.com/owncloud/android/pull/3293 \ No newline at end of file diff --git a/owncloud-android-library b/owncloud-android-library index b491641eff8..22719c8f40f 160000 --- a/owncloud-android-library +++ b/owncloud-android-library @@ -1 +1 @@ -Subproject commit b491641eff8da4c1cf4971c133bf1e002d57d8dd +Subproject commit 22719c8f40fd804a104d92819b2c304d58e9c085 diff --git a/owncloudApp/build.gradle b/owncloudApp/build.gradle index dbf26a705bf..3c09d09bbcd 100644 --- a/owncloudApp/build.gradle +++ b/owncloudApp/build.gradle @@ -36,9 +36,11 @@ dependencies { implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" kapt "androidx.lifecycle:lifecycle-common-java8:$archLifecycleVersion" kapt "org.xerial:sqlite-jdbc:3.34.0" // fix kapt for Apple Silicon https://stackoverflow.com/a/68285501/1079990 @@ -48,13 +50,16 @@ dependencies { implementation "org.koin:koin-core:$koinVersion" implementation "org.koin:koin-androidx-viewmodel:$koinVersion" + // WorkManager + implementation "androidx.work:work-runtime-ktx:2.5.0" + // KTX extensions, see https://developer.android.com/kotlin/ktx.html implementation "androidx.core:core-ktx:$ktxCoreVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$ktxViewModelVersion" implementation "androidx.fragment:fragment-ktx:$ktxFragmentVersion" // Preferences - implementation 'androidx.preference:preference:1.1.1' + implementation 'androidx.preference:preference-ktx:1.1.1' // Tests testImplementation project(':owncloudTestUtil') diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/camerauploads/SettingsPictureUploadsFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/camerauploads/SettingsPictureUploadsFragmentTest.kt deleted file mode 100644 index b0b4045b2c0..00000000000 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/camerauploads/SettingsPictureUploadsFragmentTest.kt +++ /dev/null @@ -1,251 +0,0 @@ -/** - * ownCloud Android client application - * - * @author Juan Carlos Garrote Gascón - * - * Copyright (C) 2021 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.settings.camerauploads - -import android.content.Context -import android.os.Build -import android.os.Environment -import androidx.fragment.app.testing.FragmentScenario -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.preference.CheckBoxPreference -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.SwitchPreferenceCompat -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.intent.Intents -import androidx.test.espresso.intent.Intents.intended -import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent -import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.platform.app.InstrumentationRegistry -import com.owncloud.android.R -import com.owncloud.android.db.PreferenceManager -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ENABLED -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_PATH -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_SOURCE -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY -import com.owncloud.android.presentation.ui.settings.fragments.SettingsPictureUploadsFragment -import com.owncloud.android.presentation.viewmodels.settings.SettingsPictureUploadsViewModel -import com.owncloud.android.ui.activity.LocalFolderPickerActivity -import com.owncloud.android.utils.matchers.verifyPreference -import io.mockk.every -import io.mockk.mockk -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module - -class SettingsPictureUploadsFragmentTest { - - private lateinit var fragmentScenario: FragmentScenario - - private lateinit var prefEnablePictureUploads: SwitchPreferenceCompat - private lateinit var prefPictureUploadsPath: Preference - private lateinit var prefPictureUploadsOnWifi: CheckBoxPreference - private lateinit var prefPictureUploadsSourcePath: Preference - private lateinit var prefPictureUploadsBehaviour: ListPreference - private lateinit var prefPictureUploadsAccount: ListPreference - - private lateinit var picturesViewModel: SettingsPictureUploadsViewModel - private lateinit var context: Context - - private val exampleUploadPath = "/Upload/Path" - private val exampleUploadSourcePath = "/Upload/Source/Path" - private val listOfLoggedAccounts = arrayOf("first", "second", "third") - - @Before - fun setUp() { - context = InstrumentationRegistry.getInstrumentation().targetContext - picturesViewModel = mockk(relaxUnitFun = true) - - every { picturesViewModel.getPictureUploadsPath() } returns exampleUploadPath - every { picturesViewModel.getPictureUploadsSourcePath() } returns exampleUploadSourcePath - every { picturesViewModel.isPictureUploadEnabled() } returns false - every { picturesViewModel.getLoggedAccountNames() } returns listOfLoggedAccounts - every { picturesViewModel.getPictureUploadsAccount() } returns listOfLoggedAccounts.first() - - stopKoin() - - startKoin { - context - modules( - module(override = true) { - viewModel { - picturesViewModel - } - } - ) - } - - fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_ownCloud) - fragmentScenario.onFragment { fragment -> - prefEnablePictureUploads = fragment.findPreference(PREF__CAMERA_PICTURE_UPLOADS_ENABLED)!! - prefPictureUploadsPath = fragment.findPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH)!! - prefPictureUploadsOnWifi = fragment.findPreference(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY)!! - prefPictureUploadsSourcePath = fragment.findPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE)!! - prefPictureUploadsBehaviour = fragment.findPreference(PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR)!! - prefPictureUploadsAccount = fragment.findPreference(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME)!! - } - } - - @After - fun tearDown() { - androidx.preference.PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit() - } - - @Test - fun pictureUploadsView() { - prefEnablePictureUploads.verifyPreference( - keyPref = PREF__CAMERA_PICTURE_UPLOADS_ENABLED, - titlePref = context.getString(R.string.prefs_camera_picture_upload), - summaryPref = context.getString(R.string.prefs_camera_picture_upload_summary), - visible = true, - enabled = true - ) - assertFalse(prefEnablePictureUploads.isChecked) - - prefPictureUploadsPath.verifyPreference( - keyPref = PREF__CAMERA_PICTURE_UPLOADS_PATH, - titlePref = context.getString(R.string.prefs_camera_picture_upload_path_title), - summaryPref = exampleUploadPath, - visible = true, - enabled = false - ) - - prefPictureUploadsOnWifi.verifyPreference( - keyPref = PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY, - titlePref = context.getString(R.string.prefs_camera_picture_upload_on_wifi), - visible = true, - enabled = false - ) - assertFalse(prefPictureUploadsOnWifi.isChecked) - - val comment = - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) context.getString( - R.string.prefs_camera_upload_source_path_title_optional - ) - else context.getString( - R.string.prefs_camera_upload_source_path_title_required - ) - prefPictureUploadsSourcePath.verifyPreference( - keyPref = PREF__CAMERA_PICTURE_UPLOADS_SOURCE, - titlePref = String.format(prefPictureUploadsSourcePath.title.toString(), comment), - summaryPref = exampleUploadSourcePath, - visible = true, - enabled = false - ) - - prefPictureUploadsBehaviour.verifyPreference( - keyPref = PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR, - titlePref = context.getString(R.string.prefs_camera_upload_behaviour_title), - summaryPref = context.getString(R.string.pref_behaviour_entries_keep_file), - visible = true, - enabled = false - ) - - prefPictureUploadsAccount.verifyPreference( - keyPref = PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME, - titlePref = context.getString(R.string.prefs_picture_upload_account), - summaryPref = prefPictureUploadsAccount.context.getString(androidx.preference.R.string.not_set), - visible = true, - enabled = false - ) - } - - @Test - fun enablePictureUploads() { - firstEnablePictureUploads() - checkPreferencesEnabled(true) - checkPreferencesInitialized() - } - - @Test - fun disablePictureUploadsAccept() { - firstEnablePictureUploads() - onView(withText(R.string.prefs_camera_picture_upload)).perform(click()) - onView(withText(R.string.common_yes)).perform(click()) - checkPreferencesEnabled(false) - checkPreferencesReset() - } - - @Test - fun disablePictureUploadsRefuse() { - firstEnablePictureUploads() - onView(withText(R.string.prefs_camera_picture_upload)).perform(click()) - onView(withText(R.string.common_no)).perform(click()) - checkPreferencesEnabled(true) - } - - @Ignore("Makes the subsequent tests crash. Will have to be updated when changed to Android's file picker") - @Test - fun openPictureUploadSourcePathPicker() { - firstEnablePictureUploads() - val cameraFolder = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_DCIM - ).absolutePath + "/Camera" - Intents.init() - onView( - withText( - String.format( - context.getString(R.string.prefs_camera_upload_source_path_title), - context.getString(R.string.prefs_camera_upload_source_path_title_required) - ) - ) - ).perform(click()) - intended(hasComponent(LocalFolderPickerActivity::class.java.name)) - hasExtra(LocalFolderPickerActivity.EXTRA_PATH, cameraFolder) - Intents.release() - onView(withText(android.R.string.cancel)).perform(click()) - } - - private fun firstEnablePictureUploads() { - onView(withText(R.string.prefs_camera_picture_upload)).perform(click()) - onView(withText(android.R.string.ok)).perform(click()) - } - - private fun checkPreferencesEnabled(enabled: Boolean) { - assertEquals(enabled, prefEnablePictureUploads.isChecked) - assertEquals(enabled, prefPictureUploadsPath.isEnabled) - assertEquals(enabled, prefPictureUploadsOnWifi.isEnabled) - assertEquals(enabled, prefPictureUploadsSourcePath.isEnabled) - assertEquals(enabled, prefPictureUploadsBehaviour.isEnabled) - assertEquals(enabled, prefPictureUploadsAccount.isEnabled) - } - - private fun checkPreferencesInitialized() { - assertEquals(listOfLoggedAccounts.first(), prefPictureUploadsAccount.summary) - assertEquals(exampleUploadPath, prefPictureUploadsPath.summary) - } - - private fun checkPreferencesReset() { - assertEquals(context.getString(androidx.preference.R.string.not_set), prefPictureUploadsAccount.summary) - assertEquals(picturesViewModel.getPictureUploadsPath(), prefPictureUploadsPath.summary) - } -} diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/camerauploads/SettingsVideoUploadsFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/camerauploads/SettingsVideoUploadsFragmentTest.kt deleted file mode 100644 index 74e787e46b0..00000000000 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/camerauploads/SettingsVideoUploadsFragmentTest.kt +++ /dev/null @@ -1,250 +0,0 @@ -/** - * ownCloud Android client application - * - * @author Juan Carlos Garrote Gascón - * - * Copyright (C) 2021 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.settings.camerauploads - -import android.content.Context -import android.os.Build -import android.os.Environment -import androidx.fragment.app.testing.FragmentScenario -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.preference.CheckBoxPreference -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.SwitchPreferenceCompat -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.intent.Intents -import androidx.test.espresso.intent.Intents.intended -import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent -import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.platform.app.InstrumentationRegistry -import com.owncloud.android.R -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ENABLED -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_PATH -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_SOURCE -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY -import com.owncloud.android.presentation.ui.settings.fragments.SettingsVideoUploadsFragment -import com.owncloud.android.presentation.viewmodels.settings.SettingsVideoUploadsViewModel -import com.owncloud.android.ui.activity.LocalFolderPickerActivity -import com.owncloud.android.utils.matchers.verifyPreference -import io.mockk.every -import io.mockk.mockk -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module - -class SettingsVideoUploadsFragmentTest { - - private lateinit var fragmentScenario: FragmentScenario - - private lateinit var prefEnableVideoUploads: SwitchPreferenceCompat - private lateinit var prefVideoUploadsPath: Preference - private lateinit var prefVideoUploadsOnWifi: CheckBoxPreference - private lateinit var prefVideoUploadsSourcePath: Preference - private lateinit var prefVideoUploadsBehaviour: ListPreference - private lateinit var prefVideoUploadsAccount: ListPreference - - private lateinit var videosViewModel: SettingsVideoUploadsViewModel - private lateinit var context: Context - - private val exampleUploadPath = "/Upload/Path" - private val exampleUploadSourcePath = "/Upload/Source/Path" - private val listOfLoggedAccounts = arrayOf("first", "second", "third") - - @Before - fun setUp() { - context = InstrumentationRegistry.getInstrumentation().targetContext - videosViewModel = mockk(relaxUnitFun = true) - - every { videosViewModel.getVideoUploadsPath() } returns exampleUploadPath - every { videosViewModel.getVideoUploadsSourcePath() } returns exampleUploadSourcePath - every { videosViewModel.isVideoUploadEnabled() } returns false - every { videosViewModel.getLoggedAccountNames() } returns listOfLoggedAccounts - every { videosViewModel.getVideoUploadsAccount() } returns listOfLoggedAccounts.first() - - stopKoin() - - startKoin { - context - modules( - module(override = true) { - viewModel { - videosViewModel - } - } - ) - } - - fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_ownCloud) - fragmentScenario.onFragment { fragment -> - prefEnableVideoUploads = fragment.findPreference(PREF__CAMERA_VIDEO_UPLOADS_ENABLED)!! - prefVideoUploadsPath = fragment.findPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH)!! - prefVideoUploadsOnWifi = fragment.findPreference(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY)!! - prefVideoUploadsSourcePath = fragment.findPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE)!! - prefVideoUploadsBehaviour = fragment.findPreference(PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR)!! - prefVideoUploadsAccount = fragment.findPreference(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME)!! - } - } - - @After - fun tearDown() { - androidx.preference.PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit() - } - - @Test - fun videoUploadsView() { - prefEnableVideoUploads.verifyPreference( - keyPref = PREF__CAMERA_VIDEO_UPLOADS_ENABLED, - titlePref = context.getString(R.string.prefs_camera_video_upload), - summaryPref = context.getString(R.string.prefs_camera_video_upload_summary), - visible = true, - enabled = true - ) - assertFalse(prefEnableVideoUploads.isChecked) - - prefVideoUploadsPath.verifyPreference( - keyPref = PREF__CAMERA_VIDEO_UPLOADS_PATH, - titlePref = context.getString(R.string.prefs_camera_video_upload_path_title), - summaryPref = exampleUploadPath, - visible = true, - enabled = false - ) - - prefVideoUploadsOnWifi.verifyPreference( - keyPref = PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY, - titlePref = context.getString(R.string.prefs_camera_video_upload_on_wifi), - visible = true, - enabled = false - ) - assertFalse(prefVideoUploadsOnWifi.isChecked) - - val comment = - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) context.getString( - R.string.prefs_camera_upload_source_path_title_optional - ) - else context.getString( - R.string.prefs_camera_upload_source_path_title_required - ) - prefVideoUploadsSourcePath.verifyPreference( - keyPref = PREF__CAMERA_VIDEO_UPLOADS_SOURCE, - titlePref = String.format(prefVideoUploadsSourcePath.title.toString(), comment), - summaryPref = exampleUploadSourcePath, - visible = true, - enabled = false - ) - - prefVideoUploadsBehaviour.verifyPreference( - keyPref = PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR, - titlePref = context.getString(R.string.prefs_camera_upload_behaviour_title), - summaryPref = context.getString(R.string.pref_behaviour_entries_keep_file), - visible = true, - enabled = false - ) - - prefVideoUploadsAccount.verifyPreference( - keyPref = PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME, - titlePref = context.getString(R.string.prefs_video_upload_account), - summaryPref = prefVideoUploadsAccount.context.getString(androidx.preference.R.string.not_set), - visible = true, - enabled = false - ) - } - - @Test - fun enableVideoUploads() { - firstEnableVideoUploads() - checkPreferencesEnabled(true) - checkPreferencesInitialized() - } - - @Test - fun disableVideoUploadsAccept() { - firstEnableVideoUploads() - onView(withText(R.string.prefs_camera_video_upload)).perform(click()) - onView(withText(R.string.common_yes)).perform(click()) - checkPreferencesEnabled(false) - checkPreferencesReset() - } - - @Test - fun disableVideoUploadsRefuse() { - firstEnableVideoUploads() - onView(withText(R.string.prefs_camera_video_upload)).perform(click()) - onView(withText(R.string.common_no)).perform(click()) - checkPreferencesEnabled(true) - } - - @Ignore("Makes the subsequent tests crash. Will have to be updated when changed to Android's file picker") - @Test - fun openVideoUploadSourcePathPicker() { - firstEnableVideoUploads() - val cameraFolder = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_DCIM - ).absolutePath + "/Camera" - Intents.init() - onView( - withText( - String.format( - context.getString(R.string.prefs_camera_upload_source_path_title), - context.getString(R.string.prefs_camera_upload_source_path_title_required) - ) - ) - ).perform(click()) - intended(hasComponent(LocalFolderPickerActivity::class.java.name)) - hasExtra(LocalFolderPickerActivity.EXTRA_PATH, cameraFolder) - Intents.release() - onView(withText(android.R.string.cancel)).perform(click()) - } - - private fun firstEnableVideoUploads() { - onView(withText(R.string.prefs_camera_video_upload)).perform(click()) - onView(withText(android.R.string.ok)).perform(click()) - } - - private fun checkPreferencesEnabled(enabled: Boolean) { - assertEquals(enabled, prefEnableVideoUploads.isChecked) - assertEquals(enabled, prefVideoUploadsPath.isEnabled) - assertEquals(enabled, prefVideoUploadsOnWifi.isEnabled) - assertEquals(enabled, prefVideoUploadsSourcePath.isEnabled) - assertEquals(enabled, prefVideoUploadsBehaviour.isEnabled) - assertEquals(enabled, prefVideoUploadsAccount.isEnabled) - } - - private fun checkPreferencesInitialized() { - assertEquals(listOfLoggedAccounts.first(), prefVideoUploadsAccount.summary) - assertEquals(exampleUploadPath, prefVideoUploadsPath.summary) - } - - private fun checkPreferencesReset() { - assertEquals(context.getString(androidx.preference.R.string.not_set), prefVideoUploadsAccount.summary) - assertEquals(videosViewModel.getVideoUploadsPath(), prefVideoUploadsPath.summary) - } -} diff --git a/owncloudApp/src/main/AndroidManifest.xml b/owncloudApp/src/main/AndroidManifest.xml index 5ef13af44b5..798a25c43dd 100644 --- a/owncloudApp/src/main/AndroidManifest.xml +++ b/owncloudApp/src/main/AndroidManifest.xml @@ -143,10 +143,6 @@ android:name=".files.services.RetryDownloadJobService" android:exported="true" android:permission="android.permission.BIND_JOB_SERVICE" /> - @@ -213,24 +209,11 @@ android:name=".ui.activity.WhatsNewActivity" android:configChanges="keyboardHidden|orientation|screenSize" /> - - - - - - - - - - diff --git a/owncloudApp/src/main/java/com/owncloud/android/broadcastreceivers/ConnectivityActionReceiver.java b/owncloudApp/src/main/java/com/owncloud/android/broadcastreceivers/ConnectivityActionReceiver.java deleted file mode 100755 index 3255a866442..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/broadcastreceivers/ConnectivityActionReceiver.java +++ /dev/null @@ -1,182 +0,0 @@ -/** - * ownCloud Android client application - * - * @author LukeOwncloud - * @author Christian Schabesberger - * @author David González Verdugo - * Copyright (C) 2020 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.broadcastreceivers; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.net.NetworkInfo; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; - -import com.owncloud.android.MainApp; -import com.owncloud.android.db.PreferenceManager; -import com.owncloud.android.db.UploadResult; -import com.owncloud.android.files.services.FileUploader; -import com.owncloud.android.files.services.TransferRequester; -import timber.log.Timber; - -/** - * Receives all connectivity action from Android OS at all times and performs - * required OC actions. For now that are: - Signal connectivity to - * {@link FileUploader}. - * - * Later can be added: - Signal connectivity to download service, deletion - * service, ... - Handle offline mode (cf. - * https://github.com/owncloud/android/issues/162) - * - * Have fun with the comments :S - */ -public class ConnectivityActionReceiver extends BroadcastReceiver { - - /** - * Magic keyword, by Google. - * - * {@See http://developer.android.com/intl/es/reference/android/net/wifi/WifiInfo.html#getSSID()} - */ - private static final String UNKNOWN_SSID = ""; - - @Override - public void onReceive(final Context context, Intent intent) { - // LOG ALL EVENTS: - Timber.v("action: %s", intent.getAction()); - Timber.v("component: %s", intent.getComponent()); - Bundle extras = intent.getExtras(); - if (extras != null) { - for (String key : extras.keySet()) { - Timber.v("key [" + key + "]: " + extras.get(key)); - } - } else { - Timber.v("no extras"); - } - - /* - * There is an interesting mess to process WifiManager.NETWORK_STATE_CHANGED_ACTION and - * ConnectivityManager.CONNECTIVITY_ACTION in a simple and reliable way. - * - * The former triggers much more events than what we really need to know about Wifi connection. - * - * But there are annoying uncertainties about ConnectivityManager.CONNECTIVITY_ACTION due - * to the deprecation of ConnectivityManager.EXTRA_NETWORK_INFO in API level 14, and the absence - * of ConnectivityManager.EXTRA_NETWORK_TYPE until API level 17. Dear Google, how should we - * handle API levels 14 to 16? - * - * In the end maybe we need to keep in memory the current knowledge about connectivity - * and update it taking into account several Intents received in a row - * - * But first let's try something "simple" to keep a basic retry of camera uploads in - * version 1.9.2, similar to the existent until 1.9.1. To be improved. - */ - if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { - NetworkInfo networkInfo = - intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); - WifiInfo wifiInfo = - intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO); - String bssid = - intent.getStringExtra(WifiManager.EXTRA_BSSID); - if (networkInfo.isConnected() && // not enough; see (*) right below - wifiInfo != null && - !UNKNOWN_SSID.equals(wifiInfo.getSSID().toLowerCase()) && - bssid != null - ) { - Timber.d("WiFi connected"); - - wifiConnected(context); - } else { - // TODO tons of things to check to conclude disconnection; - // TODO maybe alternative commented below, based on CONNECTIVITY_ACTION is better - Timber.d("WiFi disconnected ... but don't know if right now"); - } - } - // (*) When WiFi is lost, an Intent with network state CONNECTED and SSID "" is - // received right before another Intent with network state DISCONNECTED; needs to - // be differentiated of a new Wifi connection. - // - // Besides, with a new connection two Intents are received, having only the second the extra - // WifiManager.EXTRA_BSSID, with the BSSID of the access point accessed. - // - // Not sure if this protocol is exact, since it's not documented. Only found mild references in - // - http://developer.android.com/intl/es/reference/android/net/wifi/WifiInfo.html#getSSID() - // - http://developer.android.com/intl/es/reference/android/net/wifi/WifiManager.html#EXTRA_BSSID - // and reproduced in Nexus 5 with Android 6. - - // TODO check if this helps us - /* - * Possible alternative attending ConnectivityManager.CONNECTIVITY_ACTION. - * - * Let's see what QA has to say - * - if(intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { - NetworkInfo networkInfo = intent.getParcelableExtra( - ConnectivityManager.EXTRA_NETWORK_INFO // deprecated in API 14 - ); - int networkType = intent.getIntExtra( - ConnectivityManager.EXTRA_NETWORK_TYPE, // only from API level 17 - -1 - ); - boolean couldBeWifiAction = - (networkInfo == null && networkType < 0) || // cases of lack of info - networkInfo.getType() == ConnectivityManager.TYPE_WIFI || - networkType == ConnectivityManager.TYPE_WIFI; - - if (couldBeWifiAction) { - if (ConnectivityUtils.isAppConnectedViaWiFi(context)) { - Timber.d("WiFi connected"); - wifiConnected(context); - } else { - Timber.d("WiFi disconnected"); - wifiDisconnected(context); - } - } /* else, CONNECTIVIY_ACTION is (probably) about other network interface (mobile, bluetooth, ...) - } - */ - } - - private void wifiConnected(Context context) { - // for the moment, only recovery of camera uploads, similar to behaviour in release 1.9.1 - if ( - (PreferenceManager.cameraPictureUploadEnabled(context) && - PreferenceManager.cameraPictureUploadViaWiFiOnly(context)) || - (PreferenceManager.cameraVideoUploadEnabled(context) && - PreferenceManager.cameraVideoUploadViaWiFiOnly(context)) - ) { - - Handler h = new Handler(Looper.getMainLooper()); - h.postDelayed(() -> { - Timber.d("Requesting retry of camera uploads (& friends)"); - TransferRequester requester = new TransferRequester(); - - requester.retryFailedUploads( - MainApp.Companion.getAppContext(), - null, - UploadResult.DELAYED_FOR_WIFI, // for the rest of enqueued when Wifi fell - true - ); - }, - 500 - ); - } - } -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/datamodel/CameraUploadsSyncStorageManager.java b/owncloudApp/src/main/java/com/owncloud/android/datamodel/CameraUploadsSyncStorageManager.java deleted file mode 100644 index f0adcc7c021..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/datamodel/CameraUploadsSyncStorageManager.java +++ /dev/null @@ -1,167 +0,0 @@ -/** - * ownCloud Android client application - * - * @author David González Verdugo - * Copyright (C) 2017 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.datamodel; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; - -import com.owncloud.android.db.ProviderMeta; -import timber.log.Timber; - -import java.util.Observable; - -public class CameraUploadsSyncStorageManager extends Observable { - - private ContentResolver mContentResolver; - - public CameraUploadsSyncStorageManager(ContentResolver contentResolver) { - if (contentResolver == null) { - throw new IllegalArgumentException("Cannot create an instance with a NULL contentResolver"); - } - mContentResolver = contentResolver; - } - - /** - * Stores a camera upload sync object in DB - * - * @param ocCameraUploadSync Camera upload sync object to store - * @return camera upload sync id, -1 if the insert process fails - */ - public long storeCameraUploadSync(OCCameraUploadSync ocCameraUploadSync) { - Timber.v("Inserting camera upload sync with timestamp of last pictures synchronization " - + ocCameraUploadSync.getPicturesLastSync() + " and timestamp of last videos " + - "synchronzization" + ocCameraUploadSync.getVideosLastSync()); - - ContentValues cv = new ContentValues(); - cv.put(ProviderMeta.ProviderTableMeta.PICTURES_LAST_SYNC_TIMESTAMP, ocCameraUploadSync. - getPicturesLastSync()); - cv.put(ProviderMeta.ProviderTableMeta.VIDEOS_LAST_SYNC_TIMESTAMP, ocCameraUploadSync. - getVideosLastSync()); - - Uri result = getDB().insert(ProviderMeta.ProviderTableMeta.CONTENT_URI_CAMERA_UPLOADS_SYNC, - cv); - - Timber.d("storeUpload returns with: " + result + " for camera upload sync " + ocCameraUploadSync.getId()); - if (result == null) { - Timber.e("Failed to insert camera upload sync " + ocCameraUploadSync.getId() + " into camera uploads sync db."); - return -1; - } else { - long new_id = Long.parseLong(result.getPathSegments().get(1)); - ocCameraUploadSync.setId(new_id); - notifyObserversNow(); - return new_id; - } - } - - /** - * Update a camera upload sync object in DB. - * - * @param ocCameraUploadSync Camera upload sync object with state to update - * @return num of updated camera upload sync - */ - public int updateCameraUploadSync(OCCameraUploadSync ocCameraUploadSync) { - Timber.v("Updating %s", ocCameraUploadSync.getId()); - - ContentValues cv = new ContentValues(); - cv.put(ProviderMeta.ProviderTableMeta.PICTURES_LAST_SYNC_TIMESTAMP, ocCameraUploadSync. - getPicturesLastSync()); - cv.put(ProviderMeta.ProviderTableMeta.VIDEOS_LAST_SYNC_TIMESTAMP, ocCameraUploadSync. - getVideosLastSync()); - - int result = getDB().update(ProviderMeta.ProviderTableMeta.CONTENT_URI_CAMERA_UPLOADS_SYNC, - cv, - ProviderMeta.ProviderTableMeta._ID + "=?", - new String[]{String.valueOf(ocCameraUploadSync.getId())} - ); - - Timber.d("updateCameraUploadSync returns with: " + result + " for camera upload sync: " + - ocCameraUploadSync.getId()); - if (result != 1) { - Timber.e("Failed to update item " + ocCameraUploadSync.getId() + " into " + - "camera upload sync db."); - } else { - notifyObserversNow(); - } - - return result; - } - - /** - * Retrieves a camera upload sync object from DB - * @param selection filter declaring which rows to return, formatted as an SQL WHERE clause - * @param selectionArgs include ?s in selection, which will be replaced by the values from here - * @param sortOrder How to order the rows, formatted as an SQL ORDER BY clause - * @return camera upload sync object - */ - public OCCameraUploadSync getCameraUploadSync(String selection, String[] selectionArgs, - String sortOrder) { - Cursor c = getDB().query( - ProviderMeta.ProviderTableMeta.CONTENT_URI_CAMERA_UPLOADS_SYNC, - null, - selection, - selectionArgs, - sortOrder - ); - - OCCameraUploadSync ocCameraUploadSync = null; - - if (c.moveToFirst()) { - ocCameraUploadSync = createOCCameraUploadSyncFromCursor(c); - if (ocCameraUploadSync == null) { - Timber.e("Camera upload sync could not be created from cursor"); - } - } - - c.close(); - - return ocCameraUploadSync; - } - - private OCCameraUploadSync createOCCameraUploadSyncFromCursor(Cursor c) { - OCCameraUploadSync cameraUploadSync = null; - if (c != null) { - long picturesLastSync = c.getLong(c.getColumnIndex(ProviderMeta.ProviderTableMeta. - PICTURES_LAST_SYNC_TIMESTAMP)); - long videosLastSync = c.getLong(c.getColumnIndex(ProviderMeta.ProviderTableMeta. - VIDEOS_LAST_SYNC_TIMESTAMP)); - - cameraUploadSync = new OCCameraUploadSync(picturesLastSync, videosLastSync); - - cameraUploadSync.setId(c.getLong(c.getColumnIndex(ProviderMeta.ProviderTableMeta._ID))); - } - return cameraUploadSync; - } - - private ContentResolver getDB() { - return mContentResolver; - } - - /** - * Should be called when some value of this DB was changed. All observers - * are informed. - */ - private void notifyObserversNow() { - Timber.d("notifyObserversNow"); - setChanged(); - notifyObservers(); - } -} \ No newline at end of file diff --git a/owncloudApp/src/main/java/com/owncloud/android/datamodel/OCUpload.java b/owncloudApp/src/main/java/com/owncloud/android/datamodel/OCUpload.java index 7b2bc4b1c17..de3da3690af 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/datamodel/OCUpload.java +++ b/owncloudApp/src/main/java/com/owncloud/android/datamodel/OCUpload.java @@ -110,7 +110,7 @@ public class OCUpload implements Parcelable { * @param accountName Name of an ownCloud account to update the file to. */ public OCUpload(String localPath, String remotePath, String accountName) { - if (localPath == null || !localPath.startsWith(File.separator)) { + if (localPath == null) { throw new IllegalArgumentException("Local path must be an absolute path in the local file system"); } if (remotePath == null || !remotePath.startsWith(File.separator)) { diff --git a/owncloudApp/src/main/java/com/owncloud/android/db/PreferenceManager.java b/owncloudApp/src/main/java/com/owncloud/android/db/PreferenceManager.java index 3818f938813..92ae58ee411 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/db/PreferenceManager.java +++ b/owncloudApp/src/main/java/com/owncloud/android/db/PreferenceManager.java @@ -23,32 +23,16 @@ package com.owncloud.android.db; -import android.accounts.Account; import android.content.Context; import android.content.SharedPreferences; -import com.owncloud.android.R; -import com.owncloud.android.authentication.AccountUtils; -import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.ui.activity.BiometricActivity; import com.owncloud.android.utils.FileStorageUtils; -import java.io.File; - /** * Helper to simplify reading of Preferences all around the app */ public abstract class PreferenceManager { - /** - * Constant to access value of last path selected by the user to upload a file shared from other app. - * Value handled by the app without direct access in the UI. - */ - private static final String AUTO_PREF__LAST_UPLOAD_PATH = "last_upload_path"; - private static final String AUTO_PREF__SORT_ORDER_FILE_DISP = "sortOrderFileDisp"; - private static final String AUTO_PREF__SORT_ASCENDING_FILE_DISP = "sortAscendingFileDisp"; - private static final String AUTO_PREF__SORT_ORDER_UPLOAD = "sortOrderUpload"; - private static final String AUTO_PREF__SORT_ASCENDING_UPLOAD = "sortAscendingUpload"; - // Legacy preferences - done in version 2.18 public static final String PREF__LEGACY_CLICK_DEV_MENU = "clickDeveloperMenu"; public static final String PREF__LEGACY_CAMERA_PICTURE_UPLOADS_ENABLED = "camera_picture_uploads"; @@ -60,7 +44,6 @@ public abstract class PreferenceManager { public static final String PREF__LEGACY_CAMERA_UPLOADS_BEHAVIOUR = "camera_uploads_behaviour"; public static final String PREF__LEGACY_CAMERA_UPLOADS_SOURCE = "camera_uploads_source_path"; public static final String PREF__LEGACY_CAMERA_UPLOADS_ACCOUNT_NAME = "camera_uploads_account_name"; - public static final String PREF__CAMERA_PICTURE_UPLOADS_ENABLED = "enable_picture_uploads"; public static final String PREF__CAMERA_VIDEO_UPLOADS_ENABLED = "enable_video_uploads"; public static final String PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY = "picture_uploads_on_wifi"; @@ -73,10 +56,19 @@ public abstract class PreferenceManager { public static final String PREF__CAMERA_VIDEO_UPLOADS_SOURCE = "video_uploads_source_path"; public static final String PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME = "picture_uploads_account_name"; public static final String PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME = "video_uploads_account_name"; - + public static final String PREF__CAMERA_PICTURE_UPLOADS_LAST_SYNC = "picture_uploads_last_sync"; + public static final String PREF__CAMERA_VIDEO_UPLOADS_LAST_SYNC = "video_uploads_last_sync"; public static final String PREF__CAMERA_UPLOADS_DEFAULT_PATH = "/CameraUpload"; - public static final String PREF__LEGACY_FINGERPRINT = "set_fingerprint"; + /** + * Constant to access value of last path selected by the user to upload a file shared from other app. + * Value handled by the app without direct access in the UI. + */ + private static final String AUTO_PREF__LAST_UPLOAD_PATH = "last_upload_path"; + private static final String AUTO_PREF__SORT_ORDER_FILE_DISP = "sortOrderFileDisp"; + private static final String AUTO_PREF__SORT_ASCENDING_FILE_DISP = "sortAscendingFileDisp"; + private static final String AUTO_PREF__SORT_ORDER_UPLOAD = "sortOrderUpload"; + private static final String AUTO_PREF__SORT_ASCENDING_UPLOAD = "sortAscendingUpload"; public static void migrateFingerprintToBiometricKey(Context context) { SharedPreferences sharedPref = getDefaultSharedPreferences(context); @@ -127,84 +119,6 @@ public static void deleteOldSettingsPreferences(Context context) { editor.apply(); } - public static boolean cameraPictureUploadEnabled(Context context) { - return getDefaultSharedPreferences(context).getBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false); - } - - public static boolean cameraVideoUploadEnabled(Context context) { - return getDefaultSharedPreferences(context).getBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false); - } - - public static boolean cameraPictureUploadViaWiFiOnly(Context context) { - return getDefaultSharedPreferences(context).getBoolean(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY, false); - } - - public static boolean cameraVideoUploadViaWiFiOnly(Context context) { - return getDefaultSharedPreferences(context).getBoolean(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY, false); - } - - public static CameraUploadsConfiguration getCameraUploadsConfiguration(Context context) { - CameraUploadsConfiguration result = new CameraUploadsConfiguration(); - SharedPreferences prefs = getDefaultSharedPreferences(context); - result.setEnabledForPictures( - prefs.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false) - ); - result.setEnabledForVideos( - prefs.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false) - ); - result.setWifiOnlyForPictures( - prefs.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY, false) - ); - result.setWifiOnlyForVideos( - prefs.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY, false) - ); - result.setUploadAccountNameForPictures( - prefs.getString(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME, "" ) - ); - result.setUploadAccountNameForVideos( - prefs.getString(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME, "" ) - ); - String uploadPath = prefs.getString( - PREF__CAMERA_PICTURE_UPLOADS_PATH, - PREF__CAMERA_UPLOADS_DEFAULT_PATH + File.separator - ); - result.setUploadPathForPictures( - uploadPath.endsWith(File.separator) ? uploadPath : uploadPath + File.separator - ); - uploadPath = prefs.getString( - PREF__CAMERA_VIDEO_UPLOADS_PATH, - PREF__CAMERA_UPLOADS_DEFAULT_PATH + File.separator - ); - result.setUploadPathForVideos( - uploadPath.endsWith(File.separator) ? uploadPath : uploadPath + File.separator - ); - result.setBehaviourAfterUploadPictures( - prefs.getString( - PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR, - context.getResources().getStringArray(R.array.pref_behaviour_entryValues)[0] - ) - ); - result.setBehaviourAfterUploadVideos( - prefs.getString( - PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR, - context.getResources().getStringArray(R.array.pref_behaviour_entryValues)[0] - ) - ); - result.setSourcePathPictures( - prefs.getString( - PREF__CAMERA_PICTURE_UPLOADS_SOURCE, - CameraUploadsConfiguration.getDefaultSourcePath() - ) - ); - result.setSourcePathVideos( - prefs.getString( - PREF__CAMERA_VIDEO_UPLOADS_SOURCE, - CameraUploadsConfiguration.getDefaultSourcePath() - ) - ); - return result; - } - /** * Gets the path where the user selected to do the last upload of a file shared from other app. * @@ -307,129 +221,4 @@ private static void saveIntPreference(String key, int value, Context context) { public static SharedPreferences getDefaultSharedPreferences(Context context) { return android.preference.PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } - - /** - * Aggregates preferences related to camera uploads in a single object. - */ - public static class CameraUploadsConfiguration { - - private boolean mEnabledForPictures; - private boolean mEnabledForVideos; - private boolean mWifiOnlyForPictures; - private boolean mWifiOnlyForVideos; - private String mUploadAccountNameForPictures; - private String mUploadAccountNameForVideos; - private String mUploadPathForPictures; - private String mUploadPathForVideos; - private String mBehaviourAfterUploadPictures; - private String mBehaviourAfterUploadVideos; - private String mSourcePathPictures; - private String mSourcePathVideos; - - public static String getDefaultSourcePath() { - return FileStorageUtils.getDefaultCameraSourcePath(); - } - - public boolean isEnabledForPictures() { - return mEnabledForPictures; - } - - public void setEnabledForPictures(boolean uploadPictures) { - mEnabledForPictures = uploadPictures; - } - - public boolean isEnabledForVideos() { - return mEnabledForVideos; - } - - public void setEnabledForVideos(boolean uploadVideos) { - mEnabledForVideos = uploadVideos; - } - - public boolean isWifiOnlyForPictures() { - return mWifiOnlyForPictures; - } - - public void setWifiOnlyForPictures(boolean wifiOnlyForPictures) { - mWifiOnlyForPictures = wifiOnlyForPictures; - } - - public boolean isWifiOnlyForVideos() { - return mWifiOnlyForVideos; - } - - public void setWifiOnlyForVideos(boolean wifiOnlyForVideos) { - mWifiOnlyForVideos = wifiOnlyForVideos; - } - - public String getUploadAccountNameForPictures() { - return mUploadAccountNameForPictures; - } - - public String getUploadAccountNameForVideos() { - return mUploadAccountNameForVideos; - } - - public void setUploadAccountNameForPictures(String uploadAccountName) { - mUploadAccountNameForPictures = uploadAccountName; - } - - public void setUploadAccountNameForVideos(String uploadAccountName) { - mUploadAccountNameForVideos = uploadAccountName; - } - - public String getUploadPathForPictures() { - return mUploadPathForPictures; - } - - public void setUploadPathForPictures(String uploadPathForPictures) { - mUploadPathForPictures = uploadPathForPictures; - } - - public String getUploadPathForVideos() { - return mUploadPathForVideos; - } - - public void setUploadPathForVideos(String uploadPathForVideos) { - mUploadPathForVideos = uploadPathForVideos; - } - - public int getBehaviourAfterUploadPictures() { - if (mBehaviourAfterUploadPictures.equalsIgnoreCase("MOVE")) { - return FileUploader.LOCAL_BEHAVIOUR_MOVE; - } - return FileUploader.LOCAL_BEHAVIOUR_FORGET; // "NOTHING" - } - - public void setBehaviourAfterUploadPictures(String behaviourAfterUploadPictures) { - mBehaviourAfterUploadPictures = behaviourAfterUploadPictures; - } - - public int getBehaviourAfterUploadVideos() { - if (mBehaviourAfterUploadVideos.equalsIgnoreCase("MOVE")) { - return FileUploader.LOCAL_BEHAVIOUR_MOVE; - } - return FileUploader.LOCAL_BEHAVIOUR_FORGET; // "NOTHING" - } - - public void setBehaviourAfterUploadVideos(String behaviourAfterUploadVideos) { - mBehaviourAfterUploadVideos = behaviourAfterUploadVideos; - } - - public String getSourcePathPictures() { - return mSourcePathPictures; - } - - public void setSourcePathPictures(String sourcePathPictures) { - mSourcePathPictures = sourcePathPictures; - } - - public String getSourcePathVideos() { - return mSourcePathVideos; - } - - public void setSourcePathVideos(String sourcePathVideos) { - mSourcePathVideos = sourcePathVideos; - } - } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/CommonModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/CommonModule.kt index 630fe6a9a3f..53d7b664993 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/CommonModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/CommonModule.kt @@ -22,11 +22,11 @@ package com.owncloud.android.dependecyinjection import com.owncloud.android.presentation.manager.AvatarManager import com.owncloud.android.providers.AccountProvider -import com.owncloud.android.providers.CameraUploadsHandlerProvider import com.owncloud.android.providers.ContextProvider import com.owncloud.android.providers.CoroutinesDispatcherProvider import com.owncloud.android.providers.LogsProvider import com.owncloud.android.providers.OCContextProvider +import com.owncloud.android.providers.WorkManagerProvider import org.koin.android.ext.koin.androidContext import org.koin.dsl.module @@ -36,6 +36,6 @@ val commonModule = module { single { CoroutinesDispatcherProvider() } factory { OCContextProvider(androidContext()) } single { LogsProvider(get()) } - single { CameraUploadsHandlerProvider(androidContext()) } + single { WorkManagerProvider(androidContext()) } single { AccountProvider(androidContext()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt index 8eee467813e..e5a261e09b7 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt @@ -22,9 +22,13 @@ package com.owncloud.android.dependecyinjection import android.accounts.AccountManager import com.owncloud.android.MainApp.Companion.accountType +import com.owncloud.android.MainApp.Companion.dataFolder +import com.owncloud.android.data.LocalStorageProvider import com.owncloud.android.data.OwncloudDatabase import com.owncloud.android.data.authentication.datasources.LocalAuthenticationDataSource import com.owncloud.android.data.authentication.datasources.implementation.OCLocalAuthenticationDataSource +import com.owncloud.android.data.folderbackup.datasources.FolderBackupLocalDataSource +import com.owncloud.android.data.folderbackup.datasources.implementation.FolderBackupLocalDataSourceImpl import com.owncloud.android.data.capabilities.datasources.LocalCapabilitiesDataSource import com.owncloud.android.data.capabilities.datasources.implementation.OCLocalCapabilitiesDataSource import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider @@ -42,11 +46,14 @@ val localDataSourceModule = module { single { OwncloudDatabase.getDatabase(androidContext()).capabilityDao() } single { OwncloudDatabase.getDatabase(androidContext()).shareDao() } single { OwncloudDatabase.getDatabase(androidContext()).userDao() } + single { OwncloudDatabase.getDatabase(androidContext()).folderBackUpDao() } single { SharedPreferencesProviderImpl(get()) } + single { LocalStorageProvider(dataFolder) } factory { OCLocalAuthenticationDataSource(androidContext(), get(), get(), accountType) } factory { OCLocalCapabilitiesDataSource(get()) } factory { OCLocalShareDataSource(get()) } factory { OCLocalUserDataSource(get()) } + factory { FolderBackupLocalDataSourceImpl(get()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt index 7ce3d8fa084..e61ff9dddb3 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt @@ -22,6 +22,7 @@ package com.owncloud.android.dependecyinjection import com.owncloud.android.data.authentication.repository.OCAuthenticationRepository import com.owncloud.android.data.capabilities.repository.OCCapabilityRepository import com.owncloud.android.data.files.repository.OCFileRepository +import com.owncloud.android.data.folderbackup.FolderBackupRepositoryImpl import com.owncloud.android.data.oauth.OAuthRepositoryImpl import com.owncloud.android.data.server.repository.OCServerInfoRepository import com.owncloud.android.data.sharing.sharees.repository.OCShareeRepository @@ -29,6 +30,7 @@ import com.owncloud.android.data.sharing.shares.repository.OCShareRepository import com.owncloud.android.data.user.repository.OCUserRepository import com.owncloud.android.domain.authentication.AuthenticationRepository import com.owncloud.android.domain.authentication.oauth.OAuthRepository +import com.owncloud.android.domain.camerauploads.FolderBackupRepository import com.owncloud.android.domain.capabilities.CapabilityRepository import com.owncloud.android.domain.files.FileRepository import com.owncloud.android.domain.server.ServerInfoRepository @@ -46,4 +48,5 @@ val repositoryModule = module { factory { OCShareRepository(get(), get()) } factory { OCUserRepository(get(), get()) } factory { OAuthRepositoryImpl(get()) } + factory { FolderBackupRepositoryImpl(get()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt index 98bcd9df194..a6be0bf81af 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -26,6 +26,13 @@ import com.owncloud.android.domain.authentication.usecases.GetBaseUrlUseCase import com.owncloud.android.domain.authentication.usecases.LoginBasicAsyncUseCase import com.owncloud.android.domain.authentication.usecases.LoginOAuthAsyncUseCase import com.owncloud.android.domain.authentication.usecases.SupportsOAuth2UseCase +import com.owncloud.android.domain.camerauploads.usecases.GetCameraUploadsConfigurationUseCase +import com.owncloud.android.domain.camerauploads.usecases.GetPictureUploadsConfigurationStreamUseCase +import com.owncloud.android.domain.camerauploads.usecases.GetVideoUploadsConfigurationStreamUseCase +import com.owncloud.android.domain.camerauploads.usecases.ResetPictureUploadsUseCase +import com.owncloud.android.domain.camerauploads.usecases.ResetVideoUploadsUseCase +import com.owncloud.android.domain.camerauploads.usecases.SavePictureUploadsConfigurationUseCase +import com.owncloud.android.domain.camerauploads.usecases.SaveVideoUploadsConfigurationUseCase import com.owncloud.android.domain.capabilities.usecases.GetCapabilitiesAsLiveDataUseCase import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase import com.owncloud.android.domain.capabilities.usecases.RefreshCapabilitiesFromServerAsyncUseCase @@ -81,4 +88,13 @@ val useCaseModule = module { // Server factory { GetServerInfoAsyncUseCase(get()) } + + // Camera Uploads + factory { GetCameraUploadsConfigurationUseCase(get()) } + factory { SavePictureUploadsConfigurationUseCase(get()) } + factory { SaveVideoUploadsConfigurationUseCase(get()) } + factory { ResetPictureUploadsUseCase(get()) } + factory { ResetVideoUploadsUseCase(get()) } + factory { GetPictureUploadsConfigurationStreamUseCase(get()) } + factory { GetVideoUploadsConfigurationStreamUseCase(get()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index a39f6ca5666..dbd852791e1 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -32,6 +32,7 @@ import com.owncloud.android.presentation.viewmodels.settings.SettingsSecurityVie import com.owncloud.android.presentation.viewmodels.settings.SettingsVideoUploadsViewModel import com.owncloud.android.presentation.viewmodels.settings.SettingsViewModel import com.owncloud.android.presentation.viewmodels.sharing.OCShareViewModel +import com.owncloud.android.ui.dialog.RemoveAccountDialogViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -53,6 +54,7 @@ val viewModelModule = module { viewModel { SettingsSecurityViewModel(get()) } viewModel { SettingsLogsViewModel(get(), get()) } viewModel { SettingsMoreViewModel(get()) } - viewModel { SettingsPictureUploadsViewModel(get(), get(), get()) } - viewModel { SettingsVideoUploadsViewModel(get(), get(), get()) } + viewModel { SettingsPictureUploadsViewModel(get(), get(), get(), get(), get(), get()) } + viewModel { SettingsVideoUploadsViewModel(get(), get(), get(), get(), get(), get()) } + viewModel { RemoveAccountDialogViewModel(get(), get(), get(), get()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt index 053bea11e5a..4a30bc4ae34 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt @@ -27,6 +27,7 @@ import com.owncloud.android.domain.exceptions.BadOcVersionException import com.owncloud.android.domain.exceptions.FileNotFoundException import com.owncloud.android.domain.exceptions.IncorrectAddressException import com.owncloud.android.domain.exceptions.InstanceNotConfiguredException +import com.owncloud.android.domain.exceptions.LocalFileNotFoundException import com.owncloud.android.domain.exceptions.NoConnectionWithServerException import com.owncloud.android.domain.exceptions.NoNetworkConnectionException import com.owncloud.android.domain.exceptions.OAuth2ErrorAccessDeniedException @@ -68,6 +69,7 @@ fun Throwable.parseError( is AccountNotNewException -> resources.getString(R.string.auth_account_not_new) is AccountNotTheSameException -> resources.getString(R.string.auth_account_not_the_same) is RedirectToNonSecureException -> resources.getString(R.string.auth_redirect_non_secure_connection_title) + is LocalFileNotFoundException -> resources.getString(R.string.local_file_not_found_toast) else -> resources.getString(R.string.common_error_unknown) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/files/services/CameraUploadsHandler.java b/owncloudApp/src/main/java/com/owncloud/android/files/services/CameraUploadsHandler.java deleted file mode 100644 index 6d4c5fccaaf..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/files/services/CameraUploadsHandler.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * ownCloud Android client application - * - * @author David González Verdugo - * @author Juan Carlos Garrote Gascón - * - * Copyright (C) 2017 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.files.services; - -import android.app.job.JobInfo; -import android.app.job.JobScheduler; -import android.content.ComponentName; -import android.content.Context; -import android.os.PersistableBundle; - -import com.owncloud.android.datamodel.CameraUploadsSyncStorageManager; -import com.owncloud.android.datamodel.OCCameraUploadSync; -import com.owncloud.android.db.PreferenceManager.CameraUploadsConfiguration; -import com.owncloud.android.utils.Extras; -import timber.log.Timber; - -/** - * Schedule the periodic job responsible for camera uploads and initialize the required - * information, as long as it matches the configuration for camera uploads - */ -public class CameraUploadsHandler { - - private static final long MILLISECONDS_INTERVAL_CAMERA_UPLOAD = 900000; - - // It needs to be always the same so that the previous job is removed and replaced with a new one with the recent - // configuration - private static final int JOB_ID_CAMERA_UPLOAD = 1; - - private CameraUploadsConfiguration mCameraUploadsConfig; // Camera uploads configuration, set by the user - - public CameraUploadsHandler(CameraUploadsConfiguration cameraUploadsConfiguration) { - mCameraUploadsConfig = cameraUploadsConfiguration; - } - - /** - * Schedule a periodic job to check pictures and videos to be uploaded - */ - public void scheduleCameraUploadsSyncJob(Context context) { - // DB Connection - CameraUploadsSyncStorageManager cameraUploadsSyncStorageManager = new - CameraUploadsSyncStorageManager(context.getContentResolver()); - - OCCameraUploadSync ocCameraUploadSync = cameraUploadsSyncStorageManager. - getCameraUploadSync(null, null, null); - - // Initialize synchronization timestamps for pictures/videos, if needed - if (ocCameraUploadSync == null || - ocCameraUploadSync.getPicturesLastSync() == 0 || - ocCameraUploadSync.getVideosLastSync() == 0) { - - initializeCameraUploadSync(cameraUploadsSyncStorageManager, ocCameraUploadSync); - } - - ComponentName serviceComponent = new ComponentName(context, CameraUploadsSyncJobService.class); - JobInfo.Builder builder; - - builder = new JobInfo.Builder(JOB_ID_CAMERA_UPLOAD, serviceComponent); - - builder.setPersisted(true); - - // Execute job every 15 minutes - builder.setPeriodic(MILLISECONDS_INTERVAL_CAMERA_UPLOAD); - - // Extra data - PersistableBundle extras = new PersistableBundle(); - - extras.putInt(Extras.EXTRA_CAMERA_UPLOADS_SYNC_JOB_ID, JOB_ID_CAMERA_UPLOAD); - - if (mCameraUploadsConfig.isEnabledForPictures()) { - extras.putString(Extras.EXTRA_CAMERA_UPLOADS_PICTURES_PATH, mCameraUploadsConfig. - getUploadPathForPictures()); - extras.putString(Extras.EXTRA_CAMERA_UPLOADS_PICTURES_SOURCE_PATH, mCameraUploadsConfig. - getSourcePathPictures()); - extras.putInt(Extras.EXTRA_CAMERA_UPLOADS_PICTURES_BEHAVIOR_AFTER_UPLOAD, mCameraUploadsConfig. - getBehaviourAfterUploadPictures()); - extras.putString(Extras.EXTRA_CAMERA_UPLOADS_PICTURES_ACCOUNT, mCameraUploadsConfig. - getUploadAccountNameForPictures()); - } - - if (mCameraUploadsConfig.isEnabledForVideos()) { - extras.putString(Extras.EXTRA_CAMERA_UPLOADS_VIDEOS_PATH, mCameraUploadsConfig. - getUploadPathForVideos()); - extras.putString(Extras.EXTRA_CAMERA_UPLOADS_VIDEOS_SOURCE_PATH, mCameraUploadsConfig. - getSourcePathVideos()); - extras.putInt(Extras.EXTRA_CAMERA_UPLOADS_VIDEOS_BEHAVIOR_AFTER_UPLOAD, mCameraUploadsConfig. - getBehaviourAfterUploadVideos()); - extras.putString(Extras.EXTRA_CAMERA_UPLOADS_VIDEOS_ACCOUNT, mCameraUploadsConfig. - getUploadAccountNameForVideos()); - } - - builder.setExtras(extras); - - Timber.d("Scheduling a CameraUploadsSyncJobService"); - - JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - - jobScheduler.schedule(builder.build()); - } - - /** - * Initialize the timestamps for upload pictures/videos. These timestamps define the start of the - * period in which to check the pictures/videos saved, discarding those created before enabling - * Camera Uploads feature - */ - private void initializeCameraUploadSync(CameraUploadsSyncStorageManager cameraUploadsSyncStorageManager, - OCCameraUploadSync ocCameraUploadSync) { - - // Set synchronization timestamps not needed - if (!mCameraUploadsConfig.isEnabledForPictures() && !mCameraUploadsConfig.isEnabledForVideos()) { - return; - } - - long timeStamp = System.currentTimeMillis(); - - if (ocCameraUploadSync == null) { // No synchronization timestamp for pictures/videos yet - - long firstPicturesTimeStamp = mCameraUploadsConfig.isEnabledForPictures() ? timeStamp : 0; - long firstVideosTimeStamp = mCameraUploadsConfig.isEnabledForVideos() ? timeStamp : 0; - - // Initialize synchronization timestamp for pictures or videos in database - OCCameraUploadSync firstOcCameraUploadSync = new OCCameraUploadSync(firstPicturesTimeStamp, - firstVideosTimeStamp); - - Timber.d("Storing synchronization timestamp in database"); - - cameraUploadsSyncStorageManager.storeCameraUploadSync(firstOcCameraUploadSync); - - } else { - - if (ocCameraUploadSync.getPicturesLastSync() == 0 && mCameraUploadsConfig.isEnabledForPictures()) { - // Pictures synchronization timestamp not initialized yet, initialize it - ocCameraUploadSync.setPicturesLastSync(timeStamp); - } - - if (ocCameraUploadSync.getVideosLastSync() == 0 && mCameraUploadsConfig.isEnabledForVideos()) { - // Videos synchronization timestamp not initialized yet, initialize it - ocCameraUploadSync.setVideosLastSync(timeStamp); - } - - cameraUploadsSyncStorageManager.updateCameraUploadSync(ocCameraUploadSync); - } - } - - /** - * Update timestamp (in milliseconds) from which to start checking pictures to upload - * - * @param lastSyncTimestamp - */ - public void updatePicturesLastSync(Context context, long lastSyncTimestamp) { - // DB connection - CameraUploadsSyncStorageManager cameraUploadsSyncStorageManager = new - CameraUploadsSyncStorageManager(context.getContentResolver()); - - OCCameraUploadSync ocCameraUploadSync = cameraUploadsSyncStorageManager. - getCameraUploadSync(null, null, null); - - if (ocCameraUploadSync != null) { - ocCameraUploadSync.setPicturesLastSync(lastSyncTimestamp); - cameraUploadsSyncStorageManager.updateCameraUploadSync(ocCameraUploadSync); - } - } - - /** - * Update timestamp (in milliseconds) from which to start checking videos to upload - * - * @param lastSyncTimestamp - */ - public void updateVideosLastSync(Context context, long lastSyncTimestamp) { - // DB connection - CameraUploadsSyncStorageManager cameraUploadsSyncStorageManager = new - CameraUploadsSyncStorageManager(context.getContentResolver()); - - OCCameraUploadSync ocCameraUploadSync = cameraUploadsSyncStorageManager. - getCameraUploadSync(null, null, null); - - if (ocCameraUploadSync == null) { - return; - } else { - ocCameraUploadSync.setVideosLastSync(lastSyncTimestamp); - cameraUploadsSyncStorageManager.updateCameraUploadSync(ocCameraUploadSync); - } - } - - public void setCameraUploadsConfig(CameraUploadsConfiguration mCameraUploadsConfig) { - this.mCameraUploadsConfig = mCameraUploadsConfig; - } -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/files/services/CameraUploadsSyncJobService.java b/owncloudApp/src/main/java/com/owncloud/android/files/services/CameraUploadsSyncJobService.java deleted file mode 100644 index a1f2b39cc12..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/files/services/CameraUploadsSyncJobService.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * ownCloud Android client application - * - * @author David González Verdugo - * @author Christian Schabesberger - * @author Juan Carlos Garrote Gascón - * - * Copyright (C) 2020 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.files.services; - -import android.accounts.Account; -import android.app.job.JobParameters; -import android.app.job.JobScheduler; -import android.app.job.JobService; -import android.content.Context; -import android.os.AsyncTask; - -import com.owncloud.android.authentication.AccountUtils; -import com.owncloud.android.datamodel.CameraUploadsSyncStorageManager; -import com.owncloud.android.datamodel.OCCameraUploadSync; -import com.owncloud.android.db.PreferenceManager; -import com.owncloud.android.db.PreferenceManager.CameraUploadsConfiguration; -import com.owncloud.android.operations.UploadFileOperation; -import com.owncloud.android.utils.Extras; -import com.owncloud.android.utils.MimetypeIconUtil; -import timber.log.Timber; - -import java.io.File; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Date; -import java.util.Locale; - -public class CameraUploadsSyncJobService extends JobService { - - @Override - public boolean onStartJob(JobParameters jobParameters) { - - Timber.d("Starting job to sync camera folder"); - - new CameraUploadsSyncJobTask(this).execute(jobParameters); - - return true; // True because we have a thread still running in background - } - - private static class CameraUploadsSyncJobTask extends AsyncTask { - - private final JobService mCameraUploadsSyncJobService; - - private Account mCameraUploadsVideosAccount; - private Account mCameraUploadsPicturesAccount; - private CameraUploadsSyncStorageManager mCameraUploadsSyncStorageManager; - private OCCameraUploadSync mOCCameraUploadSync; - private String mCameraUploadsPicturesPath; - private String mCameraUploadsVideosPath; - private String mCameraUploadsPicturesSourcePath; - private String mCameraUploadsVideosSourcePath; - private int mCameraUploadsPicturesBehaviorAfterUpload; - private int mCameraUploadsVideosBehaviorAfterUpload; - - public CameraUploadsSyncJobTask(JobService mCameraUploadsSyncJobService) { - this.mCameraUploadsSyncJobService = mCameraUploadsSyncJobService; - } - - @Override - protected JobParameters doInBackground(JobParameters... jobParams) { - // Cancel periodic job if feature is disabled - CameraUploadsConfiguration cameraUploadsConfiguration = PreferenceManager. - getCameraUploadsConfiguration(mCameraUploadsSyncJobService); - - if (!cameraUploadsConfiguration.isEnabledForPictures() && - !cameraUploadsConfiguration.isEnabledForVideos()) { - cancelPeriodicJob(jobParams[0].getJobId()); - - return jobParams[0]; - } - - String accountNameForPictureUploads = jobParams[0].getExtras().getString(Extras.EXTRA_CAMERA_UPLOADS_PICTURES_ACCOUNT); - mCameraUploadsPicturesAccount = AccountUtils.getOwnCloudAccountByName(mCameraUploadsSyncJobService, accountNameForPictureUploads); - String accountNameForVideoUploads = jobParams[0].getExtras().getString(Extras.EXTRA_CAMERA_UPLOADS_VIDEOS_ACCOUNT); - mCameraUploadsVideosAccount = AccountUtils.getOwnCloudAccountByName(mCameraUploadsSyncJobService, accountNameForVideoUploads); - mCameraUploadsSyncStorageManager = new CameraUploadsSyncStorageManager( - mCameraUploadsSyncJobService.getContentResolver()); - - mCameraUploadsPicturesPath = jobParams[0].getExtras().getString(Extras.EXTRA_CAMERA_UPLOADS_PICTURES_PATH); - mCameraUploadsVideosPath = jobParams[0].getExtras().getString(Extras.EXTRA_CAMERA_UPLOADS_VIDEOS_PATH); - mCameraUploadsPicturesSourcePath = jobParams[0].getExtras().getString(Extras.EXTRA_CAMERA_UPLOADS_PICTURES_SOURCE_PATH); - mCameraUploadsPicturesBehaviorAfterUpload = jobParams[0].getExtras(). - getInt(Extras.EXTRA_CAMERA_UPLOADS_PICTURES_BEHAVIOR_AFTER_UPLOAD); - mCameraUploadsVideosSourcePath = jobParams[0].getExtras().getString(Extras.EXTRA_CAMERA_UPLOADS_VIDEOS_SOURCE_PATH); - mCameraUploadsVideosBehaviorAfterUpload = jobParams[0].getExtras(). - getInt(Extras.EXTRA_CAMERA_UPLOADS_VIDEOS_BEHAVIOR_AFTER_UPLOAD); - - syncFiles(); - - return jobParams[0]; - } - - @Override - protected void onPostExecute(JobParameters jobParameters) { - mCameraUploadsSyncJobService.jobFinished(jobParameters, false); - } - - /** - * Get local images and videos and start handling them - */ - private void syncFiles() { - - // Get local images and videos - String localCameraPicturesPath = mCameraUploadsPicturesSourcePath; - String localCameraVideosPath = mCameraUploadsVideosSourcePath; - - File[] localPictureFiles = null; - File[] localVideoFiles = null; - - if (localCameraPicturesPath != null) { - File cameraPicturesFolder = new File(localCameraPicturesPath); - localPictureFiles = cameraPicturesFolder.listFiles(); - } - - if (localCameraVideosPath != null) { - File cameraVideosFolder = new File(localCameraVideosPath); - localVideoFiles = cameraVideosFolder.listFiles(); - } - - if (localPictureFiles != null) { - orderFilesByCreationTimestamp(localPictureFiles); - - for (File localFile : localPictureFiles) { - String fileName = localFile.getName(); - String mimeType = MimetypeIconUtil.getBestMimeTypeByFilename(fileName); - boolean isImage = mimeType.startsWith("image/"); - if (isImage) { - handleFile(localFile); - } - } - } - - if (localVideoFiles != null) { - orderFilesByCreationTimestamp(localVideoFiles); - - for (File localFile : localVideoFiles) { - String fileName = localFile.getName(); - String mimeType = MimetypeIconUtil.getBestMimeTypeByFilename(fileName); - boolean isVideo = mimeType.startsWith("video/"); - if (isVideo) { - handleFile(localFile); - } - } - } - - Timber.d("All files synced, finishing job"); - } - - private void orderFilesByCreationTimestamp(File[] localFiles) { - Arrays.sort(localFiles, (file1, file2) -> Long.compare(file1.lastModified(), file2.lastModified())); - } - - /** - * Request the upload of a file just created if matches the criteria of the current - * configuration for camera uploads. - * - * @param localFile image or video to upload to the server - */ - private synchronized void handleFile(File localFile) { - - String fileName = localFile.getName(); - String mimeType = MimetypeIconUtil.getBestMimeTypeByFilename(fileName); - boolean isImage = mimeType.startsWith("image/"); - boolean isVideo = mimeType.startsWith("video/"); - - String remotePath = (isImage ? mCameraUploadsPicturesPath : mCameraUploadsVideosPath) + fileName; - - int createdBy = isImage ? UploadFileOperation.CREATED_AS_CAMERA_UPLOAD_PICTURE : - UploadFileOperation.CREATED_AS_CAMERA_UPLOAD_VIDEO; - - String localPath = (isImage ? mCameraUploadsPicturesSourcePath : mCameraUploadsVideosSourcePath) + File.separator + fileName; - - int behaviour = isImage ? mCameraUploadsPicturesBehaviorAfterUpload : mCameraUploadsVideosBehaviorAfterUpload; - Account account = isImage ? mCameraUploadsPicturesAccount : mCameraUploadsVideosAccount; - - mOCCameraUploadSync = mCameraUploadsSyncStorageManager.getCameraUploadSync(null, null, - null); - - if (mOCCameraUploadSync == null) { - Timber.d("There's no timestamp to compare with in database yet, not continue"); - return; - } - - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()); - if (isImage && localFile.lastModified() <= mOCCameraUploadSync.getPicturesLastSync()) { - Timber.i("Image " + localPath + " created before period to check, ignoring " + - simpleDateFormat.format(new Date(localFile.lastModified())) + " <= " + - simpleDateFormat.format(new Date(mOCCameraUploadSync.getPicturesLastSync())) - ); - return; - } - - if (isVideo && localFile.lastModified() <= mOCCameraUploadSync.getVideosLastSync()) { - Timber.i("Video " + localPath + " created before period to check, ignoring " + - simpleDateFormat.format(new Date(localFile.lastModified())) + " <= " + - simpleDateFormat.format(new Date(mOCCameraUploadSync.getVideosLastSync())) - ); - return; - } - - TransferRequester requester = new TransferRequester(); - requester.uploadNewFile( - mCameraUploadsSyncJobService, - account, - localPath, - remotePath, - behaviour, - mimeType, - true, // create parent folder if not existent - createdBy - ); - - // Update timestamps once the first picture/video has been enqueued - updateTimestamps(isImage, isVideo, localFile.lastModified()); - - if (account != null) { - Timber.i("Requested upload of %1s to %2s in %3s", localPath, remotePath, account.name); - } else { - Timber.w("Requested upload of %1s to %2s with no account!!", localPath, remotePath); - } - } - - /** - * Update pictures and videos timestamps to upload only the pictures and videos taken later - * than those timestamps - * - * @param isImage true if file is an image, false otherwise - * @param isVideo true if file is a video, false otherwise - */ - private void updateTimestamps(boolean isImage, boolean isVideo, long fileTimestamp) { - - long picturesTimestamp = mOCCameraUploadSync.getPicturesLastSync(); - long videosTimestamp = mOCCameraUploadSync.getVideosLastSync(); - - if (isImage) { - - Timber.d("Updating timestamp for pictures"); - - picturesTimestamp = fileTimestamp; - } - - if (isVideo) { - - Timber.d("Updating timestamp for videos"); - - videosTimestamp = fileTimestamp; - } - - OCCameraUploadSync newOCCameraUploadSync = new OCCameraUploadSync(picturesTimestamp, - videosTimestamp); - - newOCCameraUploadSync.setId(mOCCameraUploadSync.getId()); - - mCameraUploadsSyncStorageManager.updateCameraUploadSync(newOCCameraUploadSync); - } - - /** - * Cancel the periodic job - * - * @param jobId id of the job to cancel - */ - private void cancelPeriodicJob(int jobId) { - - JobScheduler jobScheduler = (JobScheduler) mCameraUploadsSyncJobService.getSystemService( - Context.JOB_SCHEDULER_SERVICE); - - jobScheduler.cancel(jobId); - - Timber.d("Camera uploads disabled, cancelling the periodic job"); - } - } - - @Override - /* - * Called by the system if the job is cancelled before being finished - */ - public boolean onStopJob(JobParameters jobParameters) { - - Timber.d("Job was cancelled before finishing."); - - return true; - } -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/files/services/FileDownloader.java b/owncloudApp/src/main/java/com/owncloud/android/files/services/FileDownloader.java index 3a8d52d9b84..2615fbcc028 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/files/services/FileDownloader.java +++ b/owncloudApp/src/main/java/com/owncloud/android/files/services/FileDownloader.java @@ -52,8 +52,6 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; import com.owncloud.android.operations.DownloadFileOperation; -import com.owncloud.android.presentation.ui.authentication.AuthenticatorConstants; -import com.owncloud.android.presentation.ui.authentication.LoginActivity; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.errorhandling.ErrorMessageAdapter; @@ -602,20 +600,10 @@ private void notifyDownloadResult(DownloadFileOperation download, if (needsToUpdateCredentials) { // let the user update credentials with one click - Intent updateAccountCredentials = new Intent(this, LoginActivity.class); - updateAccountCredentials.putExtra(AuthenticatorConstants.EXTRA_ACCOUNT, - download.getAccount()); - updateAccountCredentials.putExtra( - AuthenticatorConstants.EXTRA_ACTION, - AuthenticatorConstants.ACTION_UPDATE_EXPIRED_TOKEN - ); - updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND); - getNotificationBuilder() - .setContentIntent(PendingIntent.getActivity( - this, (int) System.currentTimeMillis(), updateAccountCredentials, - PendingIntent.FLAG_ONE_SHOT)); + PendingIntent pendingIntentToRefreshCredentials = + NotificationUtils.INSTANCE.composePendingIntentToRefreshCredentials(this, download.getAccount()); + + getNotificationBuilder().setContentIntent(pendingIntentToRefreshCredentials); } else { // TODO put something smart in showDetailsIntent diff --git a/owncloudApp/src/main/java/com/owncloud/android/files/services/FileUploader.java b/owncloudApp/src/main/java/com/owncloud/android/files/services/FileUploader.java index f6095c4f625..076f16b7516 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/files/services/FileUploader.java +++ b/owncloudApp/src/main/java/com/owncloud/android/files/services/FileUploader.java @@ -65,8 +65,6 @@ import com.owncloud.android.operations.ChunkedUploadFileOperation; import com.owncloud.android.operations.RemoveChunksFolderOperation; import com.owncloud.android.operations.UploadFileOperation; -import com.owncloud.android.presentation.ui.authentication.AuthenticatorConstants; -import com.owncloud.android.presentation.ui.authentication.LoginActivity; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.UploadListActivity; import com.owncloud.android.ui.errorhandling.ErrorMessageAdapter; @@ -1025,24 +1023,10 @@ uploadResult, upload, getResources() if (needsToUpdateCredentials) { // let the user update credentials with one click - Intent updateAccountCredentials = new Intent(this, LoginActivity.class); - updateAccountCredentials.putExtra( - AuthenticatorConstants.EXTRA_ACCOUNT, upload.getAccount() - ); - updateAccountCredentials.putExtra( - AuthenticatorConstants.EXTRA_ACTION, - AuthenticatorConstants.ACTION_UPDATE_EXPIRED_TOKEN - ); + PendingIntent pendingIntentToRefreshCredentials = + NotificationUtils.INSTANCE.composePendingIntentToRefreshCredentials(this, upload.getAccount()); - updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND); - mNotificationBuilder.setContentIntent(PendingIntent.getActivity( - this, - (int) System.currentTimeMillis(), - updateAccountCredentials, - PendingIntent.FLAG_ONE_SHOT - )); + mNotificationBuilder.setContentIntent(pendingIntentToRefreshCredentials); } else { mNotificationBuilder.setContentText(content); diff --git a/owncloudApp/src/main/java/com/owncloud/android/files/services/TransferRequester.java b/owncloudApp/src/main/java/com/owncloud/android/files/services/TransferRequester.java index b201f482d0b..00f1b124aad 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/files/services/TransferRequester.java +++ b/owncloudApp/src/main/java/com/owncloud/android/files/services/TransferRequester.java @@ -27,15 +27,18 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.Build; import android.os.PersistableBundle; +import androidx.documentfile.provider.DocumentFile; +import androidx.work.WorkManager; import com.owncloud.android.authentication.AccountUtils; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.OCUpload; import com.owncloud.android.datamodel.UploadsStorageManager; -import com.owncloud.android.db.PreferenceManager; import com.owncloud.android.db.UploadResult; +import com.owncloud.android.usecases.UploadFileFromContentUriUseCase; import com.owncloud.android.utils.ConnectivityUtils; import com.owncloud.android.utils.Extras; import com.owncloud.android.utils.PowerUtils; @@ -205,7 +208,13 @@ public void retryFailedUploads(Context context, Account account, UploadResult up * false otherwise */ private void retry(Context context, Account account, OCUpload upload, boolean requestedFromWifiBackEvent) { - if (upload != null) { + if (upload == null) { + return; + } + + if (isContentUri(context, upload)) { + enqueueRetry(upload, context); + } else { Intent intent = new Intent(context, FileUploader.class); intent.putExtra(FileUploader.KEY_RETRY, true); intent.putExtra(FileUploader.KEY_ACCOUNT, account); @@ -230,6 +239,24 @@ private void retry(Context context, Account account, OCUpload upload, boolean re } } + private Boolean isContentUri(Context context, OCUpload upload) { + return DocumentFile.isDocumentUri(context, Uri.parse(upload.getLocalPath())); + } + + private void enqueueRetry(OCUpload upload, Context context) { + new UploadFileFromContentUriUseCase(WorkManager.getInstance(context)).execute( + new UploadFileFromContentUriUseCase.Params( + upload.getAccountName(), + Uri.parse(upload.getLocalPath()), + upload.getUploadEndTimestamp() / 1000 + "", + "COPY", + upload.getRemotePath(), + upload.getUploadId(), + false + ) + ); + } + /** * Return 'true' when conditions for a scheduled retry are met. * @@ -352,19 +379,9 @@ private int getRequiredNetworkType(Context context, String accountName, String r // Get last upload to be retried OCUpload ocUpload = uploadsStorageManager.getLastUploadFor(new OCFile(remotePath), accountName); - PreferenceManager.CameraUploadsConfiguration mConfig = PreferenceManager.getCameraUploadsConfiguration(context); - // Wifi by default int networkType = JobInfo.NETWORK_TYPE_UNMETERED; - if (ocUpload != null && (ocUpload.getCreatedBy() == CREATED_AS_CAMERA_UPLOAD_PICTURE && - !mConfig.isWifiOnlyForPictures() || ocUpload.getCreatedBy() == CREATED_AS_CAMERA_UPLOAD_VIDEO && - !mConfig.isWifiOnlyForVideos())) { - - // Wifi or cellular - networkType = JobInfo.NETWORK_TYPE_ANY; - } - return networkType; } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/owncloudApp/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index 384915fd6db..57dea4d1999 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/owncloudApp/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -281,12 +281,6 @@ protected RemoteOperationResult run(OwnCloudClient client) { try { - /// Check that connectivity conditions are met and delays the upload otherwise - if (delayForWifi()) { - Timber.d("Upload delayed until WiFi is available: %s", getRemotePath()); - return new RemoteOperationResult(ResultCode.DELAYED_FOR_WIFI); - } - /// check if the file continues existing before schedule the operation if (!originalFile.exists()) { Timber.d("%s not exists anymore", mOriginalStoragePath); @@ -469,25 +463,6 @@ protected void moveTemporalOriginalFiles(File temporalFile, File originalFile, S } } - /** - * Checks origin of current upload and network type to decide if should be delayed, according to - * current user preferences. - * - * @return 'True' if the upload was delayed until WiFi connectivity is available, 'false' otherwise. - */ - private boolean delayForWifi() { - boolean delayCameraUploadsPicture = ( - isCameraUploadsPicture() && PreferenceManager.cameraPictureUploadViaWiFiOnly(mContext) - ); - boolean delayCameraUploadsVideo = ( - isCameraUploadsVideo() && PreferenceManager.cameraVideoUploadViaWiFiOnly(mContext) - ); - return ( - (delayCameraUploadsPicture || delayCameraUploadsVideo) && - !ConnectivityUtils.isAppConnectedViaWiFi(mContext) - ); - } - /** * Checks the existence of the folder where the current file will be uploaded both * in the remote server and in the local database. diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/SettingsActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/SettingsActivity.kt index eb31a60f977..b2a58316091 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/SettingsActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/SettingsActivity.kt @@ -56,13 +56,7 @@ class SettingsActivity : AppCompatActivity() { if (savedInstanceState != null) return - supportFragmentManager - .beginTransaction() - .replace( - R.id.settings_container, - SettingsFragment() - ) - .commit() + redirectToSubsection(intent) } private fun updateToolbarTitle() { @@ -93,4 +87,23 @@ class SettingsActivity : AppCompatActivity() { } return super.onOptionsItemSelected(item) } + + private fun redirectToSubsection(intent: Intent?) { + val fragment = when (intent?.getStringExtra(KEY_NOTIFICATION_INTENT)) { + NOTIFICATION_INTENT_PICTURES -> SettingsPictureUploadsFragment() + NOTIFICATION_INTENT_VIDEOS -> SettingsVideoUploadsFragment() + else -> SettingsFragment() + } + + supportFragmentManager + .beginTransaction() + .replace(R.id.settings_container, fragment) + .commit() + } + + companion object { + const val KEY_NOTIFICATION_INTENT = "key_notification_intent" + const val NOTIFICATION_INTENT_PICTURES = "picture_uploads" + const val NOTIFICATION_INTENT_VIDEOS = "video_uploads" + } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsPictureUploadsFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsPictureUploadsFragment.kt index 74539525162..7079f8bceda 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsPictureUploadsFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsPictureUploadsFragment.kt @@ -25,7 +25,10 @@ import android.content.DialogInterface import android.content.Intent import android.os.Build import android.os.Bundle +import android.provider.DocumentsContract +import android.view.View import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.net.toUri import androidx.preference.CheckBoxPreference import androidx.preference.ListPreference import androidx.preference.Preference @@ -35,12 +38,13 @@ import com.owncloud.android.R import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ENABLED +import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_LAST_SYNC import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_PATH import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_SOURCE import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration import com.owncloud.android.extensions.showAlertDialog import com.owncloud.android.presentation.viewmodels.settings.SettingsPictureUploadsViewModel -import com.owncloud.android.ui.activity.LocalFolderPickerActivity import com.owncloud.android.ui.activity.UploadPathActivity import com.owncloud.android.utils.DisplayUtils import org.koin.androidx.viewmodel.ext.android.viewModel @@ -57,21 +61,23 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() { private var prefPictureUploadsSourcePath: Preference? = null private var prefPictureUploadsBehaviour: ListPreference? = null private var prefPictureUploadsAccount: ListPreference? = null + private var prefPictureUploadsLastSync: Preference? = null private val selectPictureUploadsPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult picturesViewModel.handleSelectPictureUploadsPath(result.data) - prefPictureUploadsPath?.summary = - DisplayUtils.getPathWithoutLastSlash(picturesViewModel.getPictureUploadsPath()) } private val selectPictureUploadsSourcePathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult - picturesViewModel.handleSelectPictureUploadsSourcePath(result.data) - prefPictureUploadsSourcePath?.summary = - DisplayUtils.getPathWithoutLastSlash(picturesViewModel.getPictureUploadsSourcePath()) + // here we ask the content resolver to persist the permission for us + val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + val contentUriForTree = result.data!!.data!! + + requireContext().contentResolver.takePersistableUriPermission(contentUriForTree, takeFlags) + picturesViewModel.handleSelectPictureUploadsSourcePath(contentUriForTree) } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -81,38 +87,48 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() { prefPictureUploadsPath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH) prefPictureUploadsOnWifi = findPreference(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY) prefPictureUploadsSourcePath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE) - prefPictureUploadsBehaviour = findPreference(PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR) + prefPictureUploadsLastSync = findPreference(PREF__CAMERA_PICTURE_UPLOADS_LAST_SYNC) + prefPictureUploadsBehaviour = findPreference(PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR)?.apply { + entries = listOf(getString(R.string.pref_behaviour_entries_keep_file), getString(R.string.pref_behaviour_entries_move)).toTypedArray() + entryValues = listOf(FolderBackUpConfiguration.Behavior.COPY.name, FolderBackUpConfiguration.Behavior.MOVE.name).toTypedArray() + } prefPictureUploadsAccount = findPreference(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME)?.apply { entries = picturesViewModel.getLoggedAccountNames() entryValues = picturesViewModel.getLoggedAccountNames() } - enablePictureUploads(picturesViewModel.isPictureUploadEnabled()) - - prefPictureUploadsPath?.summary = - DisplayUtils.getPathWithoutLastSlash(picturesViewModel.getPictureUploadsPath()) - prefPictureUploadsSourcePath?.summary = - DisplayUtils.getPathWithoutLastSlash(picturesViewModel.getPictureUploadsSourcePath()) - val comment = - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) getString( - R.string.prefs_camera_upload_source_path_title_optional - ) - else getString( - R.string.prefs_camera_upload_source_path_title_required - ) + val comment = getString(R.string.prefs_camera_upload_source_path_title_required) prefPictureUploadsSourcePath?.title = String.format(prefPictureUploadsSourcePath?.title.toString(), comment) initPreferenceListeners() } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initLiveDataObservers() + } + + private fun initLiveDataObservers() { + picturesViewModel.pictureUploads.observe(viewLifecycleOwner, { pictureUploadsConfiguration -> + enablePictureUploads(pictureUploadsConfiguration != null) + pictureUploadsConfiguration?.let { + prefPictureUploadsAccount?.value = it.accountName + prefPictureUploadsPath?.summary = DisplayUtils.getPathWithoutLastSlash(it.uploadPath) + prefPictureUploadsSourcePath?.summary = DisplayUtils.getPathWithoutLastSlash(it.sourcePath.toUri().path) + prefPictureUploadsOnWifi?.isChecked = it.wifiOnly + prefPictureUploadsBehaviour?.value = it.behavior.name + prefPictureUploadsLastSync?.summary = DisplayUtils.unixTimeToHumanReadable(it.lastSyncTimestamp) + } ?: resetFields() + }) + } + private fun initPreferenceListeners() { prefEnablePictureUploads?.setOnPreferenceChangeListener { _: Preference?, newValue: Any -> val value = newValue as Boolean if (value) { - picturesViewModel.setEnablePictureUpload(value) - enablePictureUploads(value) - prefPictureUploadsAccount?.value = picturesViewModel.getPictureUploadsAccount() + picturesViewModel.enablePictureUploads() showAlertDialog( title = getString(R.string.common_important), message = getString(R.string.proper_pics_folder_warning_camera_upload) @@ -124,10 +140,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() { message = getString(R.string.confirmation_disable_pictures_upload_message), positiveButtonText = getString(R.string.common_yes), positiveButtonListener = { _: DialogInterface?, _: Int -> - picturesViewModel.updatePicturesLastSync() - picturesViewModel.setEnablePictureUpload(value) - enablePictureUploads(false) - resetPreferencesAfterDisablingPicturesUploads() + picturesViewModel.disablePictureUploads() }, negativeButtonText = getString(R.string.common_no) ) @@ -137,31 +150,60 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() { prefPictureUploadsPath?.setOnPreferenceClickListener { var uploadPath = picturesViewModel.getPictureUploadsPath() - if (uploadPath?.endsWith(File.separator) == false) { + if (!uploadPath.endsWith(File.separator)) { uploadPath += File.separator } - val intent = Intent(activity, UploadPathActivity::class.java) - intent.putExtra(UploadPathActivity.KEY_CAMERA_UPLOAD_PATH, uploadPath) - intent.putExtra(UploadPathActivity.KEY_CAMERA_UPLOAD_ACCOUNT, picturesViewModel.getPictureUploadsAccount()) + val intent = Intent(activity, UploadPathActivity::class.java).apply { + putExtra(UploadPathActivity.KEY_CAMERA_UPLOAD_PATH, uploadPath) + putExtra(UploadPathActivity.KEY_CAMERA_UPLOAD_ACCOUNT, picturesViewModel.getPictureUploadsAccount()) + } selectPictureUploadsPathLauncher.launch(intent) true } prefPictureUploadsSourcePath?.setOnPreferenceClickListener { var sourcePath = picturesViewModel.getPictureUploadsSourcePath() - if (sourcePath?.endsWith(File.separator) == false) { + .takeUnless { it.isNullOrBlank() } ?: picturesViewModel.getDefaultSourcePath() + if (!sourcePath.endsWith(File.separator)) { sourcePath += File.separator } - val intent = Intent(activity, LocalFolderPickerActivity::class.java) - intent.putExtra(LocalFolderPickerActivity.EXTRA_PATH, sourcePath) + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + putExtra(DocumentsContract.EXTRA_INITIAL_URI, sourcePath) + } + addFlags( + Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + ) + } selectPictureUploadsSourcePathLauncher.launch(intent) true } + + prefPictureUploadsOnWifi?.setOnPreferenceChangeListener { _, newValue -> + newValue as Boolean + picturesViewModel.useWifiOnly(newValue) + newValue + } + + prefPictureUploadsAccount?.setOnPreferenceChangeListener { _, newValue -> + newValue as String + picturesViewModel.handleSelectAccount(newValue) + true + } + + prefPictureUploadsBehaviour?.setOnPreferenceChangeListener { _, newValue -> + newValue as String + picturesViewModel.handleSelectBehaviour(newValue) + true + } } - override fun onStop() { - picturesViewModel.schedulePictureUploadsSyncJob() - super.onStop() + override fun onDestroy() { + picturesViewModel.schedulePictureUploads() + super.onDestroy() } private fun enablePictureUploads(value: Boolean) { @@ -171,10 +213,15 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() { prefPictureUploadsSourcePath?.isEnabled = value prefPictureUploadsBehaviour?.isEnabled = value prefPictureUploadsAccount?.isEnabled = value + prefPictureUploadsLastSync?.isEnabled = value } - private fun resetPreferencesAfterDisablingPicturesUploads() { + private fun resetFields() { prefPictureUploadsAccount?.value = null - prefPictureUploadsPath?.summary = DisplayUtils.getPathWithoutLastSlash(picturesViewModel.getPictureUploadsPath()) + prefPictureUploadsPath?.summary = null + prefPictureUploadsSourcePath?.summary = null + prefPictureUploadsOnWifi?.isChecked = false + prefPictureUploadsBehaviour?.value = FolderBackUpConfiguration.Behavior.COPY.name + prefPictureUploadsLastSync?.summary = null } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsVideoUploadsFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsVideoUploadsFragment.kt index ab35426166c..4ca47ebf970 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsVideoUploadsFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/settings/fragments/SettingsVideoUploadsFragment.kt @@ -25,22 +25,26 @@ import android.content.DialogInterface import android.content.Intent import android.os.Build import android.os.Bundle +import android.provider.DocumentsContract +import android.view.View import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.net.toUri import androidx.preference.CheckBoxPreference import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat import com.owncloud.android.R +import com.owncloud.android.db.PreferenceManager import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ENABLED import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_PATH import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_SOURCE import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration import com.owncloud.android.extensions.showAlertDialog import com.owncloud.android.presentation.viewmodels.settings.SettingsVideoUploadsViewModel -import com.owncloud.android.ui.activity.LocalFolderPickerActivity import com.owncloud.android.ui.activity.UploadPathActivity import com.owncloud.android.utils.DisplayUtils import org.koin.androidx.viewmodel.ext.android.viewModel @@ -57,21 +61,23 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() { private var prefVideoUploadsSourcePath: Preference? = null private var prefVideoUploadsBehaviour: ListPreference? = null private var prefVideoUploadsAccount: ListPreference? = null + private var prefVideoUploadsLastSync: Preference? = null private val selectVideoUploadsPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult videosViewModel.handleSelectVideoUploadsPath(result.data) - prefVideoUploadsPath?.summary = - DisplayUtils.getPathWithoutLastSlash(videosViewModel.getVideoUploadsPath()) } private val selectVideoUploadsSourcePathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult - videosViewModel.handleSelectVideoUploadsSourcePath(result.data) - prefVideoUploadsSourcePath?.summary = - DisplayUtils.getPathWithoutLastSlash(videosViewModel.getVideoUploadsSourcePath()) + // here we ask the content resolver to persist the permission for us + val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + val contentUriForTree = result.data!!.data!! + + requireContext().contentResolver.takePersistableUriPermission(contentUriForTree, takeFlags) + videosViewModel.handleSelectVideoUploadsSourcePath(contentUriForTree) } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -81,38 +87,48 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() { prefVideoUploadsPath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH) prefVideoUploadsOnWifi = findPreference(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY) prefVideoUploadsSourcePath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE) - prefVideoUploadsBehaviour = findPreference(PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR) + prefVideoUploadsLastSync = findPreference(PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_LAST_SYNC) + prefVideoUploadsBehaviour = findPreference(PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR)?.apply { + entries = listOf(getString(R.string.pref_behaviour_entries_keep_file), getString(R.string.pref_behaviour_entries_move)).toTypedArray() + entryValues = listOf(FolderBackUpConfiguration.Behavior.COPY.name, FolderBackUpConfiguration.Behavior.MOVE.name).toTypedArray() + } prefVideoUploadsAccount = findPreference(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME)?.apply { entries = videosViewModel.getLoggedAccountNames() entryValues = videosViewModel.getLoggedAccountNames() } - enableVideoUploads(videosViewModel.isVideoUploadEnabled()) - - prefVideoUploadsPath?.summary = - DisplayUtils.getPathWithoutLastSlash(videosViewModel.getVideoUploadsPath()) - prefVideoUploadsSourcePath?.summary = - DisplayUtils.getPathWithoutLastSlash(videosViewModel.getVideoUploadsSourcePath()) - val comment = - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) getString( - R.string.prefs_camera_upload_source_path_title_optional - ) - else getString( - R.string.prefs_camera_upload_source_path_title_required - ) + val comment = getString(R.string.prefs_camera_upload_source_path_title_required) prefVideoUploadsSourcePath?.title = String.format(prefVideoUploadsSourcePath?.title.toString(), comment) initPreferenceListeners() } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initLiveDataObservers() + } + + private fun initLiveDataObservers() { + videosViewModel.videoUploads.observe(viewLifecycleOwner, { videoUploadsConfiguration -> + enableVideoUploads(videoUploadsConfiguration != null) + videoUploadsConfiguration?.let { + prefVideoUploadsAccount?.value = it.accountName + prefVideoUploadsPath?.summary = DisplayUtils.getPathWithoutLastSlash(it.uploadPath) + prefVideoUploadsSourcePath?.summary = DisplayUtils.getPathWithoutLastSlash(it.sourcePath.toUri().path) + prefVideoUploadsOnWifi?.isChecked = it.wifiOnly + prefVideoUploadsBehaviour?.value = it.behavior.name + prefVideoUploadsLastSync?.summary = DisplayUtils.unixTimeToHumanReadable(it.lastSyncTimestamp) + } ?: resetFields() + }) + } + private fun initPreferenceListeners() { prefEnableVideoUploads?.setOnPreferenceChangeListener { _: Preference?, newValue: Any -> val value = newValue as Boolean if (value) { - videosViewModel.setEnableVideoUpload(value) - enableVideoUploads(value) - prefVideoUploadsAccount?.value = videosViewModel.getVideoUploadsAccount() + videosViewModel.enableVideoUploads() showAlertDialog( title = getString(R.string.common_important), message = getString(R.string.proper_videos_folder_warning_camera_upload) @@ -124,10 +140,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() { message = getString(R.string.confirmation_disable_videos_upload_message), positiveButtonText = getString(R.string.common_yes), positiveButtonListener = { _: DialogInterface?, _: Int -> - videosViewModel.updateVideosLastSync() - videosViewModel.setEnableVideoUpload(value) - enableVideoUploads(false) - resetPreferencesAfterDisablingVideosUploads() + videosViewModel.disableVideoUploads() }, negativeButtonText = getString(R.string.common_no) ) @@ -137,31 +150,60 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() { prefVideoUploadsPath?.setOnPreferenceClickListener { var uploadPath = videosViewModel.getVideoUploadsPath() - if (uploadPath?.endsWith(File.separator) == false) { + if (!uploadPath.endsWith(File.separator)) { uploadPath += File.separator } - val intent = Intent(activity, UploadPathActivity::class.java) - intent.putExtra(UploadPathActivity.KEY_CAMERA_UPLOAD_PATH, uploadPath) - intent.putExtra(UploadPathActivity.KEY_CAMERA_UPLOAD_ACCOUNT, videosViewModel.getVideoUploadsAccount()) + val intent = Intent(activity, UploadPathActivity::class.java).apply { + putExtra(UploadPathActivity.KEY_CAMERA_UPLOAD_PATH, uploadPath) + putExtra(UploadPathActivity.KEY_CAMERA_UPLOAD_ACCOUNT, videosViewModel.getVideoUploadsAccount()) + } selectVideoUploadsPathLauncher.launch(intent) true } prefVideoUploadsSourcePath?.setOnPreferenceClickListener { var sourcePath = videosViewModel.getVideoUploadsSourcePath() - if (sourcePath?.endsWith(File.separator) == false) { + .takeUnless { it.isNullOrBlank() } ?: videosViewModel.getDefaultCameraSourcePath() + if (!sourcePath.endsWith(File.separator)) { sourcePath += File.separator } - val intent = Intent(activity, LocalFolderPickerActivity::class.java) - intent.putExtra(LocalFolderPickerActivity.EXTRA_PATH, sourcePath) + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + putExtra(DocumentsContract.EXTRA_INITIAL_URI, sourcePath) + } + addFlags( + Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + ) + } selectVideoUploadsSourcePathLauncher.launch(intent) true } + + prefVideoUploadsOnWifi?.setOnPreferenceChangeListener { _, newValue -> + newValue as Boolean + videosViewModel.useWifiOnly(newValue) + newValue + } + + prefVideoUploadsAccount?.setOnPreferenceChangeListener { _, newValue -> + newValue as String + videosViewModel.handleSelectAccount(newValue) + true + } + + prefVideoUploadsBehaviour?.setOnPreferenceChangeListener { _, newValue -> + newValue as String + videosViewModel.handleSelectBehaviour(newValue) + true + } } - override fun onStop() { - videosViewModel.scheduleVideoUploadsSyncJob() - super.onStop() + override fun onDestroy() { + videosViewModel.scheduleVideoUploads() + super.onDestroy() } private fun enableVideoUploads(value: Boolean) { @@ -171,10 +213,15 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() { prefVideoUploadsSourcePath?.isEnabled = value prefVideoUploadsBehaviour?.isEnabled = value prefVideoUploadsAccount?.isEnabled = value + prefVideoUploadsLastSync?.isEnabled = value } - private fun resetPreferencesAfterDisablingVideosUploads() { + private fun resetFields() { prefVideoUploadsAccount?.value = null - prefVideoUploadsPath?.summary = DisplayUtils.getPathWithoutLastSlash(videosViewModel.getVideoUploadsPath()) + prefVideoUploadsPath?.summary = null + prefVideoUploadsSourcePath?.summary = null + prefVideoUploadsOnWifi?.isChecked = false + prefVideoUploadsBehaviour?.value = FolderBackUpConfiguration.Behavior.COPY.name + prefVideoUploadsLastSync?.summary = null } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsPictureUploadsViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsPictureUploadsViewModel.kt index 6bfcf72740a..28801072829 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsPictureUploadsViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsPictureUploadsViewModel.kt @@ -21,98 +21,154 @@ package com.owncloud.android.presentation.viewmodels.settings import android.content.Intent +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import androidx.lifecycle.viewModelScope import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.db.PreferenceManager.CameraUploadsConfiguration -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ENABLED -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_PATH -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_SOURCE import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_UPLOADS_DEFAULT_PATH +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration.Companion.pictureUploadsName +import com.owncloud.android.domain.camerauploads.usecases.GetPictureUploadsConfigurationStreamUseCase +import com.owncloud.android.domain.camerauploads.usecases.ResetPictureUploadsUseCase +import com.owncloud.android.domain.camerauploads.usecases.SavePictureUploadsConfigurationUseCase import com.owncloud.android.providers.AccountProvider -import com.owncloud.android.providers.CameraUploadsHandlerProvider -import com.owncloud.android.ui.activity.LocalFolderPickerActivity +import com.owncloud.android.providers.CoroutinesDispatcherProvider +import com.owncloud.android.providers.WorkManagerProvider import com.owncloud.android.ui.activity.UploadPathActivity +import com.owncloud.android.utils.FileStorageUtils.getDefaultCameraSourcePath +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import java.io.File class SettingsPictureUploadsViewModel( - private val preferencesProvider: SharedPreferencesProvider, - private val cameraUploadsHandlerProvider: CameraUploadsHandlerProvider, - private val accountProvider: AccountProvider + private val accountProvider: AccountProvider, + private val savePictureUploadsConfigurationUseCase: SavePictureUploadsConfigurationUseCase, + private val getPictureUploadsConfigurationStreamUseCase: GetPictureUploadsConfigurationStreamUseCase, + private val resetPictureUploadsUseCase: ResetPictureUploadsUseCase, + private val workManagerProvider: WorkManagerProvider, + private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, ) : ViewModel() { - fun isPictureUploadEnabled() = - preferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false) + private val _pictureUploads: MutableLiveData = MutableLiveData() + val pictureUploads: LiveData = _pictureUploads - fun setEnablePictureUpload(value: Boolean) { - preferencesProvider.putBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, value) + init { + initPictureUploads() + } + + private fun initPictureUploads() { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + getPictureUploadsConfigurationStreamUseCase.execute(Unit).collect() { pictureUploadsConfiguration -> + _pictureUploads.postValue(pictureUploadsConfiguration) + } + } + } - if (value) { - // Use current account as default. It should never be null. If no accounts are attached, picture uploads are hidden - accountProvider.getCurrentOwnCloudAccount()?.name?.let { name -> - preferencesProvider.putString( - key = PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME, - value = name + fun enablePictureUploads() { + // Use current account as default. It should never be null. If no accounts are attached, picture uploads are hidden + accountProvider.getCurrentOwnCloudAccount()?.name?.let { name -> + viewModelScope.launch(coroutinesDispatcherProvider.io) { + savePictureUploadsConfigurationUseCase.execute( + SavePictureUploadsConfigurationUseCase.Params(composePictureUploadsConfiguration(accountName = name)) ) } - } else { - // Reset fields after disabling the feature - preferencesProvider.removePreference(key = PREF__CAMERA_PICTURE_UPLOADS_PATH) - preferencesProvider.removePreference(key = PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME) } } - fun updatePicturesLastSync() = cameraUploadsHandlerProvider.updatePicturesLastSync(0) + fun disablePictureUploads() { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + resetPictureUploadsUseCase.execute(Unit) + } + } - fun getPictureUploadsAccount() = preferencesProvider.getString( - key = PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME, - defaultValue = null - ) + fun useWifiOnly(wifiOnly: Boolean) { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + savePictureUploadsConfigurationUseCase.execute( + SavePictureUploadsConfigurationUseCase.Params( + composePictureUploadsConfiguration(wifiOnly = wifiOnly) + ) + ) + } + } + + fun getPictureUploadsAccount() = _pictureUploads.value?.accountName fun getLoggedAccountNames(): Array = accountProvider.getLoggedAccounts().map { it.name }.toTypedArray() - fun getPictureUploadsPath() = preferencesProvider.getString( - key = PREF__CAMERA_PICTURE_UPLOADS_PATH, - defaultValue = PREF__CAMERA_UPLOADS_DEFAULT_PATH - ) + fun getPictureUploadsPath() = _pictureUploads.value?.uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH - fun getPictureUploadsSourcePath() = preferencesProvider.getString( - key = PREF__CAMERA_PICTURE_UPLOADS_SOURCE, - defaultValue = CameraUploadsConfiguration.getDefaultSourcePath() - ) + fun getPictureUploadsSourcePath(): String? = _pictureUploads.value?.sourcePath + + fun getDefaultSourcePath(): String = getDefaultCameraSourcePath() fun handleSelectPictureUploadsPath(data: Intent?) { val folderToUpload = data?.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER) folderToUpload?.remotePath?.let { - preferencesProvider.putString( - key = PREF__CAMERA_PICTURE_UPLOADS_PATH, - value = it - ) + viewModelScope.launch(coroutinesDispatcherProvider.io) { + savePictureUploadsConfigurationUseCase.execute( + SavePictureUploadsConfigurationUseCase.Params(composePictureUploadsConfiguration(uploadPath = it)) + ) + } } } - fun handleSelectPictureUploadsSourcePath(data: Intent?) { - // If the source path has changed, update camera uploads last sync - var previousSourcePath = preferencesProvider.getString( - key = PREF__CAMERA_PICTURE_UPLOADS_SOURCE, - defaultValue = CameraUploadsConfiguration.getDefaultSourcePath() - ) + fun handleSelectAccount(accountName: String) { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + savePictureUploadsConfigurationUseCase.execute( + SavePictureUploadsConfigurationUseCase.Params(composePictureUploadsConfiguration(accountName = accountName)) + ) + } + } - previousSourcePath = previousSourcePath?.trimEnd(File.separatorChar) + fun handleSelectBehaviour(behaviorString: String) { + val behavior = FolderBackUpConfiguration.Behavior.fromString(behaviorString) - if (previousSourcePath != data?.getStringExtra(LocalFolderPickerActivity.EXTRA_PATH)) { - val currentTimeStamp = System.currentTimeMillis() - cameraUploadsHandlerProvider.updatePicturesLastSync(currentTimeStamp) + viewModelScope.launch(coroutinesDispatcherProvider.io) { + savePictureUploadsConfigurationUseCase.execute( + SavePictureUploadsConfigurationUseCase.Params(composePictureUploadsConfiguration(behavior = behavior)) + ) } + } - data?.getStringExtra(LocalFolderPickerActivity.EXTRA_PATH)?.let { - preferencesProvider.putString( - key = PREF__CAMERA_PICTURE_UPLOADS_SOURCE, - value = it + fun handleSelectPictureUploadsSourcePath(contentUriForTree: Uri) { + // If the source path has changed, update camera uploads last sync + var previousSourcePath = _pictureUploads.value?.sourcePath ?: getDefaultCameraSourcePath() + + previousSourcePath = previousSourcePath.trimEnd(File.separatorChar) + + viewModelScope.launch(coroutinesDispatcherProvider.io) { + savePictureUploadsConfigurationUseCase.execute( + SavePictureUploadsConfigurationUseCase.Params( + composePictureUploadsConfiguration( + sourcePath = contentUriForTree.toString(), + timestamp = System.currentTimeMillis().takeIf { previousSourcePath != contentUriForTree.encodedPath } + ) + ) ) } } - fun schedulePictureUploadsSyncJob() = cameraUploadsHandlerProvider.schedulePictureUploadsSyncJob() + fun schedulePictureUploads() { + workManagerProvider.enqueueCameraUploadsWorker() + } + + private fun composePictureUploadsConfiguration( + accountName: String? = _pictureUploads.value?.accountName, + uploadPath: String? = _pictureUploads.value?.uploadPath, + wifiOnly: Boolean? = _pictureUploads.value?.wifiOnly, + sourcePath: String? = _pictureUploads.value?.sourcePath, + behavior: FolderBackUpConfiguration.Behavior? = _pictureUploads.value?.behavior, + timestamp: Long? = _pictureUploads.value?.lastSyncTimestamp + ): FolderBackUpConfiguration = + FolderBackUpConfiguration( + accountName = accountName ?: accountProvider.getCurrentOwnCloudAccount()!!.name, + behavior = behavior ?: FolderBackUpConfiguration.Behavior.COPY, + sourcePath = sourcePath.orEmpty(), + uploadPath = uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH, + wifiOnly = wifiOnly ?: false, + lastSyncTimestamp = timestamp ?: System.currentTimeMillis(), + name = _pictureUploads.value?.name ?: pictureUploadsName + ) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsVideoUploadsViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsVideoUploadsViewModel.kt index a760f590303..6dbda70b18f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsVideoUploadsViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/viewmodels/settings/SettingsVideoUploadsViewModel.kt @@ -21,98 +21,154 @@ package com.owncloud.android.presentation.viewmodels.settings import android.content.Intent +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import androidx.lifecycle.viewModelScope import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.db.PreferenceManager.CameraUploadsConfiguration import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_UPLOADS_DEFAULT_PATH -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ENABLED -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_PATH -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_SOURCE +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration.Companion.videoUploadsName +import com.owncloud.android.domain.camerauploads.usecases.GetVideoUploadsConfigurationStreamUseCase +import com.owncloud.android.domain.camerauploads.usecases.ResetVideoUploadsUseCase +import com.owncloud.android.domain.camerauploads.usecases.SaveVideoUploadsConfigurationUseCase import com.owncloud.android.providers.AccountProvider -import com.owncloud.android.providers.CameraUploadsHandlerProvider -import com.owncloud.android.ui.activity.LocalFolderPickerActivity +import com.owncloud.android.providers.CoroutinesDispatcherProvider +import com.owncloud.android.providers.WorkManagerProvider import com.owncloud.android.ui.activity.UploadPathActivity +import com.owncloud.android.utils.FileStorageUtils +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import java.io.File class SettingsVideoUploadsViewModel( - private val preferencesProvider: SharedPreferencesProvider, - private val cameraUploadsHandlerProvider: CameraUploadsHandlerProvider, - private val accountProvider: AccountProvider + private val accountProvider: AccountProvider, + private val saveVideoUploadsConfigurationUseCase: SaveVideoUploadsConfigurationUseCase, + private val getVideoUploadsConfigurationStreamUseCase: GetVideoUploadsConfigurationStreamUseCase, + private val resetVideoUploadsUseCase: ResetVideoUploadsUseCase, + private val workManagerProvider: WorkManagerProvider, + private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, ) : ViewModel() { - fun isVideoUploadEnabled() = - preferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false) + private val _videoUploads: MutableLiveData = MutableLiveData() + val videoUploads: LiveData = _videoUploads - fun setEnableVideoUpload(value: Boolean) { - preferencesProvider.putBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, value) + init { + initVideoUploads() + } + + private fun initVideoUploads() { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + getVideoUploadsConfigurationStreamUseCase.execute(Unit).collect { videoUploadsConfiguration -> + _videoUploads.postValue(videoUploadsConfiguration) + } + } + } - if (value) { - // Use current account as default. It should never be null. If no accounts are attached, video uploads are hidden - accountProvider.getCurrentOwnCloudAccount()?.name?.let { name -> - preferencesProvider.putString( - key = PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME, - value = name + fun enableVideoUploads() { + // Use current account as default. It should never be null. If no accounts are attached, video uploads are hidden + accountProvider.getCurrentOwnCloudAccount()?.name?.let { name -> + viewModelScope.launch(coroutinesDispatcherProvider.io) { + saveVideoUploadsConfigurationUseCase.execute( + SaveVideoUploadsConfigurationUseCase.Params(composeVideoUploadsConfiguration(accountName = name)) ) } - } else { - // Reset fields after disabling the feature - preferencesProvider.removePreference(key = PREF__CAMERA_VIDEO_UPLOADS_PATH) - preferencesProvider.removePreference(key = PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME) } } - fun updateVideosLastSync() = cameraUploadsHandlerProvider.updateVideosLastSync(0) + fun disableVideoUploads() { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + resetVideoUploadsUseCase.execute(Unit) + } + } - fun getVideoUploadsAccount() = preferencesProvider.getString( - key = PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME, - defaultValue = null - ) + fun useWifiOnly(wifiOnly: Boolean) { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + saveVideoUploadsConfigurationUseCase.execute( + SaveVideoUploadsConfigurationUseCase.Params( + composeVideoUploadsConfiguration(wifiOnly = wifiOnly) + ) + ) + } + } + + fun getVideoUploadsAccount() = _videoUploads.value?.accountName fun getLoggedAccountNames(): Array = accountProvider.getLoggedAccounts().map { it.name }.toTypedArray() - fun getVideoUploadsPath() = preferencesProvider.getString( - key = PREF__CAMERA_VIDEO_UPLOADS_PATH, - defaultValue = PREF__CAMERA_UPLOADS_DEFAULT_PATH - ) + fun getVideoUploadsPath() = _videoUploads.value?.uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH - fun getVideoUploadsSourcePath() = preferencesProvider.getString( - key = PREF__CAMERA_VIDEO_UPLOADS_SOURCE, - defaultValue = CameraUploadsConfiguration.getDefaultSourcePath() - ) + fun getVideoUploadsSourcePath(): String? = _videoUploads.value?.sourcePath + + fun getDefaultCameraSourcePath(): String = FileStorageUtils.getDefaultCameraSourcePath() fun handleSelectVideoUploadsPath(data: Intent?) { val folderToUpload = data?.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER) folderToUpload?.remotePath?.let { - preferencesProvider.putString( - key = PREF__CAMERA_VIDEO_UPLOADS_PATH, - value = it - ) + viewModelScope.launch(coroutinesDispatcherProvider.io) { + saveVideoUploadsConfigurationUseCase.execute( + SaveVideoUploadsConfigurationUseCase.Params(composeVideoUploadsConfiguration(uploadPath = it)) + ) + } } } - fun handleSelectVideoUploadsSourcePath(data: Intent?) { - // If the source path has changed, update camera uploads last sync - var previousSourcePath = preferencesProvider.getString( - key = PREF__CAMERA_VIDEO_UPLOADS_SOURCE, - defaultValue = CameraUploadsConfiguration.getDefaultSourcePath() - ) + fun handleSelectAccount(accountName: String) { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + saveVideoUploadsConfigurationUseCase.execute( + SaveVideoUploadsConfigurationUseCase.Params(composeVideoUploadsConfiguration(accountName = accountName)) + ) + } + } - previousSourcePath = previousSourcePath?.trimEnd(File.separatorChar) + fun handleSelectBehaviour(behaviorString: String) { + val behavior = FolderBackUpConfiguration.Behavior.fromString(behaviorString) - if (previousSourcePath != data?.getStringExtra(LocalFolderPickerActivity.EXTRA_PATH)) { - val currentTimeStamp = System.currentTimeMillis() - cameraUploadsHandlerProvider.updateVideosLastSync(currentTimeStamp) + viewModelScope.launch(coroutinesDispatcherProvider.io) { + saveVideoUploadsConfigurationUseCase.execute( + SaveVideoUploadsConfigurationUseCase.Params(composeVideoUploadsConfiguration(behavior = behavior)) + ) } + } - data?.getStringExtra(LocalFolderPickerActivity.EXTRA_PATH)?.let { - preferencesProvider.putString( - key = PREF__CAMERA_VIDEO_UPLOADS_SOURCE, - value = it + fun handleSelectVideoUploadsSourcePath(contentUriForTree: Uri) { + // If the source path has changed, update camera uploads last sync + var previousSourcePath = _videoUploads.value?.sourcePath ?: getDefaultCameraSourcePath() + + previousSourcePath = previousSourcePath.trimEnd(File.separatorChar) + + viewModelScope.launch(coroutinesDispatcherProvider.io) { + saveVideoUploadsConfigurationUseCase.execute( + SaveVideoUploadsConfigurationUseCase.Params( + composeVideoUploadsConfiguration( + sourcePath = contentUriForTree.toString(), + timestamp = System.currentTimeMillis().takeIf { previousSourcePath != contentUriForTree.encodedPath } + ) + ) ) } } - fun scheduleVideoUploadsSyncJob() = cameraUploadsHandlerProvider.scheduleVideoUploadsSyncJob() + fun scheduleVideoUploads() { + workManagerProvider.enqueueCameraUploadsWorker() + } + + private fun composeVideoUploadsConfiguration( + accountName: String? = _videoUploads.value?.accountName, + uploadPath: String? = _videoUploads.value?.uploadPath, + wifiOnly: Boolean? = _videoUploads.value?.wifiOnly, + sourcePath: String? = _videoUploads.value?.sourcePath, + behavior: FolderBackUpConfiguration.Behavior? = _videoUploads.value?.behavior, + timestamp: Long? = _videoUploads.value?.lastSyncTimestamp + ): FolderBackUpConfiguration = + FolderBackUpConfiguration( + accountName = accountName ?: accountProvider.getCurrentOwnCloudAccount()!!.name, + behavior = behavior ?: FolderBackUpConfiguration.Behavior.COPY, + sourcePath = sourcePath.orEmpty(), + uploadPath = uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH, + wifiOnly = wifiOnly ?: false, + lastSyncTimestamp = timestamp ?: System.currentTimeMillis(), + name = _videoUploads.value?.name ?: videoUploadsName + ) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/providers/CameraUploadsHandlerProvider.kt b/owncloudApp/src/main/java/com/owncloud/android/providers/CameraUploadsHandlerProvider.kt deleted file mode 100644 index 488003733f4..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/providers/CameraUploadsHandlerProvider.kt +++ /dev/null @@ -1,75 +0,0 @@ -/** - * ownCloud Android client application - * - * @author Juan Carlos Garrote Gascón - * - * Copyright (C) 2021 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.providers - -import android.content.Context -import com.owncloud.android.data.preferences.datasources.implementation.SharedPreferencesProviderImpl -import com.owncloud.android.db.PreferenceManager -import com.owncloud.android.files.services.CameraUploadsHandler - -class CameraUploadsHandlerProvider( - private val context: Context -) { - private val cameraUploadsHandler = CameraUploadsHandler(PreferenceManager.getCameraUploadsConfiguration(context)) - - fun updatePicturesLastSync(timestamp: Long) = cameraUploadsHandler.updatePicturesLastSync(context, timestamp) - - fun updateVideosLastSync(timestamp: Long) = cameraUploadsHandler.updateVideosLastSync(context, timestamp) - - fun schedulePictureUploadsSyncJob() { - val configuration = PreferenceManager.getCameraUploadsConfiguration(context) - if (configuration.isEnabledForPictures) { - cameraUploadsHandler.setCameraUploadsConfig(configuration) - cameraUploadsHandler.scheduleCameraUploadsSyncJob(context) - } - } - - fun scheduleVideoUploadsSyncJob() { - val configuration = PreferenceManager.getCameraUploadsConfiguration(context) - if (configuration.isEnabledForVideos) { - cameraUploadsHandler.setCameraUploadsConfig(configuration) - cameraUploadsHandler.scheduleCameraUploadsSyncJob(context) - } - } - - fun hasCameraUploadsAttached(accountName: String): Boolean { - val cameraUploadsConfiguration = PreferenceManager.getCameraUploadsConfiguration(context) - - return accountName == cameraUploadsConfiguration.uploadAccountNameForPictures || - accountName == cameraUploadsConfiguration.uploadAccountNameForVideos - } - - fun resetCameraUploadsForAccount(accountName: String) { - val preferencesProvider = SharedPreferencesProviderImpl(context) - val cameraUploadsConfiguration = PreferenceManager.getCameraUploadsConfiguration(context) - - if (accountName == cameraUploadsConfiguration.uploadAccountNameForPictures) { - preferencesProvider.putBoolean(PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false) - preferencesProvider.removePreference(key = PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_PATH) - preferencesProvider.removePreference(key = PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME) - } - if (accountName == cameraUploadsConfiguration.uploadAccountNameForVideos) { - preferencesProvider.putBoolean(PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false) - preferencesProvider.removePreference(key = PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_PATH) - preferencesProvider.removePreference(key = PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME) - } - } -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/providers/FileContentProvider.kt b/owncloudApp/src/main/java/com/owncloud/android/providers/FileContentProvider.kt index 0f431802560..aa0f46fdc87 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/providers/FileContentProvider.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/providers/FileContentProvider.kt @@ -54,6 +54,9 @@ import com.owncloud.android.data.ProviderMeta import com.owncloud.android.data.capabilities.datasources.implementation.OCLocalCapabilitiesDataSource import com.owncloud.android.data.capabilities.datasources.implementation.OCLocalCapabilitiesDataSource.Companion.toModel import com.owncloud.android.data.capabilities.db.OCCapabilityEntity +import com.owncloud.android.data.folderbackup.datasources.FolderBackupLocalDataSource +import com.owncloud.android.data.migrations.CameraUploadsMigrationToRoom +import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider import com.owncloud.android.data.sharing.shares.db.OCShareEntity import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.UploadsStorageManager @@ -994,6 +997,47 @@ class FileContentProvider(val executors: Executors = Executors()) : ContentProvi } } + if (oldVersion < 34 && newVersion >= 34) { + Timber.i("SQL : Entering in the #34 Migrate old camera uploads configuration to room database") + db.beginTransaction() + + var pictureUploadsTimestamp: Long = 0 + var videoUploadsTimestamp: Long = 0 + + try { + val cursor = db.query(ProviderTableMeta.CAMERA_UPLOADS_SYNC_TABLE_NAME, null, null, null, null, null, null) + + if (cursor.moveToFirst()) { + pictureUploadsTimestamp = cursor.getLong(cursor.getColumnIndex(ProviderTableMeta.PICTURES_LAST_SYNC_TIMESTAMP)) + videoUploadsTimestamp = cursor.getLong(cursor.getColumnIndex(ProviderTableMeta.VIDEOS_LAST_SYNC_TIMESTAMP)) + } + + val sharedPreferencesProvider: SharedPreferencesProvider by inject() + val migrationToRoom = CameraUploadsMigrationToRoom(sharedPreferencesProvider) + + val pictureUploadsConfiguration = migrationToRoom.getPictureUploadsConfigurationPreferences(pictureUploadsTimestamp) + val videoUploadsConfiguration = migrationToRoom.getVideoUploadsConfigurationPreferences(videoUploadsTimestamp) + + val backupLocalDataSource: FolderBackupLocalDataSource by inject() + // Insert camera uploads configuration in new database + executors.diskIO().execute { + pictureUploadsConfiguration?.let { backupLocalDataSource.saveFolderBackupConfiguration(it) } + videoUploadsConfiguration?.let { backupLocalDataSource.saveFolderBackupConfiguration(it) } + if (pictureUploadsConfiguration != null || videoUploadsConfiguration != null) { + val workManagerProvider: WorkManagerProvider by inject() + workManagerProvider.enqueueCameraUploadsWorker() + } + } + cursor.close() + // Drop camera uploads timestamps from old database + db.execSQL("DROP TABLE IF EXISTS " + ProviderTableMeta.CAMERA_UPLOADS_SYNC_TABLE_NAME + ";") + db.setTransactionSuccessful() + upgraded = true + } finally { + db.endTransaction() + } + } + if (!upgraded) { Timber.i("SQL : OUT of the ADD in onUpgrade; oldVersion == $oldVersion, newVersion == $newVersion") } diff --git a/owncloudApp/src/main/java/com/owncloud/android/providers/WorkManagerProvider.kt b/owncloudApp/src/main/java/com/owncloud/android/providers/WorkManagerProvider.kt new file mode 100644 index 00000000000..d3b6b4a3435 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/providers/WorkManagerProvider.kt @@ -0,0 +1,41 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.providers + +import android.content.Context +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import com.owncloud.android.workers.CameraUploadsWorker + +class WorkManagerProvider( + val context: Context +) { + fun enqueueCameraUploadsWorker() { + val cameraUploadsWorker = PeriodicWorkRequestBuilder( + repeatInterval = CameraUploadsWorker.repeatInterval, + repeatIntervalTimeUnit = CameraUploadsWorker.repeatIntervalTimeUnit + ).addTag(CameraUploadsWorker.CAMERA_UPLOADS_WORKER) + .build() + + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork(CameraUploadsWorker.CAMERA_UPLOADS_WORKER, ExistingPeriodicWorkPolicy.KEEP, cameraUploadsWorker) + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/syncadapter/FileSyncAdapter.java b/owncloudApp/src/main/java/com/owncloud/android/syncadapter/FileSyncAdapter.java index b9000e9d0c7..2754dec7ebc 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/syncadapter/FileSyncAdapter.java +++ b/owncloudApp/src/main/java/com/owncloud/android/syncadapter/FileSyncAdapter.java @@ -45,8 +45,6 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; import com.owncloud.android.operations.SyncCapabilitiesOperation; import com.owncloud.android.operations.SynchronizeFolderOperation; -import com.owncloud.android.presentation.ui.authentication.AuthenticatorConstants; -import com.owncloud.android.presentation.ui.authentication.LoginActivity; import com.owncloud.android.ui.activity.ErrorsWhileCopyingHandlerActivity; import com.owncloud.android.utils.NotificationUtils; import timber.log.Timber; @@ -423,20 +421,13 @@ private void notifyFailedSynchronization() { ); if (needsToUpdateCredentials) { // let the user update credentials with one click - Intent updateAccountCredentials = new Intent(getContext(), LoginActivity.class); - updateAccountCredentials.putExtra(AuthenticatorConstants.EXTRA_ACCOUNT, getAccount()); - updateAccountCredentials.putExtra(AuthenticatorConstants.EXTRA_ACTION, - AuthenticatorConstants.ACTION_UPDATE_EXPIRED_TOKEN); - updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND); + PendingIntent pendingIntentToRefreshCredentials = + NotificationUtils.INSTANCE.composePendingIntentToRefreshCredentials(getContext(), getAccount()); + notificationBuilder .setTicker(i18n(R.string.sync_fail_ticker_unauthorized)) .setContentTitle(i18n(R.string.sync_fail_ticker_unauthorized)) - .setContentIntent(PendingIntent.getActivity( - getContext(), (int) System.currentTimeMillis(), updateAccountCredentials, - PendingIntent.FLAG_ONE_SHOT - )) + .setContentIntent(pendingIntentToRefreshCredentials) .setContentText(i18n(R.string.sync_fail_content_unauthorized, getAccount().name)); } else { notificationBuilder diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/LocalFolderPickerActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/LocalFolderPickerActivity.java deleted file mode 100644 index b1ca96ef462..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/LocalFolderPickerActivity.java +++ /dev/null @@ -1,259 +0,0 @@ -/** - * ownCloud Android client application - * - * @author David A. Velasco - * @author Christian Schabesberger - * @author David González Verdugo - * @author Shashvat Kedia - * Copyright (C) 2020 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.ui.activity; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.os.Environment; -import android.view.MenuItem; -import android.view.View; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.LinearLayout; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; -import com.owncloud.android.R; -import com.owncloud.android.ui.fragment.LocalFileListFragment; -import com.owncloud.android.utils.PreferenceUtils; -import timber.log.Timber; - -import java.io.File; - -/** - * Displays local folders and let the user choose one of them, which path is set as result. - */ - -public class LocalFolderPickerActivity extends ToolbarActivity implements LocalFileListFragment.ContainerActivity { - - public static final String EXTRA_PATH = LocalFolderPickerActivity.class.getCanonicalName() + ".PATH"; - - private static final String FTAG_LIST_OF_FOLDERS = "LIST_OF_FOLDERS"; - - private File mCurrentFolder = null; - - protected Button mCancelBtn; - protected Button mChooseBtn; - protected ImageButton mHomeBtn; - - /** - * Helper to launch a {@link LocalFolderPickerActivity} for which you would like a result when finished. - * Your onActivityResult() method will be called with the given requestCode. - * - * @param activity Activity calling {@link LocalFolderPickerActivity} for a result. - * @param startPath Absolute path to the local folder to show when the activity is shown. - * @param requestCode If >= 0, this code will be returned in onActivityResult(). - */ - public static void startLocalFolderPickerActivityForResult( - Activity activity, - String startPath, - int requestCode - ) { - Intent action = new Intent(activity, LocalFolderPickerActivity.class); - action.putExtra(LocalFolderPickerActivity.EXTRA_PATH, startPath); - activity.startActivityForResult(action, requestCode); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - Timber.v("onCreate() start"); - super.onCreate(savedInstanceState); - - // set current folder - String startPath = (savedInstanceState != null) ? - savedInstanceState.getString(LocalFolderPickerActivity.EXTRA_PATH) : - getIntent().getStringExtra(EXTRA_PATH); - if (startPath != null) { - mCurrentFolder = new File(startPath); - } - if (mCurrentFolder == null || !mCurrentFolder.exists()) { - mCurrentFolder = Environment.getExternalStorageDirectory(); // default - } else if (!mCurrentFolder.isDirectory()) { - mCurrentFolder = mCurrentFolder.getParentFile(); - } - - // inflate and set the layout view - setContentView(R.layout.files_folder_picker); // beware - inflated in other activities too - - // Allow or disallow touches with other visible windows - LinearLayout filesFolderPickerLayout = findViewById(R.id.filesFolderPickerLayout); - filesFolderPickerLayout.setFilterTouchesWhenObscured( - PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(this) - ); - - if (savedInstanceState == null) { - createFragments(); - } - - // set input controllers - mCancelBtn = findViewById(R.id.folder_picker_btn_cancel); - mCancelBtn.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - setResult(RESULT_CANCELED); - finish(); - } - }); - mChooseBtn = findViewById(R.id.folder_picker_btn_choose); - mChooseBtn.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - // return the path of the current folder - Intent data = new Intent(); - data.putExtra(EXTRA_PATH, mCurrentFolder.getAbsolutePath()); - setResult(RESULT_OK, data); - finish(); - } - }); - mHomeBtn = findViewById(R.id.folder_picker_btn_home); - mHomeBtn.setVisibility(View.VISIBLE); - mHomeBtn.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mCurrentFolder = Environment.getExternalStorageDirectory(); - getListFragment().listFolder(mCurrentFolder); - updateActionBar(); - } - }); - - // init toolbar - setupStandardToolbar(null, true, true, true); - updateActionBar(); - - Timber.v("onCreate() end"); - } - - private void createFragments() { - LocalFileListFragment listOfFiles = LocalFileListFragment.newInstance(true); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.add(R.id.fragment_container, listOfFiles, FTAG_LIST_OF_FOLDERS); - transaction.commit(); - } - - /** - * Updates contents shown by action bar. - */ - private void updateActionBar() { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - boolean mayBrowseUp = mayBrowseUp(); - updateStandardToolbar( - mayBrowseUp ? mCurrentFolder.getName() : File.separator, - mayBrowseUp, - mayBrowseUp - ); - } else { - Timber.w("Action bar missing in action"); - } - } - - /** - * Handles presses on 'Up' button, not exactly the same as 'BACK' button. - * - * @param item Action in option menu tapped by the user. - * @return 'true' if consumed, 'false' otherwise. - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - boolean retval = true; - switch (item.getItemId()) { - case android.R.id.home: { - if (mayBrowseUp()) { - onBackPressed(); - } - break; - } - default: - retval = super.onOptionsItemSelected(item); - } - return retval; - } - - /** - * Handles presses on 'BACK' button. - */ - @Override - public void onBackPressed() { - if (!mayBrowseUp()) { - finish(); - return; - } - LocalFileListFragment listFragment = getListFragment(); - if (listFragment != null) { - listFragment.browseUp(); - mCurrentFolder = listFragment.getCurrentFolder(); - updateActionBar(); - } else { - Timber.e("List of files not found - cannot browse up"); - } - } - - /** - * @return 'true' when browsing to the parent folder is possible, 'false' otherwise - */ - private boolean mayBrowseUp() { - return (mCurrentFolder != null && mCurrentFolder.getParentFile() != null); - } - - /** - * {@inheritDoc} - */ - @Override - protected void onSaveInstanceState(Bundle outState) { - Timber.v("onSaveInstanceState() start"); - super.onSaveInstanceState(outState); - outState.putString(LocalFolderPickerActivity.EXTRA_PATH, mCurrentFolder.getAbsolutePath()); - Timber.v("onSaveInstanceState() end"); - } - - /** - * {@inheritDoc} - */ - @Override - public void onFolderClicked(File folder) { - if (folder.isDirectory()) { - mCurrentFolder = folder; - } - updateActionBar(); - } - - @Override - public File getCurrentFolder() { - return mCurrentFolder; - } - - @Nullable - protected LocalFileListFragment getListFragment() { - Fragment listOfFiles = getSupportFragmentManager().findFragmentByTag( - FTAG_LIST_OF_FOLDERS - ); - if (listOfFiles != null) { - return (LocalFileListFragment) listOfFiles; - } - return null; - } - -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java index 0748ccb1951..f4ae06225b4 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java @@ -42,28 +42,37 @@ import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; +import androidx.work.WorkManager; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.authentication.AccountUtils; import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.files.services.CameraUploadsHandler; +import com.owncloud.android.domain.UseCaseResult; +import com.owncloud.android.domain.camerauploads.model.CameraUploadsConfiguration; +import com.owncloud.android.domain.camerauploads.usecases.GetCameraUploadsConfigurationUseCase; import com.owncloud.android.files.services.FileDownloader; import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.presentation.ui.authentication.AuthenticatorConstants; import com.owncloud.android.presentation.ui.authentication.LoginActivity; -import com.owncloud.android.providers.CameraUploadsHandlerProvider; import com.owncloud.android.services.OperationsService; import com.owncloud.android.ui.adapter.AccountListAdapter; import com.owncloud.android.ui.adapter.AccountListItem; import com.owncloud.android.ui.dialog.RemoveAccountDialogFragment; +import com.owncloud.android.ui.dialog.RemoveAccountDialogViewModel; import com.owncloud.android.ui.helpers.FileOperationsHelper; +import com.owncloud.android.usecases.CancelUploadFromAccountUseCase; import com.owncloud.android.utils.PreferenceUtils; +import kotlin.Lazy; +import kotlin.Unit; +import org.jetbrains.annotations.NotNull; import timber.log.Timber; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; +import static org.koin.java.KoinJavaComponent.inject; + /** * An Activity that allows the user to manage accounts. */ @@ -87,9 +96,13 @@ public class ManageAccountsActivity extends FileActivity String mOriginalCurrentAccount; private Drawable mTintedCheck; + private RemoveAccountDialogViewModel mRemoveAccountDialogViewModel = null; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + @NotNull Lazy removeAccountDialogViewModelLazy = inject(RemoveAccountDialogViewModel.class); + mRemoveAccountDialogViewModel = removeAccountDialogViewModelLazy.getValue(); mTintedCheck = ContextCompat.getDrawable(this, R.drawable.ic_current_white); mTintedCheck = DrawableCompat.wrap(mTintedCheck); @@ -228,10 +241,9 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void removeAccount(Account account) { mAccountBeingRemoved = account.name; - CameraUploadsHandlerProvider cameraUploadsHandlerProvider = new CameraUploadsHandlerProvider(this); RemoveAccountDialogFragment dialog = RemoveAccountDialogFragment.newInstance( account, - cameraUploadsHandlerProvider.hasCameraUploadsAttached(account.name) + mRemoveAccountDialogViewModel.hasCameraUploadsAttached(account.name) ); dialog.show(getSupportFragmentManager(), RemoveAccountDialogFragment.FTAG_CONFIRMATION); } @@ -320,6 +332,8 @@ public void run(AccountManagerFuture future) { if (mDownloaderBinder != null) { mDownloaderBinder.cancel(account); } + CancelUploadFromAccountUseCase cancelUploadFromAccountUseCase = new CancelUploadFromAccountUseCase(WorkManager.getInstance(getBaseContext())); + cancelUploadFromAccountUseCase.execute(new CancelUploadFromAccountUseCase.Params(account.name)); } mAccountListAdapter = new AccountListAdapter(this, getAccountListItems(), mTintedCheck); diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/adapter/ExpandableUploadListAdapter.java b/owncloudApp/src/main/java/com/owncloud/android/ui/adapter/ExpandableUploadListAdapter.java index a567bc0da05..62d09af99b5 100755 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/adapter/ExpandableUploadListAdapter.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/adapter/ExpandableUploadListAdapter.java @@ -25,6 +25,7 @@ import android.content.Context; import android.database.DataSetObserver; import android.graphics.Bitmap; +import android.net.Uri; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; @@ -38,6 +39,8 @@ import android.widget.TextView; import androidx.appcompat.widget.AppCompatButton; +import androidx.documentfile.provider.DocumentFile; +import androidx.work.WorkManager; import com.google.android.material.snackbar.Snackbar; import com.owncloud.android.R; import com.owncloud.android.authentication.AccountUtils; @@ -54,6 +57,7 @@ import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.fragment.OptionsInUploadListClickListener; import com.owncloud.android.ui.fragment.UploadListFragment; +import com.owncloud.android.usecases.CancelUploadWithIdUseCase; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.MimetypeIconUtil; import com.owncloud.android.utils.PreferenceUtils; @@ -347,11 +351,19 @@ private View getView(OCUpload[] uploadsItems, int position, View convertView, Vi rightButton.setImageResource(R.drawable.ic_action_cancel_grey); rightButton.setVisibility(View.VISIBLE); rightButton.setOnClickListener(v -> { - FileUploader.FileUploaderBinder uploaderBinder = mParentActivity.getFileUploaderBinder(); - if (uploaderBinder != null) { - uploaderBinder.cancel(upload); - refreshView(); + String localPath = upload.getLocalPath(); + boolean isDocumentUri = DocumentFile.isDocumentUri(parent.getContext(), Uri.parse(localPath)); + if (isDocumentUri) { + CancelUploadWithIdUseCase cancelUploadWithIdUseCase = new CancelUploadWithIdUseCase(WorkManager.getInstance(parent.getContext())); + cancelUploadWithIdUseCase.execute(new CancelUploadWithIdUseCase.Params(upload)); + } else { + FileUploader.FileUploaderBinder uploaderBinder = mParentActivity.getFileUploaderBinder(); + + if (uploaderBinder != null) { + uploaderBinder.cancel(upload); + } } + refreshView(); }); } else if (upload.getUploadStatus() == UploadStatus.UPLOAD_FAILED) { diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RemoveAccountDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RemoveAccountDialogFragment.kt index 1800ebb8cac..5b4c6e1de95 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RemoveAccountDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RemoveAccountDialogFragment.kt @@ -29,8 +29,8 @@ import android.os.Bundle import android.os.Handler import android.provider.DocumentsContract import com.owncloud.android.R -import com.owncloud.android.providers.CameraUploadsHandlerProvider import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener +import org.koin.java.KoinJavaComponent.inject /** * Dialog requiring confirmation before removing an OC Account. @@ -40,6 +40,7 @@ import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDia * Container Activity needs to implement AccountManagerCallback. */ class RemoveAccountDialogFragment : ConfirmationDialogFragment(), ConfirmationDialogFragmentListener { + val viewModel: RemoveAccountDialogViewModel by inject(RemoveAccountDialogViewModel::class.java) private var targetAccount: Account? = null @@ -72,8 +73,7 @@ class RemoveAccountDialogFragment : ConfirmationDialogFragment(), ConfirmationDi am.removeAccount(targetAccount, callback, Handler()) // Reset camera uploads if they were enabled for this account - val cameraUploadsHandlerProvider = CameraUploadsHandlerProvider(requireContext()) - cameraUploadsHandlerProvider.resetCameraUploadsForAccount(targetAccount!!.name) + viewModel.resetCameraUploadsForAccount(targetAccount!!.name) // Notify removal to Document Provider val authority = resources.getString(R.string.document_provider_authority) diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RemoveAccountDialogViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RemoveAccountDialogViewModel.kt new file mode 100644 index 00000000000..09ec3c0ba07 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RemoveAccountDialogViewModel.kt @@ -0,0 +1,68 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.ui.dialog + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.owncloud.android.domain.camerauploads.model.CameraUploadsConfiguration +import com.owncloud.android.domain.camerauploads.usecases.GetCameraUploadsConfigurationUseCase +import com.owncloud.android.domain.camerauploads.usecases.ResetPictureUploadsUseCase +import com.owncloud.android.domain.camerauploads.usecases.ResetVideoUploadsUseCase +import com.owncloud.android.providers.CoroutinesDispatcherProvider +import kotlinx.coroutines.launch + +class RemoveAccountDialogViewModel( + private val getCameraUploadsConfigurationUseCase: GetCameraUploadsConfigurationUseCase, + private val resetPictureUploadsUseCase: ResetPictureUploadsUseCase, + private val resetVideoUploadsUseCase: ResetVideoUploadsUseCase, + private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, +) : ViewModel() { + + init { + initCameraUploadsConfiguration() + } + + private var cameraUploadsConfiguration: CameraUploadsConfiguration? = null + + private fun initCameraUploadsConfiguration() { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + cameraUploadsConfiguration = getCameraUploadsConfigurationUseCase.execute(Unit).getDataOrNull() + } + } + + fun hasCameraUploadsAttached(accountName: String): Boolean { + return accountName == cameraUploadsConfiguration?.pictureUploadsConfiguration?.accountName || + accountName == cameraUploadsConfiguration?.videoUploadsConfiguration?.accountName + } + + fun resetCameraUploadsForAccount(accountName: String) { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + val cameraUploadsConfiguration = getCameraUploadsConfigurationUseCase.execute(Unit) + + if (accountName == cameraUploadsConfiguration.getDataOrNull()?.pictureUploadsConfiguration?.accountName) { + resetPictureUploadsUseCase.execute(Unit) + } + if (accountName == cameraUploadsConfiguration.getDataOrNull()?.videoUploadsConfiguration?.accountName) { + resetVideoUploadsUseCase.execute(Unit) + } + } + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadFromAccountUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadFromAccountUseCase.kt new file mode 100644 index 00000000000..503e24ffc44 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadFromAccountUseCase.kt @@ -0,0 +1,43 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.usecases + +import androidx.work.WorkManager +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.domain.BaseUseCase +import timber.log.Timber + +class CancelUploadFromAccountUseCase( + private val workManager: WorkManager +) : BaseUseCase() { + + override fun run(params: Params) { + workManager.cancelAllWorkByTag(params.accountName) + + val uploadsStorageManager = UploadsStorageManager(MainApp.appContext.contentResolver) + uploadsStorageManager.removeUploads(params.accountName) + + Timber.i("Uploads of ${params.accountName} has been cancelled.") + } + + data class Params( + val accountName: String, + ) +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadWithIdUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadWithIdUseCase.kt new file mode 100644 index 00000000000..1740befac41 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/CancelUploadWithIdUseCase.kt @@ -0,0 +1,44 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.usecases + +import androidx.work.WorkManager +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.OCUpload +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.domain.BaseUseCase +import timber.log.Timber + +class CancelUploadWithIdUseCase( + private val workManager: WorkManager +) : BaseUseCase() { + + override fun run(params: Params) { + workManager.cancelAllWorkByTag(params.upload.uploadId.toString()) + + val uploadsStorageManager = UploadsStorageManager(MainApp.appContext.contentResolver) + uploadsStorageManager.removeUpload(params.upload) + + Timber.i("Upload with id ${params.upload.uploadId} has been cancelled.") + } + + data class Params( + val upload: OCUpload, + ) +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFileFromContentUriUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFileFromContentUriUseCase.kt new file mode 100644 index 00000000000..dbeef88270a --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/UploadFileFromContentUriUseCase.kt @@ -0,0 +1,71 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.usecases + +import android.net.Uri +import androidx.work.Constraints +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.workDataOf +import com.owncloud.android.domain.BaseUseCase +import com.owncloud.android.workers.UploadFileFromContentUriWorker +import timber.log.Timber + +class UploadFileFromContentUriUseCase( + private val workManager: WorkManager +) : BaseUseCase() { + + override fun run(params: Params) { + val inputData = workDataOf( + UploadFileFromContentUriWorker.KEY_PARAM_ACCOUNT_NAME to params.accountName, + UploadFileFromContentUriWorker.KEY_PARAM_BEHAVIOR to params.behavior, + UploadFileFromContentUriWorker.KEY_PARAM_CONTENT_URI to params.contentUri.toString(), + UploadFileFromContentUriWorker.KEY_PARAM_LAST_MODIFIED to params.lastModifiedInSeconds, + UploadFileFromContentUriWorker.KEY_PARAM_UPLOAD_PATH to params.uploadPath, + UploadFileFromContentUriWorker.KEY_PARAM_UPLOAD_ID to params.uploadIdInStorageManager + ) + + val networkRequired = if (params.wifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED + val constraints = Constraints.Builder() + .setRequiredNetworkType(networkRequired) + .build() + + val uploadFileFromContentUriWorker = OneTimeWorkRequestBuilder() + .setInputData(inputData) + .setConstraints(constraints) + .addTag(params.accountName) + .addTag(params.uploadIdInStorageManager.toString()) + .addTag(UploadFileFromContentUriWorker.TRANSFER_TAG_CAMERA_UPLOAD) + .build() + + workManager.enqueue(uploadFileFromContentUriWorker) + Timber.i("Upload of ${params.contentUri.path} has been enqueued.") + } + + data class Params( + val accountName: String, + val contentUri: Uri, + val lastModifiedInSeconds: String, + val behavior: String, + val uploadPath: String, + val uploadIdInStorageManager: Long, + val wifiOnly: Boolean + ) +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/utils/FileStorageUtils.java b/owncloudApp/src/main/java/com/owncloud/android/utils/FileStorageUtils.java index 99112ee2272..1a2567d4fd4 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/utils/FileStorageUtils.java +++ b/owncloudApp/src/main/java/com/owncloud/android/utils/FileStorageUtils.java @@ -47,7 +47,6 @@ public class FileStorageUtils { public static final int SORT_DATE = 1; public static final int SORT_SIZE = 2; public static final int FILE_DISPLAY_SORT = 3; - public static final String CAMERA_FOLDER = "/Camera"; public static Integer mSortOrderFileDisp = SORT_NAME; public static Boolean mSortAscendingFileDisp = true; @@ -239,6 +238,6 @@ public static boolean deleteDir(File dir) { } public static String getDefaultCameraSourcePath() { - return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + CAMERA_FOLDER; + return getLocalStorageProvider().getDefaultCameraSourcePath(); } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/utils/NotificationUtils.kt b/owncloudApp/src/main/java/com/owncloud/android/utils/NotificationUtils.kt index 5948f6030c8..6a9ed83ee5e 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/utils/NotificationUtils.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/utils/NotificationUtils.kt @@ -26,6 +26,8 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.os.Build +import android.os.Build.VERSION.SDK_INT import android.os.Handler import android.os.HandlerThread import android.os.Process @@ -33,7 +35,14 @@ import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.presentation.ui.authentication.ACTION_UPDATE_EXPIRED_TOKEN +import com.owncloud.android.presentation.ui.authentication.EXTRA_ACCOUNT +import com.owncloud.android.presentation.ui.authentication.EXTRA_ACTION +import com.owncloud.android.presentation.ui.authentication.LoginActivity +import com.owncloud.android.presentation.ui.settings.SettingsActivity +import com.owncloud.android.presentation.ui.settings.SettingsActivity.Companion.KEY_NOTIFICATION_INTENT import com.owncloud.android.ui.activity.ConflictsResolveActivity +import com.owncloud.android.ui.activity.UploadListActivity import java.util.Random object NotificationUtils { @@ -46,6 +55,80 @@ object NotificationUtils { } } + fun createBasicNotification( + context: Context, + contentTitle: String, + contentText: String, + notificationChannelId: String, + notificationId: Int, + intent: PendingIntent?, + onGoing: Boolean = false, + timeOut: Long?, + ) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + val notificationBuilder = newNotificationBuilder(context, notificationChannelId).apply { + setContentTitle(contentTitle) + color = ContextCompat.getColor(context, R.color.primary) + setSmallIcon(R.drawable.notification_icon) + setWhen(System.currentTimeMillis()) + setContentText(contentText) + setOngoing(onGoing) + setAutoCancel(true) + } + + intent?.let { + notificationBuilder.setContentIntent(it) + } + + timeOut?.let { + // [setTimeoutAfter] was introduced in API 26. + // https://developer.android.com/reference/android/app/Notification.Builder#setTimeoutAfter(long) + notificationBuilder.setTimeoutAfter(it) + } + + notificationManager.notify(notificationId, notificationBuilder.build()) + + // Remove success notification for devices with API < 26 with a workaround + if (SDK_INT < Build.VERSION_CODES.O && timeOut != null) { + cancelWithDelay( + notificationManager = notificationManager, + notificationId = notificationId, + delayInMillis = timeOut + ) + } + } + + fun composePendingIntentToRefreshCredentials(context: Context, account: Account): PendingIntent { + val updateCredentialsIntent = + Intent(context, LoginActivity::class.java).apply { + putExtra(EXTRA_ACCOUNT, account) + putExtra(EXTRA_ACTION, ACTION_UPDATE_EXPIRED_TOKEN) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + addFlags(Intent.FLAG_FROM_BACKGROUND) + } + + return PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), updateCredentialsIntent, PendingIntent.FLAG_ONE_SHOT) + } + + fun composePendingIntentToUploadList(context: Context): PendingIntent { + val showUploadListIntent = Intent(context, UploadListActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + + return PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), showUploadListIntent, 0) + } + + fun composePendingIntentToCameraUploads(context: Context, notificationKey: String): PendingIntent { + val openSettingsActivity = Intent(context, SettingsActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + putExtra(KEY_NOTIFICATION_INTENT, notificationKey) + } + + return PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), openSettingsActivity, 0) + } + @JvmStatic fun cancelWithDelay( notificationManager: NotificationManager, diff --git a/owncloudApp/src/main/java/com/owncloud/android/workers/CameraUploadsWorker.kt b/owncloudApp/src/main/java/com/owncloud/android/workers/CameraUploadsWorker.kt new file mode 100644 index 00000000000..cc73406de07 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/workers/CameraUploadsWorker.kt @@ -0,0 +1,294 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.workers + +import android.content.Context +import android.net.Uri +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import androidx.work.CoroutineWorker +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCUpload +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus +import com.owncloud.android.domain.UseCaseResult +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration +import com.owncloud.android.domain.camerauploads.usecases.GetCameraUploadsConfigurationUseCase +import com.owncloud.android.domain.camerauploads.usecases.SavePictureUploadsConfigurationUseCase +import com.owncloud.android.domain.camerauploads.usecases.SaveVideoUploadsConfigurationUseCase +import com.owncloud.android.operations.UploadFileOperation.CREATED_AS_CAMERA_UPLOAD_PICTURE +import com.owncloud.android.operations.UploadFileOperation.CREATED_AS_CAMERA_UPLOAD_VIDEO +import com.owncloud.android.presentation.ui.settings.SettingsActivity +import com.owncloud.android.usecases.UploadFileFromContentUriUseCase +import com.owncloud.android.utils.MimetypeIconUtil +import com.owncloud.android.utils.NotificationUtils +import com.owncloud.android.utils.UPLOAD_NOTIFICATION_CHANNEL_ID +import org.koin.core.KoinComponent +import org.koin.core.inject +import timber.log.Timber +import java.io.File +import java.util.Date +import java.util.concurrent.TimeUnit + +class CameraUploadsWorker( + val appContext: Context, + workerParameters: WorkerParameters +) : CoroutineWorker( + appContext, + workerParameters +), KoinComponent { + + enum class SyncType(val prefixForType: String) { + PICTURE_UPLOADS("image/"), VIDEO_UPLOADS("video/"); + + fun getNotificationId(): Int = + when (this) { + PICTURE_UPLOADS -> pictureUploadsNotificationId + VIDEO_UPLOADS -> videoUploadsNotificationId + } + } + + private val getCameraUploadsConfigurationUseCase: GetCameraUploadsConfigurationUseCase by inject() + + override suspend fun doWork(): Result { + + when (val useCaseResult = getCameraUploadsConfigurationUseCase.execute(Unit)) { + is UseCaseResult.Success -> { + val cameraUploadsConfiguration = useCaseResult.data + if (cameraUploadsConfiguration == null || cameraUploadsConfiguration.areCameraUploadsDisabled()) { + cancelWorker() + return Result.success() + } + cameraUploadsConfiguration.pictureUploadsConfiguration?.let { pictureUploadsConfiguration -> + try { + checkSourcePathIsAValidUriOrThrowException(pictureUploadsConfiguration.sourcePath) + syncFolder(pictureUploadsConfiguration) + } catch (illegalArgumentException: IllegalArgumentException) { + showNotificationToUpdateUri(SyncType.PICTURE_UPLOADS) + return Result.failure() + } + } + cameraUploadsConfiguration.videoUploadsConfiguration?.let { videoUploadsConfiguration -> + try { + checkSourcePathIsAValidUriOrThrowException(videoUploadsConfiguration.sourcePath) + syncFolder(videoUploadsConfiguration) + } catch (illegalArgumentException: IllegalArgumentException) { + showNotificationToUpdateUri(SyncType.VIDEO_UPLOADS) + return Result.failure() + } + } + } + is UseCaseResult.Error -> { + Timber.e(useCaseResult.throwable, "Worker ${useCaseResult.throwable}") + } + } + return Result.success() + } + + @Throws(IllegalArgumentException::class) + private fun checkSourcePathIsAValidUriOrThrowException(sourcePath: String) { + val sourceUri: Uri = sourcePath.toUri() + DocumentFile.fromTreeUri(applicationContext, sourceUri) + } + + private fun cancelWorker() { + WorkManager.getInstance(appContext).cancelUniqueWork(CAMERA_UPLOADS_WORKER) + } + + private fun syncFolder(folderBackUpConfiguration: FolderBackUpConfiguration?) { + if (folderBackUpConfiguration == null) return + + val syncType = when { + folderBackUpConfiguration.isPictureUploads -> SyncType.PICTURE_UPLOADS + folderBackUpConfiguration.isVideoUploads -> SyncType.VIDEO_UPLOADS + // Else should not happen for the moment. Maybe in upcoming features.. + else -> SyncType.PICTURE_UPLOADS + } + + val localPicturesDocumentFiles: List = getFilesReadyToUpload( + syncType = syncType, + sourcePath = folderBackUpConfiguration.sourcePath, + lastSyncTimestamp = folderBackUpConfiguration.lastSyncTimestamp + ) + + showNotification(syncType, localPicturesDocumentFiles.size) + + for (documentFile in localPicturesDocumentFiles) { + val uploadId = storeInUploadsDatabase( + documentFile = documentFile, + uploadPath = folderBackUpConfiguration.uploadPath.plus(File.separator).plus(documentFile.name), + accountName = folderBackUpConfiguration.accountName, + behavior = folderBackUpConfiguration.behavior, + createdByWorker = when (syncType) { + SyncType.PICTURE_UPLOADS -> CREATED_AS_CAMERA_UPLOAD_PICTURE + SyncType.VIDEO_UPLOADS -> CREATED_AS_CAMERA_UPLOAD_VIDEO + } + ) + enqueueSingleUpload( + contentUri = documentFile.uri, + uploadPath = folderBackUpConfiguration.uploadPath.plus(File.separator).plus(documentFile.name), + lastModified = documentFile.lastModified(), + behavior = folderBackUpConfiguration.behavior.toString(), + accountName = folderBackUpConfiguration.accountName, + uploadId = uploadId, + wifiOnly = folderBackUpConfiguration.wifiOnly + ) + } + updateTimestamp(folderBackUpConfiguration, syncType) + } + + private fun showNotification( + syncType: SyncType, + numberOfFilesToUpload: Int + ) { + if (numberOfFilesToUpload == 0) return + + val contentText = when (syncType) { + SyncType.PICTURE_UPLOADS -> R.string.uploader_upload_picture_upload_files + SyncType.VIDEO_UPLOADS -> R.string.uploader_upload_video_upload_files + } + + NotificationUtils.createBasicNotification( + context = appContext, + contentTitle = appContext.getString(R.string.uploader_upload_camera_upload_files), + contentText = appContext.getString(contentText, numberOfFilesToUpload), + notificationChannelId = UPLOAD_NOTIFICATION_CHANNEL_ID, + notificationId = syncType.getNotificationId(), + intent = NotificationUtils.composePendingIntentToUploadList(appContext), + onGoing = false, + timeOut = 5_000 + ) + } + + private fun showNotificationToUpdateUri( + syncType: SyncType + ) { + val contentText: Int = when (syncType) { + SyncType.PICTURE_UPLOADS -> R.string.uploader_upload_picture_upload_error + SyncType.VIDEO_UPLOADS -> R.string.uploader_upload_video_upload_error + } + val notificationKey: String = when (syncType) { + SyncType.PICTURE_UPLOADS -> SettingsActivity.NOTIFICATION_INTENT_PICTURES + SyncType.VIDEO_UPLOADS -> SettingsActivity.NOTIFICATION_INTENT_VIDEOS + } + NotificationUtils.createBasicNotification( + context = appContext, + contentTitle = appContext.getString(R.string.uploader_upload_camera_upload_source_path_error), + contentText = appContext.getString(contentText), + notificationChannelId = UPLOAD_NOTIFICATION_CHANNEL_ID, + notificationId = syncType.getNotificationId(), + intent = NotificationUtils.composePendingIntentToCameraUploads(appContext, notificationKey), + onGoing = false, + timeOut = null + ) + } + + private fun updateTimestamp(folderBackUpConfiguration: FolderBackUpConfiguration, syncType: SyncType) { + val currentTimestamp = System.currentTimeMillis() + + when (syncType) { + SyncType.PICTURE_UPLOADS -> { + val savePictureUploadsConfigurationUseCase: SavePictureUploadsConfigurationUseCase by inject() + savePictureUploadsConfigurationUseCase.execute( + SavePictureUploadsConfigurationUseCase.Params(folderBackUpConfiguration.copy(lastSyncTimestamp = currentTimestamp)) + ) + } + SyncType.VIDEO_UPLOADS -> { + val saveVideoUploadsConfigurationUseCase: SaveVideoUploadsConfigurationUseCase by inject() + saveVideoUploadsConfigurationUseCase.execute( + SaveVideoUploadsConfigurationUseCase.Params(folderBackUpConfiguration.copy(lastSyncTimestamp = currentTimestamp)) + ) + } + } + } + + private fun getFilesReadyToUpload( + syncType: SyncType, + sourcePath: String, + lastSyncTimestamp: Long, + ): List { + val sourceUri: Uri = sourcePath.toUri() + val documentTree = DocumentFile.fromTreeUri(applicationContext, sourceUri) + val arrayOfLocalFiles = documentTree?.listFiles() ?: arrayOf() + + val filteredList: List = arrayOfLocalFiles + .sortedBy { it.lastModified() } + .filter { it.lastModified() > lastSyncTimestamp } + .filter { MimetypeIconUtil.getBestMimeTypeByFilename(it.name).startsWith(syncType.prefixForType) } + + Timber.i("Last sync ${syncType.name}: ${Date(lastSyncTimestamp)}") + Timber.i("${arrayOfLocalFiles.size} files found in folder: ${sourceUri.path}") + Timber.i("${filteredList.size} files are ${syncType.name} and were taken after last sync") + + return filteredList + } + + private fun enqueueSingleUpload( + contentUri: Uri, + uploadPath: String, + lastModified: Long, + behavior: String, + accountName: String, + uploadId: Long, + wifiOnly: Boolean + ) { + val lastModifiedInSeconds = (lastModified / 1000L).toString() + + UploadFileFromContentUriUseCase(WorkManager.getInstance(appContext)).execute( + UploadFileFromContentUriUseCase.Params( + accountName = accountName, + contentUri = contentUri, + lastModifiedInSeconds = lastModifiedInSeconds, + behavior = behavior, + uploadPath = uploadPath, + uploadIdInStorageManager = uploadId, + wifiOnly = wifiOnly + ) + ) + } + + private fun storeInUploadsDatabase( + documentFile: DocumentFile, + uploadPath: String, + accountName: String, + behavior: FolderBackUpConfiguration.Behavior, + createdByWorker: Int + ): Long { + val uploadStorageManager = UploadsStorageManager(appContext.contentResolver) + + val ocUpload = OCUpload(documentFile.uri.toString(), uploadPath, accountName).apply { + fileSize = documentFile.length() + isForceOverwrite = false + createdBy = createdByWorker + localAction = behavior.ordinal + uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS + } + return uploadStorageManager.storeUpload(ocUpload) + } + + companion object { + const val CAMERA_UPLOADS_WORKER = "CAMERA_UPLOADS_WORKER" + const val repeatInterval: Long = 15L + val repeatIntervalTimeUnit: TimeUnit = TimeUnit.MINUTES + private const val pictureUploadsNotificationId = 101 + private const val videoUploadsNotificationId = 102 + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromContentUriWorker.kt b/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromContentUriWorker.kt new file mode 100644 index 00000000000..3ab52111322 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromContentUriWorker.kt @@ -0,0 +1,246 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.workers + +import android.accounts.Account +import android.content.Context +import android.net.Uri +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.owncloud.android.R +import com.owncloud.android.authentication.AccountUtils +import com.owncloud.android.data.executeRemoteOperation +import com.owncloud.android.datamodel.OCUpload +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.UploadResult +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration +import com.owncloud.android.domain.exceptions.ConflictException +import com.owncloud.android.domain.exceptions.FileNotFoundException +import com.owncloud.android.domain.exceptions.ForbiddenException +import com.owncloud.android.domain.exceptions.LocalFileNotFoundException +import com.owncloud.android.domain.exceptions.NoConnectionWithServerException +import com.owncloud.android.domain.exceptions.QuotaExceededException +import com.owncloud.android.domain.exceptions.SSLRecoverablePeerUnverifiedException +import com.owncloud.android.domain.exceptions.ServiceUnavailableException +import com.owncloud.android.domain.exceptions.SpecificUnsupportedMediaTypeException +import com.owncloud.android.domain.exceptions.UnauthorizedException +import com.owncloud.android.extensions.parseError +import com.owncloud.android.lib.common.OwnCloudAccount +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.SingleSessionManager +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.resources.files.CheckPathExistenceRemoteOperation +import com.owncloud.android.lib.resources.files.ContentUriRequestBody +import com.owncloud.android.lib.resources.files.CreateRemoteFolderOperation +import com.owncloud.android.lib.resources.files.UploadFileFromContentUriOperation +import com.owncloud.android.utils.NotificationUtils +import com.owncloud.android.utils.RemoteFileUtils.Companion.getAvailableRemotePath +import com.owncloud.android.utils.UPLOAD_NOTIFICATION_CHANNEL_ID +import org.koin.core.KoinComponent +import timber.log.Timber +import java.io.File + +class UploadFileFromContentUriWorker( + private val appContext: Context, + private val workerParameters: WorkerParameters +) : CoroutineWorker( + appContext, + workerParameters +), KoinComponent { + + private lateinit var account: Account + private lateinit var contentUri: Uri + private lateinit var lastModified: String + private lateinit var behavior: FolderBackUpConfiguration.Behavior + private lateinit var uploadPath: String + private var uploadIdInStorageManager: Long = -1 + + override suspend fun doWork(): Result { + + if (!areParametersValid()) return Result.failure() + + return try { + checkDocumentFileExists() + checkPermissionsToReadDocumentAreGranted() + checkParentFolderExistence() + checkNameCollisionAndGetAnAvailableOneInCase() + uploadDocument() + updateUploadsDatabaseWithResult(null) + Result.success() + } catch (throwable: Throwable) { + Timber.e(throwable) + showNotification(throwable) + updateUploadsDatabaseWithResult(throwable) + Result.failure() + } + } + + private fun areParametersValid(): Boolean { + val paramAccountName = workerParameters.inputData.getString(KEY_PARAM_ACCOUNT_NAME) + val paramUploadPath = workerParameters.inputData.getString(KEY_PARAM_UPLOAD_PATH) + val paramLastModified = workerParameters.inputData.getString(KEY_PARAM_LAST_MODIFIED) + val paramBehavior = workerParameters.inputData.getString(KEY_PARAM_BEHAVIOR) + val paramContentUri = workerParameters.inputData.getString(KEY_PARAM_CONTENT_URI) + val paramUploadId = workerParameters.inputData.getLong(KEY_PARAM_UPLOAD_ID, -1) + + account = AccountUtils.getOwnCloudAccountByName(appContext, paramAccountName) ?: return false + contentUri = paramContentUri?.toUri() ?: return false + uploadPath = paramUploadPath ?: return false + behavior = paramBehavior?.let { FolderBackUpConfiguration.Behavior.fromString(it) } ?: return false + lastModified = paramLastModified ?: return false + uploadIdInStorageManager = paramUploadId + + return true + } + + private fun checkPermissionsToReadDocumentAreGranted() { + val documentFile = DocumentFile.fromSingleUri(appContext, contentUri) + if (documentFile?.canRead() != true) { + // Permissions not granted. Throw an exception to ask for them. + throw Throwable("Cannot read the file") + } + } + + private fun checkDocumentFileExists() { + val documentFile = DocumentFile.fromSingleUri(appContext, contentUri) + if (documentFile?.exists() != true && documentFile?.isFile != true) { + // File does not exists anymore. Throw an exception to tell the user + throw LocalFileNotFoundException() + } + } + + private fun checkParentFolderExistence() { + var pathToGrant: String = File(uploadPath).parent ?: "" + pathToGrant = if (pathToGrant.endsWith(File.separator)) pathToGrant else pathToGrant + File.separator + + val checkPathExistenceOperation = CheckPathExistenceRemoteOperation(pathToGrant, false) + val checkPathExistenceResult = checkPathExistenceOperation.execute(getClientForThisUpload()) + if (checkPathExistenceResult.code == ResultCode.FILE_NOT_FOUND) { + val createRemoteFolderOperation = CreateRemoteFolderOperation(pathToGrant, true) + createRemoteFolderOperation.execute(getClientForThisUpload()) + } + } + + private fun checkNameCollisionAndGetAnAvailableOneInCase() { + Timber.d("Checking name collision in server") + val remotePath = getAvailableRemotePath(getClientForThisUpload(), uploadPath) + if (remotePath != null && remotePath != uploadPath) { + uploadPath = remotePath + Timber.d("Name collision detected, let's rename it to %s", remotePath) + } + } + + private fun uploadDocument() { + val client = getClientForThisUpload() + val requestBody = ContentUriRequestBody(appContext.contentResolver, contentUri) + + val uploadFileFromContentUriOperation = UploadFileFromContentUriOperation(uploadPath, lastModified, requestBody) + + val result = executeRemoteOperation { uploadFileFromContentUriOperation.execute(client) } + + if (result == Unit && behavior == FolderBackUpConfiguration.Behavior.MOVE) { + removeLocalFile() + } + } + + private fun removeLocalFile() { + val documentFile = DocumentFile.fromSingleUri(appContext, contentUri) + documentFile?.delete() + } + + private fun updateUploadsDatabaseWithResult(throwable: Throwable?) { + val uploadStorageManager = UploadsStorageManager(appContext.contentResolver) + + val ocUpload = OCUpload(contentUri.toString(), uploadPath, account.name).apply { + uploadStatus = getUploadStatusForThrowable(throwable) + uploadEndTimestamp = System.currentTimeMillis() + lastResult = getUploadResultFromThrowable(throwable) + uploadId = uploadIdInStorageManager + } + + uploadStorageManager.updateUpload(ocUpload) + } + + private fun getUploadStatusForThrowable(throwable: Throwable?): UploadsStorageManager.UploadStatus { + return if (throwable == null) { + UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED + } else { + UploadsStorageManager.UploadStatus.UPLOAD_FAILED + } + } + + private fun showNotification(throwable: Throwable) { + // check credentials error + val needsToUpdateCredentials = throwable is UnauthorizedException + + val tickerId = + if (needsToUpdateCredentials) R.string.uploader_upload_failed_credentials_error else R.string.uploader_upload_failed_ticker + + val pendingIntent = if (needsToUpdateCredentials) { + NotificationUtils.composePendingIntentToRefreshCredentials(appContext, account) + } else { + NotificationUtils.composePendingIntentToUploadList(appContext) + } + + NotificationUtils.createBasicNotification( + context = appContext, + contentTitle = appContext.getString(tickerId), + contentText = throwable.parseError("", appContext.resources, true).toString(), + notificationChannelId = UPLOAD_NOTIFICATION_CHANNEL_ID, + notificationId = 12, + intent = pendingIntent, + onGoing = false, + timeOut = null + ) + } + + private fun getUploadResultFromThrowable(throwable: Throwable?): UploadResult { + if (throwable == null) return UploadResult.UPLOADED + + return when (throwable) { + is LocalFileNotFoundException -> UploadResult.FOLDER_ERROR + is NoConnectionWithServerException -> UploadResult.NETWORK_CONNECTION + is UnauthorizedException -> UploadResult.CREDENTIAL_ERROR + is FileNotFoundException -> UploadResult.FILE_NOT_FOUND + is ConflictException -> UploadResult.CONFLICT_ERROR + is ForbiddenException -> UploadResult.PRIVILEDGES_ERROR + is ServiceUnavailableException -> UploadResult.SERVICE_UNAVAILABLE + is QuotaExceededException -> UploadResult.QUOTA_EXCEEDED + is SpecificUnsupportedMediaTypeException -> UploadResult.SPECIFIC_UNSUPPORTED_MEDIA_TYPE + is SSLRecoverablePeerUnverifiedException -> UploadResult.SSL_RECOVERABLE_PEER_UNVERIFIED + else -> UploadResult.UNKNOWN + } + } + + private fun getClientForThisUpload(): OwnCloudClient = SingleSessionManager.getDefaultSingleton() + .getClientFor(OwnCloudAccount(AccountUtils.getOwnCloudAccountByName(appContext, account.name), appContext), appContext) + + companion object { + const val TRANSFER_TAG_CAMERA_UPLOAD = "TRANSFER_TAG_CAMERA_UPLOAD" + + const val KEY_PARAM_ACCOUNT_NAME = "KEY_PARAM_ACCOUNT_NAME" + const val KEY_PARAM_BEHAVIOR = "KEY_PARAM_BEHAVIOR" + const val KEY_PARAM_CONTENT_URI = "KEY_PARAM_CONTENT_URI" + const val KEY_PARAM_LAST_MODIFIED = "KEY_PARAM_LAST_MODIFIED" + const val KEY_PARAM_UPLOAD_PATH = "KEY_PARAM_UPLOAD_PATH" + const val KEY_PARAM_UPLOAD_ID = "KEY_PARAM_UPLOAD_ID" + } +} diff --git a/owncloudApp/src/main/res/values-ar/strings.xml b/owncloudApp/src/main/res/values-ar/strings.xml index 36813de9d57..97eb99a1281 100644 --- a/owncloudApp/src/main/res/values-ar/strings.xml +++ b/owncloudApp/src/main/res/values-ar/strings.xml @@ -434,8 +434,7 @@ %1$d ملفات، %2$d مجلدات مجلد الكاميرا (%1$s) مطلوب - اختياري - الملف الأصلي + الملف الأصلي الملف الأصلي نسخ الملف نقل الملف diff --git a/owncloudApp/src/main/res/values-bg-rBG/strings.xml b/owncloudApp/src/main/res/values-bg-rBG/strings.xml index 187b8b46a1f..ee2b3187c5e 100644 --- a/owncloudApp/src/main/res/values-bg-rBG/strings.xml +++ b/owncloudApp/src/main/res/values-bg-rBG/strings.xml @@ -431,8 +431,7 @@ %1$d файла, %2$d папки Папка за камерата (%1$s) задължителен - по избор - Оригиналният файл ще бъде + Оригиналният файл ще бъде Оригиналният файл ще бъде Копиране на файл Преместване на файл diff --git a/owncloudApp/src/main/res/values-ca/strings.xml b/owncloudApp/src/main/res/values-ca/strings.xml index ce66aa1d20a..14ae77f3507 100644 --- a/owncloudApp/src/main/res/values-ca/strings.xml +++ b/owncloudApp/src/main/res/values-ca/strings.xml @@ -432,8 +432,7 @@ %1$d fitxers, %2$d carpetes Carpeta de la Càmera(%1$s) requerit - opcional - L\'arxiu original serà + L\'arxiu original serà L\'arxiu original serà Copiar fitxer Moure fitxer diff --git a/owncloudApp/src/main/res/values-cs-rCZ/strings.xml b/owncloudApp/src/main/res/values-cs-rCZ/strings.xml index bfcae2ae1a2..d8edef11d41 100644 --- a/owncloudApp/src/main/res/values-cs-rCZ/strings.xml +++ b/owncloudApp/src/main/res/values-cs-rCZ/strings.xml @@ -433,8 +433,7 @@ správce systému. %1$d soubory(ů), %2$d adresáře(ů) Adresář fotoaparátu (%1$s) vyžadován - nepovinný - Původní soubor bude + Původní soubor bude Původní soubor bude Zkopírovat soubor Přesunout soubor diff --git a/owncloudApp/src/main/res/values-da/strings.xml b/owncloudApp/src/main/res/values-da/strings.xml index d8855149c5e..1753ce9c89a 100644 --- a/owncloudApp/src/main/res/values-da/strings.xml +++ b/owncloudApp/src/main/res/values-da/strings.xml @@ -397,8 +397,7 @@ %1$d filer, %2$d mapper Kamera mappe (%1$s) Krævet - Valgfri - Kopier fil + Kopier fil Flyt fil opbevares i den oprindelige mappe flyttet til app-mappe diff --git a/owncloudApp/src/main/res/values-de-rCH/strings.xml b/owncloudApp/src/main/res/values-de-rCH/strings.xml index 2e9571bf71e..285cfb396eb 100644 --- a/owncloudApp/src/main/res/values-de-rCH/strings.xml +++ b/owncloudApp/src/main/res/values-de-rCH/strings.xml @@ -435,8 +435,7 @@ %1$d Dateien, %2$d Ordner Kamera-Ordner (%1$s) Benötigt - Optional - Originale Datei wird + Originale Datei wird Originale Datei wird kopiere Datei verschiebe Datei diff --git a/owncloudApp/src/main/res/values-de-rDE/strings.xml b/owncloudApp/src/main/res/values-de-rDE/strings.xml index 5ded538e541..fefa0b3e98f 100644 --- a/owncloudApp/src/main/res/values-de-rDE/strings.xml +++ b/owncloudApp/src/main/res/values-de-rDE/strings.xml @@ -437,8 +437,7 @@ %1$d Dateien, %2$d Ordner Kamera-Ordner (%1$s) Benötigt - Optional - Originaldatei wird... + Originaldatei wird... Originaldatei wird... Datei kopieren Datei verschieben diff --git a/owncloudApp/src/main/res/values-de/strings.xml b/owncloudApp/src/main/res/values-de/strings.xml index 75ee828250f..701b9abda9c 100644 --- a/owncloudApp/src/main/res/values-de/strings.xml +++ b/owncloudApp/src/main/res/values-de/strings.xml @@ -439,8 +439,7 @@ %1$d Dateien, %2$d Ordner Kamera-Ordner (%1$s) Benötigt - Optional - Originale Datei wird + Originale Datei wird Originale Datei wird kopiere Datei verschiebe Datei diff --git a/owncloudApp/src/main/res/values-el/strings.xml b/owncloudApp/src/main/res/values-el/strings.xml index b788ffdb005..da3163c6e7f 100644 --- a/owncloudApp/src/main/res/values-el/strings.xml +++ b/owncloudApp/src/main/res/values-el/strings.xml @@ -428,8 +428,7 @@ %1$d αρχεία, %2$d φάκελοι Φάκελος κάμερας (%1$s) απαιτείται - προαιρετικό - Το αρχικό αρχείο θα είναι + Το αρχικό αρχείο θα είναι Το αρχικό αρχείο θα είναι Αντιγραφή αρχείου Μετακίνηση αρχείου diff --git a/owncloudApp/src/main/res/values-en-rGB/strings.xml b/owncloudApp/src/main/res/values-en-rGB/strings.xml index a9de4e3bf83..2b1a59a0a19 100644 --- a/owncloudApp/src/main/res/values-en-rGB/strings.xml +++ b/owncloudApp/src/main/res/values-en-rGB/strings.xml @@ -436,8 +436,7 @@ %1$d files, %2$d folders Camera folder (%1$s) required - optional - Original file will be + Original file will be Original file will be Copy file Move file diff --git a/owncloudApp/src/main/res/values-es-rAR/strings.xml b/owncloudApp/src/main/res/values-es-rAR/strings.xml index dcde66eaefb..1b04fed3d69 100644 --- a/owncloudApp/src/main/res/values-es-rAR/strings.xml +++ b/owncloudApp/src/main/res/values-es-rAR/strings.xml @@ -438,8 +438,7 @@ Abajo, encontrás la lista con los enlaces a los archivos locales y remotos en % %1$d archivos, %2$d carpetas Carpeta de la cámara (%1$s) requerido - opcional - El archivo original debe ser + El archivo original debe ser El archivo original debe ser Copiar archivo Mover archivo diff --git a/owncloudApp/src/main/res/values-es-rMX/strings.xml b/owncloudApp/src/main/res/values-es-rMX/strings.xml index 7f5b0bcd57b..dd65a037494 100644 --- a/owncloudApp/src/main/res/values-es-rMX/strings.xml +++ b/owncloudApp/src/main/res/values-es-rMX/strings.xml @@ -404,8 +404,7 @@ %1$d archivos, %2$d carpetas Carpeta de la Camera (%1$s) requerido - opcional - El archivo original será... + El archivo original será... El archivo original será... Copiar archivo Mover archivo diff --git a/owncloudApp/src/main/res/values-es/strings.xml b/owncloudApp/src/main/res/values-es/strings.xml index 456b2652ebe..0c7b89b755e 100644 --- a/owncloudApp/src/main/res/values-es/strings.xml +++ b/owncloudApp/src/main/res/values-es/strings.xml @@ -453,8 +453,7 @@ Cuenta para subir videos Carpeta de cámara (%1$s) requerido - opcional - Archivo original será + Archivo original será Archivo original será Copiar archivo Mover archivo diff --git a/owncloudApp/src/main/res/values-et-rEE/strings.xml b/owncloudApp/src/main/res/values-et-rEE/strings.xml index 4bd2ffcd624..818b1722c05 100644 --- a/owncloudApp/src/main/res/values-et-rEE/strings.xml +++ b/owncloudApp/src/main/res/values-et-rEE/strings.xml @@ -352,8 +352,7 @@ Allpool on loend kohalikest failidest ning serveris asuvatest failidest %5$s, mi %1$d faili, 1 kaust %1$d faili, %2$d kausta nõutud - valikuline - Algne fail + Algne fail Algne failAlgne fail Kopeeri fail Liiguta fail diff --git a/owncloudApp/src/main/res/values-fa/strings.xml b/owncloudApp/src/main/res/values-fa/strings.xml index 995c0bd4833..bf1690acd5c 100644 --- a/owncloudApp/src/main/res/values-fa/strings.xml +++ b/owncloudApp/src/main/res/values-fa/strings.xml @@ -366,8 +366,7 @@ %1$d فایل، 1 پوشه %1$d فایل, %2$d پوشه ضروری - اختیاری - رونوشت از پرونده + رونوشت از پرونده جابه‌جایی پرونده اشتراک گذاری هم‌رسانی %1$s diff --git a/owncloudApp/src/main/res/values-fi-rFI/strings.xml b/owncloudApp/src/main/res/values-fi-rFI/strings.xml index fdf6c47ce40..ec1de0af38d 100644 --- a/owncloudApp/src/main/res/values-fi-rFI/strings.xml +++ b/owncloudApp/src/main/res/values-fi-rFI/strings.xml @@ -378,8 +378,7 @@ %1$d tiedostoa, %2$d kansiota Kamerakansio (%1$s) vaadittu - valinnainen - Kopioi tiedosto + Kopioi tiedosto Siirrä tiedosto pidetään alkuperäisessä kansiossa siirretään sovelluskansioon diff --git a/owncloudApp/src/main/res/values-fr/strings.xml b/owncloudApp/src/main/res/values-fr/strings.xml index 4c9e9e9d479..31430c70f49 100644 --- a/owncloudApp/src/main/res/values-fr/strings.xml +++ b/owncloudApp/src/main/res/values-fr/strings.xml @@ -431,8 +431,7 @@ Ci-dessous la liste des fichiers locaux, et les fichiers distants dans %5$s auxq %1$d fichiers, %2$d dossiers Dossier de l\'appareil photo (%1$s) Requis - Optionnel - Le fichier original sera + Le fichier original sera Le fichier original sera Copier le fichier Déplacer le fichier diff --git a/owncloudApp/src/main/res/values-gl/strings.xml b/owncloudApp/src/main/res/values-gl/strings.xml index c47b13dfd18..64125852766 100644 --- a/owncloudApp/src/main/res/values-gl/strings.xml +++ b/owncloudApp/src/main/res/values-gl/strings.xml @@ -438,8 +438,7 @@ Descárgueo de aquí: %2$s %1$d ficheiros, %2$d cartafoles Cartafol da cámara (%1$s) requirido - opcional - O ficheiro orixinal vai ser + O ficheiro orixinal vai ser O ficheiro orixinal vai ser Copiar ficheiro Mover ficheiro diff --git a/owncloudApp/src/main/res/values-he/strings.xml b/owncloudApp/src/main/res/values-he/strings.xml index 2623df234fe..225f7a3996f 100644 --- a/owncloudApp/src/main/res/values-he/strings.xml +++ b/owncloudApp/src/main/res/values-he/strings.xml @@ -437,8 +437,7 @@ %1$d קבצים, %2$d תיקיות תיקיית מצלמה (%1$s) נדרש - אופציונלי - קובץ מקורי יהיה + קובץ מקורי יהיה קובץ מקורי יהיה העתקת קובץ העברת קובץ diff --git a/owncloudApp/src/main/res/values-hu-rHU/strings.xml b/owncloudApp/src/main/res/values-hu-rHU/strings.xml index 92ac7e3ea5e..7d1e9fdf899 100644 --- a/owncloudApp/src/main/res/values-hu-rHU/strings.xml +++ b/owncloudApp/src/main/res/values-hu-rHU/strings.xml @@ -419,8 +419,7 @@ %1$d fájl, %2$d könyvtár Kamera mappa (%1$s) kötelező - választható - Eredeti fájl lesz ... + Eredeti fájl lesz ... Eredeti fájl lesz ... Fájl másolása Fájl mozgatása diff --git a/owncloudApp/src/main/res/values-id/strings.xml b/owncloudApp/src/main/res/values-id/strings.xml index cbca3e2d61c..7775f98ad3e 100644 --- a/owncloudApp/src/main/res/values-id/strings.xml +++ b/owncloudApp/src/main/res/values-id/strings.xml @@ -424,8 +424,7 @@ %1$d berkas, %2$d folder Folder kamera (%1$s) Diperlukan - Pilihan - File asli akan di + File asli akan di File asli akan di Salin file Pindahkan file diff --git a/owncloudApp/src/main/res/values-is/strings.xml b/owncloudApp/src/main/res/values-is/strings.xml index e95f01f2c22..18dff207ab6 100644 --- a/owncloudApp/src/main/res/values-is/strings.xml +++ b/owncloudApp/src/main/res/values-is/strings.xml @@ -395,8 +395,7 @@ %1$d skrár, %2$d möppur Myndavélarmappa (%1$s) krafist - valkvætt - Upprunaleg skrá verður + Upprunaleg skrá verður Upprunaleg skrá verður Afrita skrá Færa skrá diff --git a/owncloudApp/src/main/res/values-it/strings.xml b/owncloudApp/src/main/res/values-it/strings.xml index 5403de3a131..2b3cd27f83e 100644 --- a/owncloudApp/src/main/res/values-it/strings.xml +++ b/owncloudApp/src/main/res/values-it/strings.xml @@ -434,8 +434,7 @@ %1$d file, %2$d cartelle Cartella della fotocamera (%1$s) richiesto - opzionale - Il file originale verrà + Il file originale verrà Il file originale verrà Copia file Sposta file diff --git a/owncloudApp/src/main/res/values-ja-rJP/strings.xml b/owncloudApp/src/main/res/values-ja-rJP/strings.xml index e861d0bc895..6d71ae24041 100644 --- a/owncloudApp/src/main/res/values-ja-rJP/strings.xml +++ b/owncloudApp/src/main/res/values-ja-rJP/strings.xml @@ -389,8 +389,7 @@ %1$d ファイル、%2$d フォルダー カメラフォルダー (%1$s) 必須 - 任意 - ファイルをコピー + ファイルをコピー ファイルを移動 元のフォルダーに保持 アプリフォルダーに移動 diff --git a/owncloudApp/src/main/res/values-ko/strings.xml b/owncloudApp/src/main/res/values-ko/strings.xml index 12d03092912..e14f3f709ce 100644 --- a/owncloudApp/src/main/res/values-ko/strings.xml +++ b/owncloudApp/src/main/res/values-ko/strings.xml @@ -382,8 +382,7 @@ 파일 %1$d개, 폴더 %2$d개 카메라 폴더(%1$s) 필수 - 선택 - 파일 복사 + 파일 복사 파일 이동 원래 폴더에 유지됨 앱 폴더로 이동함 diff --git a/owncloudApp/src/main/res/values-lt-rLT/strings.xml b/owncloudApp/src/main/res/values-lt-rLT/strings.xml index 5c3f6faf59b..cf24ff41705 100644 --- a/owncloudApp/src/main/res/values-lt-rLT/strings.xml +++ b/owncloudApp/src/main/res/values-lt-rLT/strings.xml @@ -391,8 +391,7 @@ %1$d failai, %2$d aplankai Kameros katalogas (%1$s) privalomas - neprivalomas - Kopijuoti failą + Kopijuoti failą Perkelti failą paliktas pradiniame aplanke perkelti į app aplanką diff --git a/owncloudApp/src/main/res/values-nb-rNO/strings.xml b/owncloudApp/src/main/res/values-nb-rNO/strings.xml index d032a02e6c9..1739e4f87e6 100644 --- a/owncloudApp/src/main/res/values-nb-rNO/strings.xml +++ b/owncloudApp/src/main/res/values-nb-rNO/strings.xml @@ -423,8 +423,7 @@ %1$d filer, %2$d mapper Kamera-mappe (%1$s) obligatorisk - valgfri - Den originale filen vil bli + Den originale filen vil bli Den originale filen vil bli Kopier fil Flytt fil diff --git a/owncloudApp/src/main/res/values-nl/strings.xml b/owncloudApp/src/main/res/values-nl/strings.xml index 857bc866137..76649b99837 100644 --- a/owncloudApp/src/main/res/values-nl/strings.xml +++ b/owncloudApp/src/main/res/values-nl/strings.xml @@ -438,8 +438,7 @@ Hieronder staan de lokale bestanden en de externe bestanden in %5$s waar ze naar %1$d bestanden, %2$d mappen Cameramap (%1$s) vereist - optioneel - Origineel bestand zal zijn + Origineel bestand zal zijn Origineel bestand zal zijn Kopiëren bestand Verplaatsen bestand diff --git a/owncloudApp/src/main/res/values-oc/strings.xml b/owncloudApp/src/main/res/values-oc/strings.xml index 25438bd6ce5..0f518ab29a2 100644 --- a/owncloudApp/src/main/res/values-oc/strings.xml +++ b/owncloudApp/src/main/res/values-oc/strings.xml @@ -320,8 +320,7 @@ En rason d\'aquesta modificacion, totes los fichièrs mandats amb de versions an %1$d fichièrs, %2$d dorsièrs Dorsièr de l\'aparelh fòto (%1$s) Requesit - Opcional - Lo fichièr original serà… + Lo fichièr original serà… Lo fichièr original serà… Copiar lo fichièr Desplaçar lo fichièr diff --git a/owncloudApp/src/main/res/values-pl/strings.xml b/owncloudApp/src/main/res/values-pl/strings.xml index 57dee2a8ca4..b5d9bd1a135 100644 --- a/owncloudApp/src/main/res/values-pl/strings.xml +++ b/owncloudApp/src/main/res/values-pl/strings.xml @@ -427,8 +427,7 @@ %1$d plików, %2$d folderów Folder aparatu (%1$s) wymagane - opcjonalne - Oryginalny plik zostanie + Oryginalny plik zostanie Oryginalny plik zostanie Kopiuj plik Przenieś plik diff --git a/owncloudApp/src/main/res/values-pt-rBR/strings.xml b/owncloudApp/src/main/res/values-pt-rBR/strings.xml index 2a5f41c690d..87493537dde 100644 --- a/owncloudApp/src/main/res/values-pt-rBR/strings.xml +++ b/owncloudApp/src/main/res/values-pt-rBR/strings.xml @@ -453,8 +453,7 @@ Conta para fazer envio de vídeos Pasta da câmera (%1$s) exigido - opcional - Arquivo original será + Arquivo original será Arquivo original será Copiar o arquivo Mover o arquivo diff --git a/owncloudApp/src/main/res/values-pt-rPT/strings.xml b/owncloudApp/src/main/res/values-pt-rPT/strings.xml index 4bdcb694592..1fbfb6d6fb2 100644 --- a/owncloudApp/src/main/res/values-pt-rPT/strings.xml +++ b/owncloudApp/src/main/res/values-pt-rPT/strings.xml @@ -368,8 +368,7 @@ %1$d ficheiros, %2$d pastas Pasta da Camara (%1$s) obrigatório - opcional - Copiar ficheiro + Copiar ficheiro Mover ficheiro mantido na pasta original movido para pasta da aplicação diff --git a/owncloudApp/src/main/res/values-ro/strings.xml b/owncloudApp/src/main/res/values-ro/strings.xml index b81c2eb3c5a..9b53fbb7b48 100644 --- a/owncloudApp/src/main/res/values-ro/strings.xml +++ b/owncloudApp/src/main/res/values-ro/strings.xml @@ -399,8 +399,7 @@ %1$d fișiere, %2$d foldere Dosarul camerei (%1$s) necesar - opțional - Fișierul original va fi + Fișierul original va fi Fișierul original va fi Copiază fișier Mută fișier diff --git a/owncloudApp/src/main/res/values-ru/strings.xml b/owncloudApp/src/main/res/values-ru/strings.xml index 5fd7b3e8823..9a12af0498c 100644 --- a/owncloudApp/src/main/res/values-ru/strings.xml +++ b/owncloudApp/src/main/res/values-ru/strings.xml @@ -454,8 +454,7 @@ Учётная запись для закачки видео Каталог камеры (%1$s) обязательно - не обязательно - Исходный файл будет + Исходный файл будет Исходный файл будет Копировать файл Переместить файл diff --git a/owncloudApp/src/main/res/values-sk-rSK/strings.xml b/owncloudApp/src/main/res/values-sk-rSK/strings.xml index 0126f9d9b3e..3ed1c3bdddb 100644 --- a/owncloudApp/src/main/res/values-sk-rSK/strings.xml +++ b/owncloudApp/src/main/res/values-sk-rSK/strings.xml @@ -366,8 +366,7 @@ %1$d súb., %2$d prieč. Adresár fotoaparátu (%1$s) Požaduje sa - Volitelné - Originálny súbor bude + Originálny súbor bude Originálny súbor bude Kopírovanie súboru Presunutie súboru diff --git a/owncloudApp/src/main/res/values-sl/strings.xml b/owncloudApp/src/main/res/values-sl/strings.xml index b3c57a10770..99b46df8b78 100644 --- a/owncloudApp/src/main/res/values-sl/strings.xml +++ b/owncloudApp/src/main/res/values-sl/strings.xml @@ -404,8 +404,7 @@ %1$d datotek, %2$d map Mapa kamere (%1$s) zahtevano - izbirno - Kopiraj datoteko + Kopiraj datoteko Premakni datoteko Ohrani v izvorni mapi Premakni v mapo programa diff --git a/owncloudApp/src/main/res/values-sq/strings.xml b/owncloudApp/src/main/res/values-sq/strings.xml index c7ced70c7ec..58d4cc9a30c 100644 --- a/owncloudApp/src/main/res/values-sq/strings.xml +++ b/owncloudApp/src/main/res/values-sq/strings.xml @@ -451,8 +451,7 @@ Llogari ku të ngarkohen video Dosje kamere (%1$s) e domosdoshme - opsionale - Kartela origjinale do të jetë + Kartela origjinale do të jetë Kartela origjinale do të jetë Kopjoje kartelën Lëvize kartelën diff --git a/owncloudApp/src/main/res/values-sr/strings.xml b/owncloudApp/src/main/res/values-sr/strings.xml index 2a9fe10293e..08178e127d7 100644 --- a/owncloudApp/src/main/res/values-sr/strings.xml +++ b/owncloudApp/src/main/res/values-sr/strings.xml @@ -314,8 +314,7 @@ %1$d фајлова, 1 фасцикла %1$d фајлова, %2$d фасцикли неопходно - опционо - Оригинални фајл ће бити… + Оригинални фајл ће бити… Оригинални фајл ће бити… Копирај фајл Премести фајл diff --git a/owncloudApp/src/main/res/values-sv/strings.xml b/owncloudApp/src/main/res/values-sv/strings.xml index d6f51a023e0..3cc0d993c4a 100644 --- a/owncloudApp/src/main/res/values-sv/strings.xml +++ b/owncloudApp/src/main/res/values-sv/strings.xml @@ -365,8 +365,7 @@ %1$d filer, %2$d mappar Kameramapp (%1$s) krävs - valfritt - Kopiera fil + Kopiera fil Flytta fil behållas i orginalmapp flyttas till ownCloudmapp diff --git a/owncloudApp/src/main/res/values-th-rTH/strings.xml b/owncloudApp/src/main/res/values-th-rTH/strings.xml index 8833a165b05..9c5ae7bf272 100644 --- a/owncloudApp/src/main/res/values-th-rTH/strings.xml +++ b/owncloudApp/src/main/res/values-th-rTH/strings.xml @@ -451,8 +451,7 @@ บัญชีสำหรับอัปโหลดวิดีโอ โฟลเดอร์ของกล้อง (%1$s) สิ่งจำเป็นต้องใช้ - ตัวเลือก - ไฟล์ต้นฉบับจะเป็น + ไฟล์ต้นฉบับจะเป็น ไฟล์ต้นฉบับจะเป็น คัดลอกไฟล์ ย้ายไฟล์ diff --git a/owncloudApp/src/main/res/values-tr/strings.xml b/owncloudApp/src/main/res/values-tr/strings.xml index 007fbad0dbc..aca41f083fb 100644 --- a/owncloudApp/src/main/res/values-tr/strings.xml +++ b/owncloudApp/src/main/res/values-tr/strings.xml @@ -435,8 +435,7 @@ %1$d dosya, %2$d klasör Kamera klasörü (%1$s) gerekli - isteğe bağlı - şu olacak + şu olacak şu olacak Dosyayı kopyala Dosyayı taşı diff --git a/owncloudApp/src/main/res/values-uk/strings.xml b/owncloudApp/src/main/res/values-uk/strings.xml index b3e21976627..d563bfc0b33 100644 --- a/owncloudApp/src/main/res/values-uk/strings.xml +++ b/owncloudApp/src/main/res/values-uk/strings.xml @@ -397,8 +397,7 @@ %1$d файлів, %2$d тек Тека камери (%1$s) вимагається - необов\'язково - Копіювати файл + Копіювати файл Перемістити файл залишено в оригінальній теці перенесено до теки програми diff --git a/owncloudApp/src/main/res/values-zh-rCN/strings.xml b/owncloudApp/src/main/res/values-zh-rCN/strings.xml index 23e88221fed..0c922107329 100644 --- a/owncloudApp/src/main/res/values-zh-rCN/strings.xml +++ b/owncloudApp/src/main/res/values-zh-rCN/strings.xml @@ -415,8 +415,7 @@ %1$d 个文件,%2$d 个文件夹 相机文件夹 (%1$s) 必需 - 可选 - 原始文件将会 + 原始文件将会 原始文件将会 复制文件 移动文件 diff --git a/owncloudApp/src/main/res/values-zh-rTW/strings.xml b/owncloudApp/src/main/res/values-zh-rTW/strings.xml index 38220669d01..ab37d3331f3 100644 --- a/owncloudApp/src/main/res/values-zh-rTW/strings.xml +++ b/owncloudApp/src/main/res/values-zh-rTW/strings.xml @@ -436,8 +436,7 @@ %1$d 個檔案, %2$d 個資料夾 相機資料夾 (%1$s) 必填 - 選填 - 原始檔案將 + 原始檔案將 原始檔案將 複製檔案 移動檔案 diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 06a5a0ae938..40a232c11f7 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -187,6 +187,11 @@ Filename must not be more than %d characters Filename Uploading camera upload files + %d new pictures will be uploaded + %d new videos will be uploaded + Camera Uploads failed + Picture uploads source path is not valid anymore + Video uploads source path is not valid anymore Uploading available offline files Uploading requested from wifi files Server certificate is not trusted @@ -488,9 +493,9 @@ Account to upload videos Camera folder (%1$s) required - optional Original file will be Original file will be + Last synchronization Copy file Move file diff --git a/owncloudApp/src/main/res/values/strings__not_to_translate.xml b/owncloudApp/src/main/res/values/strings__not_to_translate.xml index 15c00cefe10..228f0168aff 100644 --- a/owncloudApp/src/main/res/values/strings__not_to_translate.xml +++ b/owncloudApp/src/main/res/values/strings__not_to_translate.xml @@ -18,15 +18,4 @@ --> - - - @string/pref_behaviour_entries_keep_file - @string/pref_behaviour_entries_move - - - - NOTHING - MOVE - - diff --git a/owncloudApp/src/main/res/xml/settings_picture_uploads.xml b/owncloudApp/src/main/res/xml/settings_picture_uploads.xml index 7a18d195382..c91198b86dc 100644 --- a/owncloudApp/src/main/res/xml/settings_picture_uploads.xml +++ b/owncloudApp/src/main/res/xml/settings_picture_uploads.xml @@ -44,12 +44,14 @@ - + diff --git a/owncloudApp/src/main/res/xml/settings_video_uploads.xml b/owncloudApp/src/main/res/xml/settings_video_uploads.xml index b8b2b0c38ef..eb51e995a17 100644 --- a/owncloudApp/src/main/res/xml/settings_video_uploads.xml +++ b/owncloudApp/src/main/res/xml/settings_video_uploads.xml @@ -44,12 +44,14 @@ - + \ No newline at end of file diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsPictureUploadsViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsPictureUploadsViewModelTest.kt deleted file mode 100644 index 95cd0c67712..00000000000 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsPictureUploadsViewModelTest.kt +++ /dev/null @@ -1,257 +0,0 @@ -/** - * ownCloud Android client application - * - * @author Juan Carlos Garrote Gascón - * - * Copyright (C) 2021 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.presentation.viewmodels.settings - -import android.content.Intent -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.db.PreferenceManager -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ENABLED -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_PATH -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_SOURCE -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_UPLOADS_DEFAULT_PATH -import com.owncloud.android.presentation.viewmodels.ViewModelTest -import com.owncloud.android.providers.AccountProvider -import com.owncloud.android.providers.CameraUploadsHandlerProvider -import com.owncloud.android.testutil.OC_ACCOUNT -import com.owncloud.android.ui.activity.LocalFolderPickerActivity -import com.owncloud.android.ui.activity.UploadPathActivity -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test - -@ExperimentalCoroutinesApi -class SettingsPictureUploadsViewModelTest : ViewModelTest() { - private lateinit var picturesViewModel: SettingsPictureUploadsViewModel - private lateinit var preferencesProvider: SharedPreferencesProvider - private lateinit var cameraUploadsHandlerProvider: CameraUploadsHandlerProvider - private lateinit var accountProvider: AccountProvider - - private val examplePath = "/Example/Path" - private val exampleSourcePath = "/Example/Source/Path" - private val exampleRemotePath = "/Example/Remote/Path" - - @Before - fun setUp() { - preferencesProvider = mockk(relaxUnitFun = true) - cameraUploadsHandlerProvider = mockk(relaxUnitFun = true) - accountProvider = mockk() - - picturesViewModel = SettingsPictureUploadsViewModel( - preferencesProvider, - cameraUploadsHandlerProvider, - accountProvider - ) - } - - @After - override fun tearDown() { - super.tearDown() - } - - @Test - fun `is picture upload enabled - ok - true`() { - every { preferencesProvider.getBoolean(any(), any()) } returns true - - val pictureUploadEnabled = picturesViewModel.isPictureUploadEnabled() - - assertTrue(pictureUploadEnabled) - - verify(exactly = 1) { - preferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false) - } - } - - @Test - fun `is picture upload enabled - ok - false`() { - every { preferencesProvider.getBoolean(any(), any()) } returns false - - val pictureUploadEnabled = picturesViewModel.isPictureUploadEnabled() - - assertFalse(pictureUploadEnabled) - - verify(exactly = 1) { - preferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false) - } - } - - @Test - fun `set enable picture upload - ok - true`() { - every { accountProvider.getCurrentOwnCloudAccount() } returns OC_ACCOUNT - - picturesViewModel.setEnablePictureUpload(true) - - verify(exactly = 1) { - preferencesProvider.putString(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME, OC_ACCOUNT.name) - preferencesProvider.putBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, true) - } - } - - @Test - fun `set enable picture upload - ok - false`() { - picturesViewModel.setEnablePictureUpload(false) - - verify(exactly = 1) { - preferencesProvider.putBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false) - preferencesProvider.removePreference(key = PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME) - preferencesProvider.removePreference(key = PREF__CAMERA_PICTURE_UPLOADS_PATH) - } - } - - @Test - fun `update pictures last sync - ok`() { - picturesViewModel.updatePicturesLastSync() - - verify(exactly = 1) { - cameraUploadsHandlerProvider.updatePicturesLastSync(0) - } - } - - @Test - fun `get picture uploads path - ok`() { - every { preferencesProvider.getString(any(), any()) } returns examplePath - - val uploadPath = picturesViewModel.getPictureUploadsPath() - - assertEquals(examplePath, uploadPath) - - verify(exactly = 1) { - preferencesProvider.getString( - PREF__CAMERA_PICTURE_UPLOADS_PATH, - PREF__CAMERA_UPLOADS_DEFAULT_PATH - ) - } - } - - @Test - fun `get picture uploads source path - ok`() { - mockkStatic(PreferenceManager.CameraUploadsConfiguration::class) - - every { preferencesProvider.getString(any(), any()) } returns exampleSourcePath - every { PreferenceManager.CameraUploadsConfiguration.getDefaultSourcePath() } returns "" - - val uploadSourcePath = picturesViewModel.getPictureUploadsSourcePath() - - assertEquals(exampleSourcePath, uploadSourcePath) - - verify(exactly = 1) { - preferencesProvider.getString( - PREF__CAMERA_PICTURE_UPLOADS_SOURCE, - PreferenceManager.CameraUploadsConfiguration.getDefaultSourcePath() - ) - } - } - - @Test - fun `handle select picture uploads path - ok`() { - val data: Intent = mockk() - val ocFile: OCFile = mockk() - - every { ocFile.remotePath } returns exampleRemotePath - every { data.getParcelableExtra(any()) } returns ocFile - - picturesViewModel.handleSelectPictureUploadsPath(data) - - verify(exactly = 1) { - data.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER) - ocFile.remotePath - preferencesProvider.putString(PREF__CAMERA_PICTURE_UPLOADS_PATH, exampleRemotePath) - } - } - - @Test - fun `handle select picture uploads path - ko - folder to upload is null`() { - val data: Intent = mockk() - - every { data.getParcelableExtra(any()) } returns null - - picturesViewModel.handleSelectPictureUploadsPath(data) - - verify(exactly = 1) { - data.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER) - } - } - - @Test - fun `handle select picture uploads source path - ok - source path hasn't changed`() { - val data: Intent = mockk() - mockkStatic(PreferenceManager.CameraUploadsConfiguration::class) - - every { preferencesProvider.getString(any(), any()) } returns exampleSourcePath - // It has to be "" for the test to pass - every { PreferenceManager.CameraUploadsConfiguration.getDefaultSourcePath() } returns "" - every { data.getStringExtra(any()) } returns exampleSourcePath - - picturesViewModel.handleSelectPictureUploadsSourcePath(data) - - verify(exactly = 2) { - data.getStringExtra(LocalFolderPickerActivity.EXTRA_PATH) - } - verify(exactly = 1) { - preferencesProvider.putString(PREF__CAMERA_PICTURE_UPLOADS_SOURCE, exampleSourcePath) - } - } - - @Test - fun `handle select picture uploads source path - ok - source path has changed`() { - val data: Intent = mockk() - val sourcePath = "/New/Source/Path" - mockkStatic(PreferenceManager.CameraUploadsConfiguration::class) - - every { preferencesProvider.getString(any(), any()) } returns exampleSourcePath - // It has to be "" for the test to pass - every { PreferenceManager.CameraUploadsConfiguration.getDefaultSourcePath() } returns "" - every { data.getStringExtra(any()) } returns sourcePath - - picturesViewModel.handleSelectPictureUploadsSourcePath(data) - - every { preferencesProvider.getString(any(), any()) } returns sourcePath - - val newSourcePath = picturesViewModel.getPictureUploadsSourcePath() - assertEquals(sourcePath, newSourcePath) - - verify(exactly = 2) { - data.getStringExtra(LocalFolderPickerActivity.EXTRA_PATH) - } - verify(exactly = 1) { - cameraUploadsHandlerProvider.updatePicturesLastSync(any()) - preferencesProvider.putString(PREF__CAMERA_PICTURE_UPLOADS_SOURCE, sourcePath) - } - } - - @Test - fun `schedule picture uploads sync job - ok`() { - picturesViewModel.schedulePictureUploadsSyncJob() - - verify(exactly = 1) { - cameraUploadsHandlerProvider.schedulePictureUploadsSyncJob() - } - } -} diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsVideoUploadsViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsVideoUploadsViewModelTest.kt deleted file mode 100644 index 3ebe85485aa..00000000000 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsVideoUploadsViewModelTest.kt +++ /dev/null @@ -1,256 +0,0 @@ -/** - * ownCloud Android client application - * - * @author Juan Carlos Garrote Gascón - * - * Copyright (C) 2021 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.presentation.viewmodels.settings - -import android.content.Intent -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.db.PreferenceManager -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_UPLOADS_DEFAULT_PATH -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ENABLED -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_PATH -import com.owncloud.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_SOURCE -import com.owncloud.android.presentation.viewmodels.ViewModelTest -import com.owncloud.android.providers.AccountProvider -import com.owncloud.android.providers.CameraUploadsHandlerProvider -import com.owncloud.android.testutil.OC_ACCOUNT -import com.owncloud.android.ui.activity.LocalFolderPickerActivity -import com.owncloud.android.ui.activity.UploadPathActivity -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test - -@ExperimentalCoroutinesApi -class SettingsVideoUploadsViewModelTest : ViewModelTest() { - private lateinit var videosViewModel: SettingsVideoUploadsViewModel - private lateinit var preferencesProvider: SharedPreferencesProvider - private lateinit var cameraUploadsHandlerProvider: CameraUploadsHandlerProvider - private lateinit var accountProvider: AccountProvider - - private val examplePath = "/Example/Path" - private val exampleSourcePath = "/Example/Source/Path" - private val exampleRemotePath = "/Example/Remote/Path" - - @Before - fun setUp() { - preferencesProvider = mockk(relaxUnitFun = true) - cameraUploadsHandlerProvider = mockk(relaxUnitFun = true) - accountProvider = mockk() - - videosViewModel = SettingsVideoUploadsViewModel( - preferencesProvider, - cameraUploadsHandlerProvider, - accountProvider - ) - } - - @After - override fun tearDown() { - super.tearDown() - } - - @Test - fun `is video upload enabled - ok - true`() { - every { preferencesProvider.getBoolean(any(), any()) } returns true - - val videoUploadEnabled = videosViewModel.isVideoUploadEnabled() - - assertTrue(videoUploadEnabled) - - verify(exactly = 1) { - preferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false) - } - } - - @Test - fun `is video upload enabled - ok - false`() { - every { preferencesProvider.getBoolean(any(), any()) } returns false - - val videoUploadEnabled = videosViewModel.isVideoUploadEnabled() - - assertFalse(videoUploadEnabled) - - verify(exactly = 1) { - preferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false) - } - } - - @Test - fun `set enable video upload - ok - true`() { - every { accountProvider.getCurrentOwnCloudAccount() } returns OC_ACCOUNT - - videosViewModel.setEnableVideoUpload(true) - - verify(exactly = 1) { - preferencesProvider.putString(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME, OC_ACCOUNT.name) - preferencesProvider.putBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, true) - } - } - - @Test - fun `set enable video upload - ok - false`() { - videosViewModel.setEnableVideoUpload(false) - - verify(exactly = 1) { - preferencesProvider.putBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false) - preferencesProvider.removePreference(key = PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME) - preferencesProvider.removePreference(key = PREF__CAMERA_VIDEO_UPLOADS_PATH) - } - } - - @Test - fun `update videos last sync - ok`() { - videosViewModel.updateVideosLastSync() - - verify(exactly = 1) { - cameraUploadsHandlerProvider.updateVideosLastSync(0) - } - } - - @Test - fun `get video uploads path - ok`() { - every { preferencesProvider.getString(any(), any()) } returns examplePath - val uploadPath = videosViewModel.getVideoUploadsPath() - - assertEquals(examplePath, uploadPath) - - verify(exactly = 1) { - preferencesProvider.getString( - PREF__CAMERA_VIDEO_UPLOADS_PATH, - PREF__CAMERA_UPLOADS_DEFAULT_PATH - ) - } - } - - @Test - fun `get video uploads source path - ok`() { - mockkStatic(PreferenceManager.CameraUploadsConfiguration::class) - - every { preferencesProvider.getString(any(), any()) } returns exampleSourcePath - every { PreferenceManager.CameraUploadsConfiguration.getDefaultSourcePath() } returns "" - - val uploadSourcePath = videosViewModel.getVideoUploadsSourcePath() - - assertEquals(exampleSourcePath, uploadSourcePath) - - verify(exactly = 1) { - preferencesProvider.getString( - PREF__CAMERA_VIDEO_UPLOADS_SOURCE, - PreferenceManager.CameraUploadsConfiguration.getDefaultSourcePath() - ) - } - } - - @Test - fun `handle select video uploads path - ok`() { - val data: Intent = mockk() - val ocFile: OCFile = mockk() - - every { ocFile.remotePath } returns exampleRemotePath - every { data.getParcelableExtra(any()) } returns ocFile - - videosViewModel.handleSelectVideoUploadsPath(data) - - verify(exactly = 1) { - data.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER) - ocFile.remotePath - preferencesProvider.putString(PREF__CAMERA_VIDEO_UPLOADS_PATH, exampleRemotePath) - } - } - - @Test - fun `handle select video uploads path - ko - folder to upload is null`() { - val data: Intent = mockk() - - every { data.getParcelableExtra(any()) } returns null - - videosViewModel.handleSelectVideoUploadsPath(data) - - verify(exactly = 1) { - data.getParcelableExtra(UploadPathActivity.EXTRA_FOLDER) - } - } - - @Test - fun `handle select video uploads source path - ok - source path hasn't changed`() { - val data: Intent = mockk() - mockkStatic(PreferenceManager.CameraUploadsConfiguration::class) - - every { preferencesProvider.getString(any(), any()) } returns exampleSourcePath - // It has to be "" for the test to pass - every { PreferenceManager.CameraUploadsConfiguration.getDefaultSourcePath() } returns "" - every { data.getStringExtra(any()) } returns exampleSourcePath - - videosViewModel.handleSelectVideoUploadsSourcePath(data) - - verify(exactly = 2) { - data.getStringExtra(LocalFolderPickerActivity.EXTRA_PATH) - } - verify(exactly = 1) { - preferencesProvider.putString(PREF__CAMERA_VIDEO_UPLOADS_SOURCE, exampleSourcePath) - } - } - - @Test - fun `handle select video uploads source path - ok - source path has changed`() { - val data: Intent = mockk() - val sourcePath = "/New/Source/Path" - mockkStatic(PreferenceManager.CameraUploadsConfiguration::class) - - every { preferencesProvider.getString(any(), any()) } returns exampleSourcePath - // It has to be "" for the test to pass - every { PreferenceManager.CameraUploadsConfiguration.getDefaultSourcePath() } returns "" - every { data.getStringExtra(any()) } returns sourcePath - - videosViewModel.handleSelectVideoUploadsSourcePath(data) - - every { preferencesProvider.getString(any(), any()) } returns sourcePath - - val newSourcePath = videosViewModel.getVideoUploadsSourcePath() - assertEquals(sourcePath, newSourcePath) - - verify(exactly = 2) { - data.getStringExtra(LocalFolderPickerActivity.EXTRA_PATH) - } - verify(exactly = 1) { - cameraUploadsHandlerProvider.updateVideosLastSync(any()) - preferencesProvider.putString(PREF__CAMERA_VIDEO_UPLOADS_SOURCE, sourcePath) - } - } - - @Test - fun `schedule video uploads sync job - ok`() { - videosViewModel.scheduleVideoUploadsSyncJob() - - verify(exactly = 1) { - cameraUploadsHandlerProvider.scheduleVideoUploadsSyncJob() - } - } -} diff --git a/owncloudData/build.gradle b/owncloudData/build.gradle index d799f17d68a..999932632b1 100644 --- a/owncloudData/build.gradle +++ b/owncloudData/build.gradle @@ -52,9 +52,12 @@ dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" + + implementation "androidx.legacy:legacy-support-v4:$androidX" // Room - implementation "androidx.room:room-runtime:$roomVersion" + implementation "androidx.room:room-ktx:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" // Dependencies for unit tests diff --git a/owncloudData/schemas/com.owncloud.android.data.OwncloudDatabase/34.json b/owncloudData/schemas/com.owncloud.android.data.OwncloudDatabase/34.json new file mode 100644 index 00000000000..52af4a5bff5 --- /dev/null +++ b/owncloudData/schemas/com.owncloud.android.data.OwncloudDatabase/34.json @@ -0,0 +1,405 @@ +{ + "formatVersion": 1, + "database": { + "version": 34, + "identityHash": "23b7b67de0e6641230799a550a78bde9", + "entities": [ + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`share_type` INTEGER NOT NULL, `share_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `id_remote_shared` TEXT NOT NULL, `owner_share` TEXT NOT NULL, `name` TEXT, `url` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shareWith", + "columnName": "share_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithAdditionalInfo", + "columnName": "share_with_additional_info", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isFolder", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "id_remote_shared", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version` TEXT NOT NULL, `sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1, `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning` INTEGER NOT NULL DEFAULT -1, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMayor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEdition", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "corePollInterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "davChunkingVersion", + "columnName": "dav_chunking_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filesSharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingPublicPasswordEnforcedReadOnly", + "columnName": "sharing_public_password_enforced_read_only", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingPublicPasswordEnforcedReadWrite", + "columnName": "sharing_public_password_enforced_read_write", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingPublicPasswordEnforcedUploadOnly", + "columnName": "sharing_public_password_enforced_public_only", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filesSharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingPublicMultiple", + "columnName": "sharing_public_multiple", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingPublicSupportsUploadOnly", + "columnName": "supports_upload_only", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesSharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesBigFileChunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "user_quotas", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))", + "fields": [ + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "used", + "columnName": "used", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "available", + "columnName": "available", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "accountName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "folder_backup", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `behavior` TEXT NOT NULL, `sourcePath` TEXT NOT NULL, `uploadPath` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, `name` TEXT NOT NULL, `lastSyncTimestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "behavior", + "columnName": "behavior", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourcePath", + "columnName": "sourcePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploadPath", + "columnName": "uploadPath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifiOnly", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSyncTimestamp", + "columnName": "lastSyncTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '23b7b67de0e6641230799a550a78bde9')" + ] + } +} \ No newline at end of file diff --git a/owncloudData/src/main/java/com/owncloud/android/data/LocalStorageProvider.kt b/owncloudData/src/main/java/com/owncloud/android/data/LocalStorageProvider.kt index eaa1915ac71..0c4960fb4b8 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/LocalStorageProvider.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/LocalStorageProvider.kt @@ -26,6 +26,7 @@ package com.owncloud.android.data import android.annotation.SuppressLint import android.net.Uri import android.os.Environment +import androidx.documentfile.provider.DocumentFile import java.io.File class LocalStorageProvider( @@ -56,6 +57,12 @@ class LocalStorageProvider( @SuppressLint("UsableSpace") fun getUsableSpace(): Long = getPrimaryStorageDirectory().usableSpace + fun getDefaultCameraSourcePath(): String { + return DocumentFile.fromFile( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + ).createDirectory(CAMERA_FOLDER)?.uri.toString() + } + /** * Return the root path of primary shared/external storage directory for this application. * For example: /storage/emulated/0/owncloud @@ -73,4 +80,8 @@ class LocalStorageProvider( * that can be in the accountName since 0.1.190B */ private fun getEncodedAccountName(accountName: String?): String = Uri.encode(accountName, "@") + + companion object { + private const val CAMERA_FOLDER = "/Camera" + } } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/OwncloudDatabase.kt b/owncloudData/src/main/java/com/owncloud/android/data/OwncloudDatabase.kt index ecc7515bed7..e61f7ef74cc 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/OwncloudDatabase.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/OwncloudDatabase.kt @@ -27,6 +27,8 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.migration.Migration +import com.owncloud.android.data.folderbackup.db.FolderBackupDao +import com.owncloud.android.data.folderbackup.db.FolderBackUpEntity import com.owncloud.android.data.capabilities.db.OCCapabilityDao import com.owncloud.android.data.capabilities.db.OCCapabilityEntity import com.owncloud.android.data.migrations.MIGRATION_27_28 @@ -35,6 +37,7 @@ import com.owncloud.android.data.migrations.MIGRATION_29_30 import com.owncloud.android.data.migrations.MIGRATION_30_31 import com.owncloud.android.data.migrations.MIGRATION_31_32 import com.owncloud.android.data.migrations.MIGRATION_32_33 +import com.owncloud.android.data.migrations.MIGRATION_33_34 import com.owncloud.android.data.sharing.shares.db.OCShareDao import com.owncloud.android.data.sharing.shares.db.OCShareEntity import com.owncloud.android.data.user.db.UserDao @@ -44,7 +47,8 @@ import com.owncloud.android.data.user.db.UserQuotaEntity entities = [ OCShareEntity::class, OCCapabilityEntity::class, - UserQuotaEntity::class + UserQuotaEntity::class, + FolderBackUpEntity::class, ], version = ProviderMeta.DB_VERSION, exportSchema = true @@ -53,6 +57,7 @@ abstract class OwncloudDatabase : RoomDatabase() { abstract fun shareDao(): OCShareDao abstract fun capabilityDao(): OCCapabilityDao abstract fun userDao(): UserDao + abstract fun folderBackUpDao(): FolderBackupDao companion object { @Volatile @@ -64,7 +69,8 @@ abstract class OwncloudDatabase : RoomDatabase() { MIGRATION_29_30, MIGRATION_30_31, MIGRATION_31_32, - MIGRATION_32_33 + MIGRATION_32_33, + MIGRATION_33_34 ) fun getDatabase( diff --git a/owncloudData/src/main/java/com/owncloud/android/data/ProviderMeta.java b/owncloudData/src/main/java/com/owncloud/android/data/ProviderMeta.java index fae486fb25f..93320eca228 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/ProviderMeta.java +++ b/owncloudData/src/main/java/com/owncloud/android/data/ProviderMeta.java @@ -31,7 +31,7 @@ public class ProviderMeta { public static final String DB_NAME = "filelist"; public static final String NEW_DB_NAME = "owncloud_database"; - public static final int DB_VERSION = 33; + public static final int DB_VERSION = 34; private ProviderMeta() { } @@ -40,6 +40,7 @@ static public class ProviderTableMeta implements BaseColumns { public static final String OCSHARES_TABLE_NAME = "ocshares"; public static final String CAPABILITIES_TABLE_NAME = "capabilities"; public static final String USER_QUOTAS_TABLE_NAME = "user_quotas"; + public static final String FOLDER_BACKUP_TABLE_NAME = "folder_backup"; // Columns of ocshares table public static final String OCSHARES_SHARE_TYPE = "share_type"; diff --git a/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/FolderBackupRepositoryImpl.kt b/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/FolderBackupRepositoryImpl.kt new file mode 100644 index 00000000000..d4c3335959e --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/FolderBackupRepositoryImpl.kt @@ -0,0 +1,44 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.data.folderbackup + +import com.owncloud.android.data.folderbackup.datasources.FolderBackupLocalDataSource +import com.owncloud.android.domain.camerauploads.FolderBackupRepository +import com.owncloud.android.domain.camerauploads.model.CameraUploadsConfiguration +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration +import kotlinx.coroutines.flow.Flow + +class FolderBackupRepositoryImpl( + private val folderBackupLocalDataSource: FolderBackupLocalDataSource +) : FolderBackupRepository { + + override fun getCameraUploadsConfiguration(): CameraUploadsConfiguration? = + folderBackupLocalDataSource.getCameraUploadsConfiguration() + + override fun getFolderBackupConfigurationStreamByName(name: String): Flow = + folderBackupLocalDataSource.getFolderBackupConfigurationStreamByName(name) + + override fun saveFolderBackupConfiguration(folderBackUpConfiguration: FolderBackUpConfiguration) { + folderBackupLocalDataSource.saveFolderBackupConfiguration(folderBackUpConfiguration) + } + + override fun resetFolderBackupConfigurationByName(name: String) = + folderBackupLocalDataSource.resetFolderBackupConfigurationByName(name) + +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/datasources/FolderBackupLocalDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/datasources/FolderBackupLocalDataSource.kt new file mode 100644 index 00000000000..102119cb62c --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/datasources/FolderBackupLocalDataSource.kt @@ -0,0 +1,33 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.data.folderbackup.datasources + +import com.owncloud.android.domain.camerauploads.model.CameraUploadsConfiguration +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration +import kotlinx.coroutines.flow.Flow + +interface FolderBackupLocalDataSource { + fun getCameraUploadsConfiguration(): CameraUploadsConfiguration? + + fun getFolderBackupConfigurationStreamByName(name: String): Flow + + fun saveFolderBackupConfiguration(folderBackUpConfiguration: FolderBackUpConfiguration) + + fun resetFolderBackupConfigurationByName(name: String) +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/datasources/implementation/FolderBackupLocalDataSourceImpl.kt b/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/datasources/implementation/FolderBackupLocalDataSourceImpl.kt new file mode 100644 index 00000000000..7f022f6a3a0 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/datasources/implementation/FolderBackupLocalDataSourceImpl.kt @@ -0,0 +1,82 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.data.folderbackup.datasources.implementation + +import com.owncloud.android.data.folderbackup.datasources.FolderBackupLocalDataSource +import com.owncloud.android.data.folderbackup.db.FolderBackUpEntity +import com.owncloud.android.data.folderbackup.db.FolderBackupDao +import com.owncloud.android.domain.camerauploads.model.CameraUploadsConfiguration +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration.Companion.pictureUploadsName +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration.Companion.videoUploadsName +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class FolderBackupLocalDataSourceImpl( + private val folderBackupDao: FolderBackupDao, +) : FolderBackupLocalDataSource { + + override fun getCameraUploadsConfiguration(): CameraUploadsConfiguration? { + val pictureUploadsConfiguration = folderBackupDao.getFolderBackUpConfigurationByName(pictureUploadsName) + val videoUploadsConfiguration = folderBackupDao.getFolderBackUpConfigurationByName(videoUploadsName) + + if (pictureUploadsConfiguration == null && videoUploadsConfiguration == null) return null + + return CameraUploadsConfiguration( + pictureUploadsConfiguration = pictureUploadsConfiguration?.toModel(), + videoUploadsConfiguration = videoUploadsConfiguration?.toModel(), + ) + } + + override fun getFolderBackupConfigurationStreamByName(name: String): Flow = + folderBackupDao.getFolderBackUpConfigurationByNameStream(name = name).map { it?.toModel() } + + override fun saveFolderBackupConfiguration(folderBackUpConfiguration: FolderBackUpConfiguration) { + folderBackupDao.update(folderBackUpConfiguration.toEntity()) + } + + override fun resetFolderBackupConfigurationByName(name: String) { + folderBackupDao.delete(name) + } + + /************************************************************************************************************** + ************************************************* Mappers **************************************************** + **************************************************************************************************************/ + private fun FolderBackUpEntity.toModel() = + FolderBackUpConfiguration( + accountName = accountName, + behavior = FolderBackUpConfiguration.Behavior.fromString(behavior), + sourcePath = sourcePath, + uploadPath = uploadPath, + wifiOnly = wifiOnly, + lastSyncTimestamp = lastSyncTimestamp, + name = name + ) + + private fun FolderBackUpConfiguration.toEntity(): FolderBackUpEntity = + FolderBackUpEntity( + accountName = accountName, + behavior = behavior.toString(), + sourcePath = sourcePath, + uploadPath = uploadPath, + wifiOnly = wifiOnly, + name = name, + lastSyncTimestamp = lastSyncTimestamp + ) +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/db/FolderBackUpEntity.kt b/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/db/FolderBackUpEntity.kt new file mode 100644 index 00000000000..429b8e09463 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/db/FolderBackUpEntity.kt @@ -0,0 +1,41 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.data.folderbackup.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.owncloud.android.data.ProviderMeta + +@Entity(tableName = ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME) +data class FolderBackUpEntity( + val accountName: String, + val behavior: String, + val sourcePath: String, + val uploadPath: String, + val wifiOnly: Boolean, + @ColumnInfo(name = folderBackUpEntityNameField) val name: String, + val lastSyncTimestamp: Long, +) { + @PrimaryKey(autoGenerate = true) var id: Int = 0 + + companion object { + internal const val folderBackUpEntityNameField = "name" + } +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/db/FolderBackupDao.kt b/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/db/FolderBackupDao.kt new file mode 100644 index 00000000000..c4893d300fe --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/folderbackup/db/FolderBackupDao.kt @@ -0,0 +1,61 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.data.folderbackup.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.owncloud.android.data.ProviderMeta +import kotlinx.coroutines.flow.Flow + +@Dao +abstract class FolderBackupDao { + @Query( + "SELECT * from " + ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME + " WHERE " + + FolderBackUpEntity.folderBackUpEntityNameField + " = :name" + ) + abstract fun getFolderBackUpConfigurationByName( + name: String + ): FolderBackUpEntity? + + @Query( + "SELECT * from " + ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME + " WHERE " + + FolderBackUpEntity.folderBackUpEntityNameField + " = :name" + ) + abstract fun getFolderBackUpConfigurationByNameStream( + name: String + ): Flow + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insert(folderBackUpEntity: FolderBackUpEntity): Long + + @Transaction + open fun update(folderBackUpEntity: FolderBackUpEntity): Long { + delete(folderBackUpEntity.name) + return insert(folderBackUpEntity) + } + + @Query( + "DELETE from " + ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME + " WHERE " + + FolderBackUpEntity.folderBackUpEntityNameField + " = :name" + ) + abstract fun delete(name: String): Int +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/migrations/Migration_34.kt b/owncloudData/src/main/java/com/owncloud/android/data/migrations/Migration_34.kt new file mode 100644 index 00000000000..ec6d3ff75a7 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/migrations/Migration_34.kt @@ -0,0 +1,103 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2020 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.owncloud.android.data.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.owncloud.android.data.ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME +import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration.Companion.pictureUploadsName +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration.Companion.videoUploadsName +import java.io.File + +val MIGRATION_33_34 = object : Migration(33, 34) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `$FOLDER_BACKUP_TABLE_NAME` (`accountName` TEXT NOT NULL, `behavior` TEXT NOT NULL, `sourcePath` TEXT NOT NULL, `uploadPath` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, `name` TEXT NOT NULL, `lastSyncTimestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)") + } +} + +@Deprecated("Legacy code. Only used to migrate old camera uploads configuration from ") +class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferencesProvider) { + + fun getPictureUploadsConfigurationPreferences(timestamp: Long): FolderBackUpConfiguration? { + + if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false)) return null + + return FolderBackUpConfiguration( + accountName = sharedPreferencesProvider.getString(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME, null) ?: "", + wifiOnly = sharedPreferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY, false), + uploadPath = getUploadPathForPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH), + sourcePath = getSourcePathForPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE), + behavior = getBehaviorForPreference(PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR), + lastSyncTimestamp = timestamp, + name = pictureUploadsName, + ) + } + + fun getVideoUploadsConfigurationPreferences(timestamp: Long): FolderBackUpConfiguration? { + if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false)) return null + + return FolderBackUpConfiguration( + accountName = sharedPreferencesProvider.getString(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME, null) ?: "", + wifiOnly = sharedPreferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY, false), + uploadPath = getUploadPathForPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH), + sourcePath = getSourcePathForPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE), + behavior = getBehaviorForPreference(PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR), + lastSyncTimestamp = timestamp, + name = videoUploadsName, + ) + } + + private fun getUploadPathForPreference(keyPreference: String): String { + val uploadPath = sharedPreferencesProvider.getString( + key = keyPreference, + defaultValue = DEFAULT_PATH_FOR_CAMERA_UPLOADS + File.separator + ) + return if (uploadPath!!.endsWith(File.separator)) uploadPath else uploadPath + File.separator + } + + private fun getSourcePathForPreference(keyPreference: String): String { + return sharedPreferencesProvider.getString(keyPreference, null) ?: "" + } + + private fun getBehaviorForPreference(keyPreference: String): FolderBackUpConfiguration.Behavior { + val storedBehaviour = sharedPreferencesProvider.getString(keyPreference, null) ?: return FolderBackUpConfiguration.Behavior.COPY + + return FolderBackUpConfiguration.Behavior.fromString(storedBehaviour) + } + + companion object { + private const val PREF__CAMERA_PICTURE_UPLOADS_ENABLED = "enable_picture_uploads" + private const val PREF__CAMERA_VIDEO_UPLOADS_ENABLED = "enable_video_uploads" + private const val PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY = "picture_uploads_on_wifi" + private const val PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY = "video_uploads_on_wifi" + private const val PREF__CAMERA_PICTURE_UPLOADS_PATH = "picture_uploads_path" + private const val PREF__CAMERA_VIDEO_UPLOADS_PATH = "video_uploads_path" + private const val PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR = "picture_uploads_behaviour" + private const val PREF__CAMERA_PICTURE_UPLOADS_SOURCE = "picture_uploads_source_path" + private const val PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR = "video_uploads_behaviour" + private const val PREF__CAMERA_VIDEO_UPLOADS_SOURCE = "video_uploads_source_path" + private const val PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME = "picture_uploads_account_name" + private const val PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME = "video_uploads_account_name" + + private const val DEFAULT_PATH_FOR_CAMERA_UPLOADS = "/CameraUpload" + } +} diff --git a/owncloudDomain/build.gradle b/owncloudDomain/build.gradle index 968a0aedf67..b7b681004ae 100644 --- a/owncloudDomain/build.gradle +++ b/owncloudDomain/build.gradle @@ -39,6 +39,7 @@ dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" // Dependencies for unit tests testImplementation project(':owncloudTestUtil') diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/FolderBackupRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/FolderBackupRepository.kt new file mode 100644 index 00000000000..7d7734121fd --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/FolderBackupRepository.kt @@ -0,0 +1,33 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.camerauploads + +import com.owncloud.android.domain.camerauploads.model.CameraUploadsConfiguration +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration +import kotlinx.coroutines.flow.Flow + +interface FolderBackupRepository { + fun getCameraUploadsConfiguration(): CameraUploadsConfiguration? + + fun getFolderBackupConfigurationStreamByName(name: String): Flow + + fun saveFolderBackupConfiguration(folderBackUpConfiguration: FolderBackUpConfiguration) + + fun resetFolderBackupConfigurationByName(name: String) +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/model/CameraUploadsConfiguration.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/model/CameraUploadsConfiguration.kt new file mode 100644 index 00000000000..7fccecc74f7 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/model/CameraUploadsConfiguration.kt @@ -0,0 +1,26 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.camerauploads.model + +data class CameraUploadsConfiguration( + val pictureUploadsConfiguration: FolderBackUpConfiguration?, + val videoUploadsConfiguration: FolderBackUpConfiguration? +) { + fun areCameraUploadsDisabled() = pictureUploadsConfiguration == null && videoUploadsConfiguration == null +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/model/FolderBackUpConfiguration.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/model/FolderBackUpConfiguration.kt new file mode 100644 index 00000000000..c8b977a9762 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/model/FolderBackUpConfiguration.kt @@ -0,0 +1,52 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.camerauploads.model + +data class FolderBackUpConfiguration( + val accountName: String, + val behavior: Behavior, + val sourcePath: String, + val uploadPath: String, + val wifiOnly: Boolean, + val lastSyncTimestamp: Long, + val name: String, +) { + + val isPictureUploads get() = name == pictureUploadsName + val isVideoUploads get() = name == videoUploadsName + + companion object { + const val pictureUploadsName = "Picture uploads" + const val videoUploadsName = "Video uploads" + } + + enum class Behavior { + MOVE, COPY; + + companion object { + fun fromString(string: String): Behavior { + return if (string.equals("MOVE", ignoreCase = true)) { + MOVE + } else { + COPY + } + } + } + } +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/GetCameraUploadsConfigurationUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/GetCameraUploadsConfigurationUseCase.kt new file mode 100644 index 00000000000..73497f5cc43 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/GetCameraUploadsConfigurationUseCase.kt @@ -0,0 +1,31 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.camerauploads.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.camerauploads.FolderBackupRepository +import com.owncloud.android.domain.camerauploads.model.CameraUploadsConfiguration + +class GetCameraUploadsConfigurationUseCase( + private val folderBackupRepository: FolderBackupRepository +) : BaseUseCaseWithResult() { + + override fun run(params: Unit): CameraUploadsConfiguration? = + folderBackupRepository.getCameraUploadsConfiguration() +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/GetPictureUploadsConfigurationStreamUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/GetPictureUploadsConfigurationStreamUseCase.kt new file mode 100644 index 00000000000..baa286f53c9 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/GetPictureUploadsConfigurationStreamUseCase.kt @@ -0,0 +1,32 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.camerauploads.usecases + +import com.owncloud.android.domain.BaseUseCase +import com.owncloud.android.domain.camerauploads.FolderBackupRepository +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration +import kotlinx.coroutines.flow.Flow + +class GetPictureUploadsConfigurationStreamUseCase( + private val folderBackupRepository: FolderBackupRepository +) : BaseUseCase, Unit>() { + + override fun run(params: Unit): Flow = + folderBackupRepository.getFolderBackupConfigurationStreamByName(FolderBackUpConfiguration.pictureUploadsName) +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/GetVideoUploadsConfigurationStreamUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/GetVideoUploadsConfigurationStreamUseCase.kt new file mode 100644 index 00000000000..2af192b2c71 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/GetVideoUploadsConfigurationStreamUseCase.kt @@ -0,0 +1,33 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.camerauploads.usecases + +import com.owncloud.android.domain.BaseUseCase +import com.owncloud.android.domain.camerauploads.FolderBackupRepository +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration.Companion.videoUploadsName +import kotlinx.coroutines.flow.Flow + +class GetVideoUploadsConfigurationStreamUseCase( + private val folderBackupRepository: FolderBackupRepository +) : BaseUseCase, Unit>() { + + override fun run(params: Unit): Flow = + folderBackupRepository.getFolderBackupConfigurationStreamByName(videoUploadsName) +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/ResetPictureUploadsUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/ResetPictureUploadsUseCase.kt new file mode 100644 index 00000000000..615b8828bbe --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/ResetPictureUploadsUseCase.kt @@ -0,0 +1,31 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.camerauploads.usecases + +import com.owncloud.android.domain.BaseUseCase +import com.owncloud.android.domain.camerauploads.FolderBackupRepository +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration.Companion.pictureUploadsName + +class ResetPictureUploadsUseCase( + private val folderBackupRepository: FolderBackupRepository +) : BaseUseCase() { + + override fun run(params: Unit) = + folderBackupRepository.resetFolderBackupConfigurationByName(pictureUploadsName) +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/ResetVideoUploadsUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/ResetVideoUploadsUseCase.kt new file mode 100644 index 00000000000..78b908bf546 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/ResetVideoUploadsUseCase.kt @@ -0,0 +1,31 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.camerauploads.usecases + +import com.owncloud.android.domain.BaseUseCase +import com.owncloud.android.domain.camerauploads.FolderBackupRepository +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration.Companion.videoUploadsName + +class ResetVideoUploadsUseCase( + private val folderBackupRepository: FolderBackupRepository +) : BaseUseCase() { + + override fun run(params: Unit) = + folderBackupRepository.resetFolderBackupConfigurationByName(videoUploadsName) +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/SavePictureUploadsConfigurationUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/SavePictureUploadsConfigurationUseCase.kt new file mode 100644 index 00000000000..198bdec59d4 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/SavePictureUploadsConfigurationUseCase.kt @@ -0,0 +1,35 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.camerauploads.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.camerauploads.FolderBackupRepository +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration + +class SavePictureUploadsConfigurationUseCase( + private val folderBackupRepository: FolderBackupRepository +) : BaseUseCaseWithResult() { + + override fun run(params: Params) = + folderBackupRepository.saveFolderBackupConfiguration(params.pictureUploadsConfiguration) + + data class Params( + val pictureUploadsConfiguration: FolderBackUpConfiguration + ) +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/SaveVideoUploadsConfigurationUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/SaveVideoUploadsConfigurationUseCase.kt new file mode 100644 index 00000000000..7759101de0d --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/camerauploads/usecases/SaveVideoUploadsConfigurationUseCase.kt @@ -0,0 +1,35 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.camerauploads.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.camerauploads.FolderBackupRepository +import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration + +class SaveVideoUploadsConfigurationUseCase( + private val folderBackupRepository: FolderBackupRepository +) : BaseUseCaseWithResult() { + + override fun run(params: Params) = + folderBackupRepository.saveFolderBackupConfiguration(params.videoUploadsConfiguration) + + data class Params( + val videoUploadsConfiguration: FolderBackUpConfiguration + ) +}