diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 369b7acfc146..5e607ef71d83 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -12,10 +12,6 @@ on: permissions: contents: read -concurrency: - group: code-ql-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - jobs: analyze: name: Analyze @@ -36,7 +32,7 @@ jobs: with: swap-size-gb: 10 - name: Initialize CodeQL - uses: github/codeql-action/init@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 + uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: languages: ${{ matrix.language }} - name: Set up JDK 17 @@ -50,4 +46,4 @@ jobs: echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties" ./gradlew assembleDebug - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 + uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index bd873fbb8143..e6664f0039ef 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -37,6 +37,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 + uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: sarif_file: results.sarif diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png b/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png index d4cf910a8206..c99c2f34db84 100644 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png and b/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open.png index f619ffc11ae9..835efe299a27 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png index f0ebdef02cc3..7af92ad864f2 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png index 5522a71e90c6..c6c31f47304e 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png index 96f049f238c6..86560bebc32e 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog.png index e451319a42a4..bb93a174e2db 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog.png index caa860dc2b97..f61b09696988 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog.png index ff264659676c..ef0b1d7912b4 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog.png index caa860dc2b97..f61b09696988 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog.png differ diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java index 68bd9cc9d452..98fe9c75e576 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java @@ -7,6 +7,8 @@ import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; @@ -58,6 +60,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Collection; +import java.util.Locale; import java.util.Objects; import androidx.annotation.NonNull; @@ -403,6 +406,21 @@ public boolean isPowerSavingExclusionAvailable() { assertTrue(result.getLogMessage(), result.isSuccess()); } + protected void enableRTL() { + Locale locale = new Locale("ar"); + Resources resources = InstrumentationRegistry.getInstrumentation().getTargetContext().getResources(); + Configuration config = resources.getConfiguration(); + config.setLocale(locale); + resources.updateConfiguration(config, null); + } + + protected void resetLocale() { + Resources resources = InstrumentationRegistry.getInstrumentation().getTargetContext().getResources(); + Configuration defaultConfig = resources.getConfiguration(); + defaultConfig.setLocale(Locale.getDefault()); + resources.updateConfiguration(defaultConfig, null); + } + protected void screenshot(View view) { screenshot(view, ""); } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.java b/app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.java index a5fb7319f75d..934233c7f2bd 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.java +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.java @@ -130,7 +130,7 @@ public void open() { sut.setFile(origin); sut.runOnUiThread(() -> { - sut.findViewById(R.id.folder_picker_btn_choose).requestFocus(); + sut.findViewById(R.id.folder_picker_btn_copy).requestFocus(); }); waitForIdleSync(); screenshot(sut); diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java index 1a2bd2367fdf..3853215b7754 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java @@ -92,6 +92,7 @@ import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; import androidx.test.espresso.intent.rule.IntentsTestRule; +import androidx.test.rule.GrantPermissionRule; import kotlin.Unit; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; @@ -108,6 +109,9 @@ private FileDisplayActivity getFileDisplayActivity() { return activityRule.launchActivity(intent); } + @Rule + public GrantPermissionRule permissionRule = GrantPermissionRule.grant( + android.Manifest.permission.POST_NOTIFICATIONS); @After public void quitLooperIfNeeded() { @@ -134,6 +138,38 @@ public void testLoadingDialog() { showDialog(dialog); } + @Test + @ScreenshotTest + public void testConfirmationDialogWithOneAction() { + ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[]{}, R.string.filedetails_sync_file, R.string.common_ok, -1, -1); + showDialog(dialog); + } + + @Test + @ScreenshotTest + public void testConfirmationDialogWithTwoAction() { + ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[]{}, R.string.filedetails_sync_file, R.string.common_ok, R.string.common_cancel, -1); + showDialog(dialog); + } + + @Test + @ScreenshotTest + public void testConfirmationDialogWithThreeAction() { + ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[]{}, R.string.filedetails_sync_file, R.string.common_ok, R.string.common_cancel, R.string.common_confirm); + showDialog(dialog); + } + + @Test + @ScreenshotTest + public void testConfirmationDialogWithThreeActionRTL() { + enableRTL(); + + ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[] { }, -1, R.string.common_ok, R.string.common_cancel, R.string.common_confirm); + showDialog(dialog); + + resetLocale(); + } + @Test @ScreenshotTest public void testRemoveFileDialog() { diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java index 5e715a0a6e0f..7e1b45d86736 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java @@ -45,6 +45,7 @@ public void showNotEnoughSpaceDialogForFolder() { FileDisplayActivity test = activityRule.launchActivity(null); OCFile ocFile = new OCFile("/Document/"); ocFile.setFileLength(5000000); + ocFile.setFolder(); SyncFileNotEnoughSpaceDialogFragment dialog = SyncFileNotEnoughSpaceDialogFragment.newInstance(ocFile, 1000); dialog.show(test.getListOfFilesFragment().getFragmentManager(), "1"); diff --git a/app/src/main/java/com/nextcloud/client/di/FragmentInjector.java b/app/src/main/java/com/nextcloud/client/di/FragmentInjector.kt similarity index 58% rename from app/src/main/java/com/nextcloud/client/di/FragmentInjector.java rename to app/src/main/java/com/nextcloud/client/di/FragmentInjector.kt index 750f7fb28161..02f759d68ba1 100644 --- a/app/src/main/java/com/nextcloud/client/di/FragmentInjector.java +++ b/app/src/main/java/com/nextcloud/client/di/FragmentInjector.kt @@ -17,30 +17,26 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +package com.nextcloud.client.di -package com.nextcloud.client.di; +import android.content.Context +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import dagger.android.support.AndroidSupportInjection -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import dagger.android.support.AndroidSupportInjection; - -class FragmentInjector extends FragmentManager.FragmentLifecycleCallbacks { - @Override - public void onFragmentPreAttached( - @NonNull FragmentManager fragmentManager, - @NonNull Fragment fragment, - @NonNull Context context +internal class FragmentInjector : FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentPreAttached( + fragmentManager: FragmentManager, + fragment: Fragment, + context: Context ) { - super.onFragmentPreAttached(fragmentManager, fragment, context); - if (fragment instanceof Injectable) { + super.onFragmentPreAttached(fragmentManager, fragment, context) + if (fragment is Injectable) { try { - AndroidSupportInjection.inject(fragment); - } catch (IllegalArgumentException directCause) { + AndroidSupportInjection.inject(fragment) + } catch (directCause: IllegalArgumentException) { // this provides a cause description that is a bit more friendly for developers - throw new InjectorNotFoundException(fragment, directCause); + throw InjectorNotFoundException(fragment, directCause) } } } diff --git a/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.java b/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.kt similarity index 65% rename from app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.java rename to app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.kt index 5eb7b27d4a02..9b341332dfe3 100644 --- a/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.java +++ b/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.kt @@ -17,23 +17,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +package com.nextcloud.client.integrations.deck -package com.nextcloud.client.integrations.deck; - -import android.app.PendingIntent; - -import com.nextcloud.client.account.User; -import com.nextcloud.java.util.Optional; -import com.owncloud.android.lib.resources.notifications.models.Notification; - -import androidx.annotation.NonNull; +import android.app.PendingIntent +import com.nextcloud.client.account.User +import com.nextcloud.java.util.Optional +import com.owncloud.android.lib.resources.notifications.models.Notification /** - * This API is for an integration with the Nextcloud - * Deck app for android. + * This API is for an integration with the [Nextcloud + * Deck](https://github.com/stefan-niedermann/nextcloud-deck) app for android. */ -public interface DeckApi { - +interface DeckApi { /** * Creates a PendingIntent that can be used in a NotificationBuilder to open the notification link in Deck app * @@ -41,9 +36,10 @@ public interface DeckApi { * @param user The user that is affected by the notification * @return If notification can be consumed by Deck, a PendingIntent opening notification link in Deck app; empty * value otherwise - * @see Deck Server App + * @see [Deck Server App](https://apps.nextcloud.com/apps/deck) */ - @NonNull - Optional createForwardToDeckActionIntent(@NonNull final Notification notification, - @NonNull final User user); + fun createForwardToDeckActionIntent( + notification: Notification, + user: User + ): Optional } diff --git a/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.java b/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.java deleted file mode 100644 index e9772952cd21..000000000000 --- a/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Nextcloud application - * - * @author Stefan Niedermann - * Copyright (C) 2020 Stefan Niedermann - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * 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.nextcloud.client.integrations.deck; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; - -import com.nextcloud.client.account.User; -import com.nextcloud.java.util.Optional; -import com.owncloud.android.lib.resources.notifications.models.Notification; - -import androidx.annotation.NonNull; - -public class DeckApiImpl implements DeckApi { - - static final String APP_NAME = "deck"; - static final String[] DECK_APP_PACKAGES = new String[] { - "it.niedermann.nextcloud.deck", - "it.niedermann.nextcloud.deck.play", - "it.niedermann.nextcloud.deck.dev" - }; - static final String DECK_ACTIVITY_TO_START = "it.niedermann.nextcloud.deck.ui.PushNotificationActivity"; - - private static final String EXTRA_ACCOUNT = "account"; - private static final String EXTRA_LINK = "link"; - private static final String EXTRA_OBJECT_ID = "objectId"; - private static final String EXTRA_SUBJECT = "subject"; - private static final String EXTRA_SUBJECT_RICH = "subjectRich"; - private static final String EXTRA_MESSAGE = "message"; - private static final String EXTRA_MESSAGE_RICH = "messageRich"; - private static final String EXTRA_USER = "user"; - private static final String EXTRA_NID = "nid"; - - private final Context context; - private final PackageManager packageManager; - - public DeckApiImpl(@NonNull Context context, @NonNull PackageManager packageManager) { - this.context = context; - this.packageManager = packageManager; - } - - @NonNull - @Override - public Optional createForwardToDeckActionIntent(@NonNull Notification notification, @NonNull User user) { - if (APP_NAME.equalsIgnoreCase(notification.app)) { - final Intent intent = new Intent(); - for (String appPackage : DECK_APP_PACKAGES) { - intent.setClassName(appPackage, DECK_ACTIVITY_TO_START); - if (packageManager.resolveActivity(intent, 0) != null) { - return Optional.of(createPendingIntent(intent, notification, user)); - } - } - } - return Optional.empty(); - } - - private PendingIntent createPendingIntent(@NonNull Intent intent, @NonNull Notification notification, @NonNull User user) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return PendingIntent.getActivity(context, notification.getNotificationId(), - putExtrasToIntent(intent, notification, user), - PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); - } - - private Intent putExtrasToIntent(@NonNull Intent intent, @NonNull Notification notification, @NonNull User user) { - return intent - .putExtra(EXTRA_ACCOUNT, user.getAccountName()) - .putExtra(EXTRA_LINK, notification.getLink()) - .putExtra(EXTRA_OBJECT_ID, notification.getObjectId()) - .putExtra(EXTRA_SUBJECT, notification.getSubject()) - .putExtra(EXTRA_SUBJECT_RICH, notification.getSubjectRich()) - .putExtra(EXTRA_MESSAGE, notification.getMessage()) - .putExtra(EXTRA_MESSAGE_RICH, notification.getMessageRich()) - .putExtra(EXTRA_USER, notification.getUser()) - .putExtra(EXTRA_NID, notification.getNotificationId()); - } -} diff --git a/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.kt b/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.kt new file mode 100644 index 000000000000..75226a0e0447 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.kt @@ -0,0 +1,85 @@ +/* + * Nextcloud application + * + * @author Stefan Niedermann + * Copyright (C) 2020 Stefan Niedermann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * 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.nextcloud.client.integrations.deck + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import com.nextcloud.client.account.User +import com.nextcloud.java.util.Optional +import com.owncloud.android.lib.resources.notifications.models.Notification + +class DeckApiImpl(private val context: Context, private val packageManager: PackageManager) : DeckApi { + override fun createForwardToDeckActionIntent(notification: Notification, user: User): Optional { + if (APP_NAME.equals(notification.app, ignoreCase = true)) { + val intent = Intent() + for (appPackage in DECK_APP_PACKAGES) { + intent.setClassName(appPackage, DECK_ACTIVITY_TO_START) + if (packageManager.resolveActivity(intent, 0) != null) { + return Optional.of(createPendingIntent(intent, notification, user)) + } + } + } + return Optional.empty() + } + + private fun createPendingIntent(intent: Intent, notification: Notification, user: User): PendingIntent { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + return PendingIntent.getActivity( + context, + notification.getNotificationId(), + putExtrasToIntent(intent, notification, user), + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) + } + + private fun putExtrasToIntent(intent: Intent, notification: Notification, user: User): Intent { + return intent + .putExtra(EXTRA_ACCOUNT, user.accountName) + .putExtra(EXTRA_LINK, notification.getLink()) + .putExtra(EXTRA_OBJECT_ID, notification.getObjectId()) + .putExtra(EXTRA_SUBJECT, notification.getSubject()) + .putExtra(EXTRA_SUBJECT_RICH, notification.getSubjectRich()) + .putExtra(EXTRA_MESSAGE, notification.getMessage()) + .putExtra(EXTRA_MESSAGE_RICH, notification.getMessageRich()) + .putExtra(EXTRA_USER, notification.getUser()) + .putExtra(EXTRA_NID, notification.getNotificationId()) + } + + companion object { + const val APP_NAME = "deck" + val DECK_APP_PACKAGES = arrayOf( + "it.niedermann.nextcloud.deck", + "it.niedermann.nextcloud.deck.play", + "it.niedermann.nextcloud.deck.dev" + ) + const val DECK_ACTIVITY_TO_START = "it.niedermann.nextcloud.deck.ui.PushNotificationActivity" + private const val EXTRA_ACCOUNT = "account" + private const val EXTRA_LINK = "link" + private const val EXTRA_OBJECT_ID = "objectId" + private const val EXTRA_SUBJECT = "subject" + private const val EXTRA_SUBJECT_RICH = "subjectRich" + private const val EXTRA_MESSAGE = "message" + private const val EXTRA_MESSAGE_RICH = "messageRich" + private const val EXTRA_USER = "user" + private const val EXTRA_NID = "nid" + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt b/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt index 0c4d2466c6c7..0e29f5e99cc4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt @@ -142,7 +142,7 @@ class NotificationWork constructor( val deckActionOverrideIntent = deckApi.createForwardToDeckActionIntent(notification, user) - val pendingIntent: PendingIntent + val pendingIntent: PendingIntent? if (deckActionOverrideIntent.isPresent) { pendingIntent = deckActionOverrideIntent.get() } else { diff --git a/app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.java b/app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.kt similarity index 55% rename from app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.java rename to app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.kt index 217ef9a3c817..0677d145b28c 100644 --- a/app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.java +++ b/app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.kt @@ -17,32 +17,21 @@ * You should have received a copy of the GNU Affero General Public * License along with this program. If not, see . */ +package com.nextcloud.ui -package com.nextcloud.ui; - -import android.content.Context; -import android.util.AttributeSet; - -import com.elyeproj.loaderviewlibrary.LoaderImageView; +import android.content.Context +import android.util.AttributeSet +import com.elyeproj.loaderviewlibrary.LoaderImageView /** * Square version of loader image. */ -class SquareLoaderImageView extends LoaderImageView { - public SquareLoaderImageView(Context context) { - super(context); - } - - public SquareLoaderImageView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public SquareLoaderImageView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } +internal class SquareLoaderImageView : LoaderImageView { + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, widthMeasureSpec); + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec) } } diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt index 024e07a89416..80f81bf57b8b 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt @@ -39,8 +39,7 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe // File moving RENAME_FILE(R.id.action_rename_file, R.string.common_rename, R.drawable.ic_rename), - MOVE(R.id.action_move, R.string.actionbar_move, R.drawable.ic_move), - COPY(R.id.action_copy, R.string.actionbar_copy, R.drawable.ic_content_copy), + MOVE_OR_COPY(R.id.action_move_or_copy, R.string.actionbar_move_or_copy, R.drawable.ic_external), // favorites FAVORITE(R.id.action_favorite, R.string.favorite, R.drawable.ic_star), @@ -83,8 +82,7 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe SEE_DETAILS, LOCK_FILE, RENAME_FILE, - MOVE, - COPY, + MOVE_OR_COPY, DOWNLOAD_FILE, EXPORT_FILE, STREAM_MEDIA, diff --git a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java index b02d41080c87..9a3b9a1ab088 100644 --- a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -125,6 +125,7 @@ import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.ErrorMessageAdapter; import com.owncloud.android.utils.PermissionUtil; +import com.owncloud.android.utils.WebViewUtil; import com.owncloud.android.utils.theme.CapabilityUtils; import com.owncloud.android.utils.theme.ViewThemeUtils; @@ -268,6 +269,7 @@ protected void onCreate(Bundle savedInstanceState) { viewThemeUtils = viewThemeUtilsFactory.withPrimaryAsBackground(); viewThemeUtils.platform.themeStatusBar(this, ColorRole.PRIMARY); + WebViewUtil webViewUtil = new WebViewUtil(this); Uri data = getIntent().getData(); boolean directLogin = data != null && data.toString().startsWith(getString(R.string.login_data_own_scheme)); @@ -337,6 +339,8 @@ protected void onCreate(Bundle savedInstanceState) { } initServerPreFragment(savedInstanceState); + + webViewUtil.checkWebViewVersion(); } private void deleteCookies() { diff --git a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java index b6c42dbe60a0..097c7808ded0 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java +++ b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java @@ -24,14 +24,11 @@ import android.content.ContentResolver; import android.content.Context; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; -import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.owncloud.android.R; import com.owncloud.android.lib.common.network.WebdavEntry; import com.owncloud.android.lib.common.network.WebdavUtils; @@ -41,9 +38,7 @@ import com.owncloud.android.lib.resources.files.model.ImageDimension; import com.owncloud.android.lib.resources.files.model.ServerFileInterface; import com.owncloud.android.lib.resources.shares.ShareeUser; -import com.owncloud.android.utils.DrawableUtil; import com.owncloud.android.utils.MimeType; -import com.owncloud.android.utils.theme.ViewThemeUtils; import java.io.File; import java.util.ArrayList; @@ -52,7 +47,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import third_parties.daveKoeller.AlphanumComparator; @@ -350,6 +344,13 @@ public boolean existsOnDevice() { return false; } + public String getFileNameWithExtension(int fileNameLength) { + String fileName = getFileName(); + String shortFileName = fileName.substring(0, Math.min(fileName.length(), fileNameLength)); + String extension = "." + fileName.substring(fileName.lastIndexOf('.') + 1); + return shortFileName + extension; + } + /** * The path, where the file is stored locally * diff --git a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java index f4092a986065..ecf266cc045c 100644 --- a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java +++ b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java @@ -161,8 +161,7 @@ private List filter(boolean inSingleFileFragment) { filterDownload(toHide, synchronizing); filterExport(toHide); filterRename(toHide, synchronizing); - filterCopy(toHide, synchronizing); - filterMove(toHide, synchronizing); + filterMoveOrCopy(toHide, synchronizing); filterRemove(toHide, synchronizing); filterSelectAll(toHide, inSingleFileFragment); filterDeselectAll(toHide, inSingleFileFragment); @@ -346,19 +345,12 @@ private void filterRemove(List toHide, boolean synchronizing) { } } - private void filterMove(List toHide, boolean synchronizing) { + private void filterMoveOrCopy(List toHide, boolean synchronizing) { if (files.isEmpty() || synchronizing || containsEncryptedFile() || containsEncryptedFolder() || containsLockedFile()) { - toHide.add(R.id.action_move); + toHide.add(R.id.action_move_or_copy); } } - private void filterCopy(List toHide, boolean synchronizing) { - if (files.isEmpty() || synchronizing || containsEncryptedFile() || containsEncryptedFolder()) { - toHide.add(R.id.action_copy); - } - } - - private void filterRename(Collection toHide, boolean synchronizing) { if (!isSingleSelection() || synchronizing || containsEncryptedFile() || containsEncryptedFolder() || containsLockedFile()) { toHide.add(R.id.action_rename_file); diff --git a/app/src/main/java/com/owncloud/android/files/services/FileDownloader.java b/app/src/main/java/com/owncloud/android/files/services/FileDownloader.java deleted file mode 100644 index cbe4d37aea17..000000000000 --- a/app/src/main/java/com/owncloud/android/files/services/FileDownloader.java +++ /dev/null @@ -1,741 +0,0 @@ -/* - * ownCloud Android client application - * - * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2016 ownCloud Inc. - * - * 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.accounts.AccountManager; -import android.accounts.OnAccountsUpdateListener; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Intent; -import android.graphics.BitmapFactory; -import android.os.Binder; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.Process; -import android.util.Pair; - -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.files.downloader.DownloadTask; -import com.nextcloud.java.util.Optional; -import com.owncloud.android.R; -import com.owncloud.android.authentication.AuthenticatorActivity; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.UploadsStorageManager; -import com.owncloud.android.lib.common.OwnCloudAccount; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; -import com.owncloud.android.lib.common.network.OnDatatransferProgressListener; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.FileUtils; -import com.owncloud.android.operations.DownloadFileOperation; -import com.owncloud.android.operations.DownloadType; -import com.owncloud.android.providers.DocumentsStorageProvider; -import com.owncloud.android.ui.activity.ConflictsResolveActivity; -import com.owncloud.android.ui.activity.FileActivity; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.ui.dialog.SendShareDialog; -import com.owncloud.android.ui.fragment.OCFileListFragment; -import com.owncloud.android.ui.notifications.NotificationUtils; -import com.owncloud.android.ui.preview.PreviewImageActivity; -import com.owncloud.android.ui.preview.PreviewImageFragment; -import com.owncloud.android.utils.ErrorMessageAdapter; -import com.owncloud.android.utils.MimeTypeUtil; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.io.File; -import java.security.SecureRandom; -import java.util.AbstractList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Vector; - -import javax.inject.Inject; - -import androidx.core.app.NotificationCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import dagger.android.AndroidInjection; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -public class FileDownloader extends Service - implements OnDatatransferProgressListener, OnAccountsUpdateListener { - - public static final String EXTRA_USER = "USER"; - public static final String EXTRA_FILE = "FILE"; - - private static final String DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED"; - private static final String DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH"; - public static final String EXTRA_DOWNLOAD_RESULT = "RESULT"; - public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH"; - public static final String EXTRA_LINKED_TO_PATH = "LINKED_TO"; - public static final String ACCOUNT_NAME = "ACCOUNT_NAME"; - public static final String DOWNLOAD_TYPE = "DOWNLOAD_TYPE"; - - private static final int FOREGROUND_SERVICE_ID = 412; - - private static final String TAG = FileDownloader.class.getSimpleName(); - - private Looper mServiceLooper; - private ServiceHandler mServiceHandler; - private IBinder mBinder; - private OwnCloudClient mDownloadClient; - private Optional currentUser = Optional.empty(); - private FileDataStorageManager mStorageManager; - - private IndexedForest mPendingDownloads = new IndexedForest<>(); - - private DownloadFileOperation mCurrentDownload; - - private NotificationManager mNotificationManager; - private NotificationCompat.Builder mNotificationBuilder; - private int mLastPercent; - - private Notification mNotification; - - private long conflictUploadId; - - public boolean mStartedDownload = false; - - @Inject UserAccountManager accountManager; - @Inject UploadsStorageManager uploadsStorageManager; - @Inject LocalBroadcastManager localBroadcastManager; - @Inject ViewThemeUtils viewThemeUtils; - - public static String getDownloadAddedMessage() { - return FileDownloader.class.getName() + DOWNLOAD_ADDED_MESSAGE; - } - - public static String getDownloadFinishMessage() { - return FileDownloader.class.getName() + DOWNLOAD_FINISH_MESSAGE; - } - - /** - * Service initialization - */ - @Override - public void onCreate() { - super.onCreate(); - AndroidInjection.inject(this); - Log_OC.d(TAG, "Creating service"); - mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - HandlerThread thread = new HandlerThread("FileDownloaderThread", Process.THREAD_PRIORITY_BACKGROUND); - thread.start(); - mServiceLooper = thread.getLooper(); - mServiceHandler = new ServiceHandler(mServiceLooper, this); - mBinder = new FileDownloaderBinder(); - - NotificationCompat.Builder builder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils).setContentTitle( - getApplicationContext().getResources().getString(R.string.app_name)) - .setContentText(getApplicationContext().getResources().getString(R.string.foreground_service_download)) - .setSmallIcon(R.drawable.notification_icon) - .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon)); - - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - builder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD); - } - - mNotification = builder.build(); - - // add AccountsUpdatedListener - AccountManager am = AccountManager.get(getApplicationContext()); - am.addOnAccountsUpdatedListener(this, null, false); - } - - - /** - * Service clean up - */ - @Override - public void onDestroy() { - Log_OC.v(TAG, "Destroying service"); - mBinder = null; - mServiceHandler = null; - mServiceLooper.quit(); - mServiceLooper = null; - mNotificationManager = null; - - // remove AccountsUpdatedListener - AccountManager am = AccountManager.get(getApplicationContext()); - am.removeOnAccountsUpdatedListener(this); - super.onDestroy(); - } - - - /** - * Entry point to add one or several files to the queue of downloads. - * - * New downloads are added calling to startService(), resulting in a call to this method. - * This ensures the service will keep on working although the caller activity goes away. - */ - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Log_OC.d(TAG, "Starting command with id " + startId); - - startForeground(FOREGROUND_SERVICE_ID, mNotification); - - if (intent == null || !intent.hasExtra(EXTRA_USER) || !intent.hasExtra(EXTRA_FILE)) { - Log_OC.e(TAG, "Not enough information provided in intent"); - return START_NOT_STICKY; - } else { - final User user = intent.getParcelableExtra(EXTRA_USER); - final OCFile file = intent.getParcelableExtra(EXTRA_FILE); - final String behaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR); - - DownloadType downloadType = DownloadType.DOWNLOAD; - if (intent.hasExtra(DOWNLOAD_TYPE)) { - downloadType = (DownloadType) intent.getSerializableExtra(DOWNLOAD_TYPE); - } - String activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME); - String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME); - conflictUploadId = intent.getLongExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, -1); - AbstractList requestedDownloads = new Vector(); - try { - DownloadFileOperation newDownload = new DownloadFileOperation(user, - file, - behaviour, - activityName, - packageName, - getBaseContext(), - downloadType); - newDownload.addDatatransferProgressListener(this); - newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder); - Pair putResult = mPendingDownloads.putIfAbsent(user.getAccountName(), - file.getRemotePath(), - newDownload); - if (putResult != null) { - String downloadKey = putResult.first; - requestedDownloads.add(downloadKey); - sendBroadcastNewDownload(newDownload, putResult.second); - } // else, file already in the queue of downloads; don't repeat the request - - } catch (IllegalArgumentException e) { - Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage()); - return START_NOT_STICKY; - } - - if (requestedDownloads.size() > 0) { - Message msg = mServiceHandler.obtainMessage(); - msg.arg1 = startId; - msg.obj = requestedDownloads; - mServiceHandler.sendMessage(msg); - } - } - - return START_NOT_STICKY; - } - - /** - * Provides a binder object that clients can use to perform operations on the queue of downloads, - * excepting the addition of new files. - * - * Implemented to perform cancellation, pause and resume of existing downloads. - */ - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - - /** - * Called when ALL the bound clients were onbound. - */ - @Override - public boolean onUnbind(Intent intent) { - ((FileDownloaderBinder) mBinder).clearListeners(); - return false; // not accepting rebinding (default behaviour) - } - - @Override - public void onAccountsUpdated(Account[] accounts) { - //review the current download and cancel it if its account doesn't exist - if (mCurrentDownload != null && !accountManager.exists(mCurrentDownload.getUser().toPlatformAccount())) { - mCurrentDownload.cancel(); - } - // The rest of downloads are cancelled when they try to start - } - - - /** - * Binder to let client components to perform operations on the queue of downloads. - *

- * It provides by itself the available operations. - */ - public class FileDownloaderBinder extends Binder implements OnDatatransferProgressListener { - - /** - * Map of listeners that will be reported about progress of downloads from a - * {@link FileDownloaderBinder} - * instance. - */ - private Map mBoundListeners = - new HashMap(); - - - /** - * Cancels a pending or current download of a remote file. - * - * @param account ownCloud account where the remote file is stored. - * @param file A file in the queue of pending downloads - */ - public void cancel(Account account, OCFile file) { - Pair removeResult = - mPendingDownloads.remove(account.name, file.getRemotePath()); - DownloadFileOperation download = removeResult.first; - if (download != null) { - download.cancel(); - } else { - if (mCurrentDownload != null && currentUser.isPresent() && - mCurrentDownload.getRemotePath().startsWith(file.getRemotePath()) && - account.name.equals(currentUser.get().getAccountName())) { - mCurrentDownload.cancel(); - } - } - } - - /** - * Cancels all the downloads for an account - */ - public void cancel(String accountName) { - if (mCurrentDownload != null && mCurrentDownload.getUser().nameEquals(accountName)) { - mCurrentDownload.cancel(); - } - // Cancel pending downloads - cancelPendingDownloads(accountName); - } - - public void clearListeners() { - mBoundListeners.clear(); - } - - - /** - * Returns True when the file described by 'file' in the ownCloud account 'account' - * is downloading or waiting to download. - * - * If 'file' is a directory, returns 'true' if any of its descendant files is downloading or - * waiting to download. - * - * @param user user where the remote file is stored. - * @param file A file that could be in the queue of downloads. - */ - public boolean isDownloading(User user, OCFile file) { - return user != null && file != null && mPendingDownloads.contains(user.getAccountName(), file.getRemotePath()); - } - - - /** - * Adds a listener interested in the progress of the download for a concrete file. - * - * @param listener Object to notify about progress of transfer. - * @param file {@link OCFile} of interest for listener. - */ - public void addDatatransferProgressListener(OnDatatransferProgressListener listener, OCFile file) { - if (file == null || listener == null) { - return; - } - mBoundListeners.put(file.getFileId(), listener); - } - - - /** - * Removes a listener interested in the progress of the download for a concrete file. - * - * @param listener Object to notify about progress of transfer. - * @param file {@link OCFile} of interest for listener. - */ - public void removeDatatransferProgressListener(OnDatatransferProgressListener listener, OCFile file) { - if (file == null || listener == null) { - return; - } - Long fileId = file.getFileId(); - if (mBoundListeners.get(fileId) == listener) { - mBoundListeners.remove(fileId); - } - } - - @Override - public void onTransferProgress(long progressRate, long totalTransferredSoFar, - long totalToTransfer, String fileName) { - OnDatatransferProgressListener boundListener = - mBoundListeners.get(mCurrentDownload.getFile().getFileId()); - if (boundListener != null) { - boundListener.onTransferProgress(progressRate, totalTransferredSoFar, - totalToTransfer, fileName); - } - } - - } - - /** - * Download worker. Performs the pending downloads in the order they were requested. - - * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}. - */ - private static class ServiceHandler extends Handler { - // don't make it a final class, and don't remove the static ; lint will warn about a - // possible memory leak - FileDownloader mService; - - public ServiceHandler(Looper looper, FileDownloader service) { - super(looper); - if (service == null) { - throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); - } - mService = service; - } - - @Override - public void handleMessage(Message msg) { - @SuppressWarnings("unchecked") - AbstractList requestedDownloads = (AbstractList) msg.obj; - if (msg.obj != null) { - Iterator it = requestedDownloads.iterator(); - while (it.hasNext()) { - String next = it.next(); - mService.downloadFile(next); - } - } - mService.mStartedDownload=false; - - (new Handler()).postDelayed(() -> { - if(!mService.mStartedDownload){ - mService.mNotificationManager.cancel(R.string.downloader_download_in_progress_ticker); - } - Log_OC.d(TAG, "Stopping after command with id " + msg.arg1); - mService.mNotificationManager.cancel(FOREGROUND_SERVICE_ID); - mService.stopForeground(true); - mService.stopSelf(msg.arg1); - }, 2000); - } - } - - - /** - * Core download method: requests a file to download and stores it. - * - * @param downloadKey Key to access the download to perform, contained in mPendingDownloads - */ - private void downloadFile(String downloadKey) { - - mStartedDownload = true; - mCurrentDownload = mPendingDownloads.get(downloadKey); - - if (mCurrentDownload != null) { - // Detect if the account exists - if (accountManager.exists(mCurrentDownload.getUser().toPlatformAccount())) { - notifyDownloadStart(mCurrentDownload); - RemoteOperationResult downloadResult = null; - try { - /// prepare client object to send the request to the ownCloud server - Account currentDownloadAccount = mCurrentDownload.getUser().toPlatformAccount(); - Optional currentDownloadUser = accountManager.getUser(currentDownloadAccount.name); - if (!currentUser.equals(currentDownloadUser)) { - currentUser = currentDownloadUser; - mStorageManager = new FileDataStorageManager(currentUser.get(), getContentResolver()); - } // else, reuse storage manager from previous operation - - // always get client from client manager, to get fresh credentials in case - // of update - OwnCloudAccount ocAccount = currentDownloadUser.get().toOwnCloudAccount(); - mDownloadClient = OwnCloudClientManagerFactory.getDefaultSingleton(). - getClientFor(ocAccount, this); - - - /// perform the download - downloadResult = mCurrentDownload.execute(mDownloadClient); - if (downloadResult.isSuccess() && mCurrentDownload.getDownloadType() == DownloadType.DOWNLOAD) { - saveDownloadedFile(); - } - - } catch (Exception e) { - Log_OC.e(TAG, "Error downloading", e); - downloadResult = new RemoteOperationResult(e); - - } finally { - Pair removeResult = mPendingDownloads.removePayload( - mCurrentDownload.getUser().getAccountName(), mCurrentDownload.getRemotePath()); - - if (downloadResult == null) { - downloadResult = new RemoteOperationResult(new RuntimeException("Error downloading…")); - } - - /// notify result - notifyDownloadResult(mCurrentDownload, downloadResult); - sendBroadcastDownloadFinished(mCurrentDownload, downloadResult, removeResult.second); - } - } else { - cancelPendingDownloads(mCurrentDownload.getUser().getAccountName()); - } - } - } - - - /** - * Updates the OC File after a successful download. - * - * TODO move to DownloadFileOperation - * unify with code from {@link DocumentsStorageProvider} and {@link DownloadTask}. - */ - private void saveDownloadedFile() { - OCFile file = mStorageManager.getFileById(mCurrentDownload.getFile().getFileId()); - - if (file == null) { - // try to get file via path, needed for overwriting existing files on conflict dialog - file = mStorageManager.getFileByDecryptedRemotePath(mCurrentDownload.getFile().getRemotePath()); - } - - if (file == null) { - Log_OC.e(this, "Could not save " + mCurrentDownload.getFile().getRemotePath()); - return; - } - - long syncDate = System.currentTimeMillis(); - file.setLastSyncDateForProperties(syncDate); - file.setLastSyncDateForData(syncDate); - file.setUpdateThumbnailNeeded(true); - file.setModificationTimestamp(mCurrentDownload.getModificationTimestamp()); - file.setModificationTimestampAtLastSyncForData(mCurrentDownload.getModificationTimestamp()); - file.setEtag(mCurrentDownload.getEtag()); - file.setMimeType(mCurrentDownload.getMimeType()); - file.setStoragePath(mCurrentDownload.getSavePath()); - file.setFileLength(new File(mCurrentDownload.getSavePath()).length()); - file.setRemoteId(mCurrentDownload.getFile().getRemoteId()); - mStorageManager.saveFile(file); - if (MimeTypeUtil.isMedia(mCurrentDownload.getMimeType())) { - FileDataStorageManager.triggerMediaScan(file.getStoragePath(), file); - } - mStorageManager.saveConflict(file, null); - } - - /** - * Creates a status notification to show the download progress - * - * @param download Download operation starting. - */ - private void notifyDownloadStart(DownloadFileOperation download) { - /// create status notification with a progress bar - mLastPercent = 0; - mNotificationBuilder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils); - mNotificationBuilder - .setSmallIcon(R.drawable.notification_icon) - .setTicker(getString(R.string.downloader_download_in_progress_ticker)) - .setContentTitle(getString(R.string.downloader_download_in_progress_ticker)) - .setOngoing(true) - .setProgress(100, 0, download.getSize() < 0) - .setContentText( - String.format(getString(R.string.downloader_download_in_progress_content), 0, - new File(download.getSavePath()).getName()) - ); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - mNotificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD); - } - - /// includes a pending intent in the notification showing the details view of the file - Intent showDetailsIntent = null; - if (PreviewImageFragment.canBePreviewed(download.getFile())) { - showDetailsIntent = new Intent(this, PreviewImageActivity.class); - } else { - showDetailsIntent = new Intent(this, FileDisplayActivity.class); - } - showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, download.getFile()); - showDetailsIntent.putExtra(FileActivity.EXTRA_USER, download.getUser()); - showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - - mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(), - showDetailsIntent, PendingIntent.FLAG_IMMUTABLE)); - - - if (mNotificationManager == null) { - mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - } - if (mNotificationManager != null) { - mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotificationBuilder.build()); - } - } - - - /** - * Callback method to update the progress bar in the status notification. - */ - @Override - public void onTransferProgress(long progressRate, long totalTransferredSoFar, - long totalToTransfer, String filePath) { - int percent = (int) (100.0 * ((double) totalTransferredSoFar) / ((double) totalToTransfer)); - if (percent != mLastPercent) { - mNotificationBuilder.setProgress(100, percent, totalToTransfer < 0); - String fileName = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1); - String text = String.format(getString(R.string.downloader_download_in_progress_content), percent, fileName); - mNotificationBuilder.setContentText(text); - - if (mNotificationManager == null) { - mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - } - - if (mNotificationManager != null) { - mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, - mNotificationBuilder.build()); - } - } - mLastPercent = percent; - } - - - /** - * Updates the status notification with the result of a download operation. - * - * @param downloadResult Result of the download operation. - * @param download Finished download operation - */ - @SuppressFBWarnings("DMI") - private void notifyDownloadResult(DownloadFileOperation download, - RemoteOperationResult downloadResult) { - if (mNotificationManager == null) { - mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - } - - if (!downloadResult.isCancelled()) { - if (downloadResult.isSuccess()) { - if (conflictUploadId > 0) { - uploadsStorageManager.removeUpload(conflictUploadId); - } - // Dont show notification except an error has occured. - return; - } - int tickerId = downloadResult.isSuccess() ? - R.string.downloader_download_succeeded_ticker : R.string.downloader_download_failed_ticker; - - boolean needsToUpdateCredentials = ResultCode.UNAUTHORIZED == downloadResult.getCode(); - tickerId = needsToUpdateCredentials ? - R.string.downloader_download_failed_credentials_error : tickerId; - - mNotificationBuilder - .setTicker(getString(tickerId)) - .setContentTitle(getString(tickerId)) - .setAutoCancel(true) - .setOngoing(false) - .setProgress(0, 0, false); - - if (needsToUpdateCredentials) { - configureUpdateCredentialsNotification(download.getUser()); - - } else { - // TODO put something smart in showDetailsIntent - Intent showDetailsIntent = new Intent(); - mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(), - showDetailsIntent, PendingIntent.FLAG_IMMUTABLE)); - } - - mNotificationBuilder.setContentText(ErrorMessageAdapter.getErrorCauseMessage(downloadResult, - download, getResources())); - - if (mNotificationManager != null) { - mNotificationManager.notify((new SecureRandom()).nextInt(), mNotificationBuilder.build()); - - // Remove success notification - if (downloadResult.isSuccess()) { - // Sleep 2 seconds, so show the notification before remove it - NotificationUtils.cancelWithDelay(mNotificationManager, - R.string.downloader_download_succeeded_ticker, 2000); - } - } - } - } - - private void configureUpdateCredentialsNotification(User user) { - // let the user update credentials with one click - Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class); - updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount()); - updateAccountCredentials.putExtra( - AuthenticatorActivity.EXTRA_ACTION, - AuthenticatorActivity.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); - mNotificationBuilder.setContentIntent( - PendingIntent.getActivity(this, - (int) System.currentTimeMillis(), - updateAccountCredentials, - PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE) - ); - } - - - /** - * Sends a broadcast when a download finishes in order to the interested activities can - * update their view - * - * @param download Finished download operation - * @param downloadResult Result of the download operation - * @param unlinkedFromRemotePath Path in the downloads tree where the download was unlinked from - */ - private void sendBroadcastDownloadFinished( - DownloadFileOperation download, - RemoteOperationResult downloadResult, - String unlinkedFromRemotePath) { - - Intent end = new Intent(getDownloadFinishMessage()); - end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess()); - end.putExtra(ACCOUNT_NAME, download.getUser().getAccountName()); - end.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath()); - end.putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.getBehaviour()); - end.putExtra(SendShareDialog.ACTIVITY_NAME, download.getActivityName()); - end.putExtra(SendShareDialog.PACKAGE_NAME, download.getPackageName()); - if (unlinkedFromRemotePath != null) { - end.putExtra(EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath); - } - end.setPackage(getPackageName()); - localBroadcastManager.sendBroadcast(end); - } - - - /** - * Sends a broadcast when a new download is added to the queue. - * - * @param download Added download operation - * @param linkedToRemotePath Path in the downloads tree where the download was linked to - */ - private void sendBroadcastNewDownload(DownloadFileOperation download, - String linkedToRemotePath) { - Intent added = new Intent(getDownloadAddedMessage()); - added.putExtra(ACCOUNT_NAME, download.getUser().getAccountName()); - added.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath()); - added.putExtra(EXTRA_LINKED_TO_PATH, linkedToRemotePath); - added.setPackage(getPackageName()); - localBroadcastManager.sendBroadcast(added); - } - - private void cancelPendingDownloads(String accountName) { - mPendingDownloads.remove(accountName); - } -} diff --git a/app/src/main/java/com/owncloud/android/files/services/FileDownloader.kt b/app/src/main/java/com/owncloud/android/files/services/FileDownloader.kt new file mode 100644 index 000000000000..6463950129ad --- /dev/null +++ b/app/src/main/java/com/owncloud/android/files/services/FileDownloader.kt @@ -0,0 +1,750 @@ +/* + * ownCloud Android client application + * + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2012-2016 ownCloud Inc. + * + * 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.accounts.AccountManager +import android.accounts.OnAccountsUpdateListener +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.os.Binder +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.os.IBinder +import android.os.Looper +import android.os.Message +import android.os.Process +import androidx.core.app.NotificationCompat +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.java.util.Optional +import com.owncloud.android.R +import com.owncloud.android.authentication.AuthenticatorActivity +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.network.OnDatatransferProgressListener +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.DownloadFileOperation +import com.owncloud.android.operations.DownloadType +import com.owncloud.android.ui.activity.ConflictsResolveActivity +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.dialog.SendShareDialog +import com.owncloud.android.ui.fragment.OCFileListFragment +import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.ui.preview.PreviewImageActivity +import com.owncloud.android.ui.preview.PreviewImageFragment +import com.owncloud.android.utils.ErrorMessageAdapter +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.theme.ViewThemeUtils +import dagger.android.AndroidInjection +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import java.io.File +import java.security.SecureRandom +import java.util.AbstractList +import javax.inject.Inject + +class FileDownloader : Service(), OnDatatransferProgressListener, OnAccountsUpdateListener { + private var mServiceLooper: Looper? = null + private var mServiceHandler: ServiceHandler? = null + private var mBinder: IBinder? = null + private var mDownloadClient: OwnCloudClient? = null + private var currentUser = Optional.empty() + private var mStorageManager: FileDataStorageManager? = null + private val mPendingDownloads = IndexedForest() + private var mCurrentDownload: DownloadFileOperation? = null + private var notificationManager: NotificationManager? = null + private var notification: Notification? = null + private var notificationBuilder: NotificationCompat.Builder? = null + private var mLastPercent = 0 + private var conflictUploadId: Long = 0 + var mStartedDownload = false + + @JvmField + @Inject + var accountManager: UserAccountManager? = null + + @JvmField + @Inject + var uploadsStorageManager: UploadsStorageManager? = null + + @JvmField + @Inject + var localBroadcastManager: LocalBroadcastManager? = null + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + /** + * Service initialization + */ + override fun onCreate() { + super.onCreate() + + AndroidInjection.inject(this) + Log_OC.d(TAG, "Creating service") + initNotificationManager() + val thread = HandlerThread("FileDownloaderThread", Process.THREAD_PRIORITY_BACKGROUND) + thread.start() + mServiceLooper = thread.looper + mServiceHandler = ServiceHandler(mServiceLooper, this) + mBinder = FileDownloaderBinder() + initNotificationBuilder() + + // add AccountsUpdatedListener + val am = AccountManager.get(applicationContext) + am.addOnAccountsUpdatedListener(this, null, false) + } + + private fun initNotificationManager() { + if (notificationManager == null) { + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + } + } + + private fun initNotificationBuilder() { + val resources = applicationContext.resources + val title = resources.getString(R.string.foreground_service_download) + + notificationBuilder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils) + .setSmallIcon(R.drawable.notification_icon) + .setOngoing(true) + .setContentTitle(title) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + notificationBuilder?.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD) + } + notification = notificationBuilder?.build() + } + + private fun notifyNotificationManager() { + notificationManager?.notify(R.string.downloader_download_in_progress_ticker, notificationBuilder?.build()) + } + + /** + * Service clean up + */ + override fun onDestroy() { + Log_OC.v(TAG, "Destroying service") + mBinder = null + mServiceHandler = null + mServiceLooper!!.quit() + mServiceLooper = null + notificationManager = null + notification = null + notificationBuilder = null + + // remove AccountsUpdatedListener + val am = AccountManager.get(applicationContext) + am.removeOnAccountsUpdatedListener(this) + super.onDestroy() + } + + /** + * Entry point to add one or several files to the queue of downloads. + * + * New downloads are added calling to startService(), resulting in a call to this method. + * This ensures the service will keep on working although the caller activity goes away. + */ + @Suppress("LongParameterList") + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + Log_OC.d(TAG, "Starting command with id $startId") + + startForeground(FOREGROUND_SERVICE_ID, notification) + + if (!intent.hasExtra(EXTRA_USER) || !intent.hasExtra(EXTRA_FILE)) { + Log_OC.e(TAG, "Not enough information provided in intent") + return START_NOT_STICKY + } + + val user = intent.getParcelableExtra(EXTRA_USER) + val file = intent.getParcelableExtra(EXTRA_FILE) + val behaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR) + var downloadType: DownloadType? = DownloadType.DOWNLOAD + if (intent.hasExtra(DOWNLOAD_TYPE)) { + downloadType = intent.getSerializableExtra(DOWNLOAD_TYPE) as DownloadType? + } + val activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME) + val packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME) + conflictUploadId = intent.getLongExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, -1) + + val requestedDownloads = handleDownloadRequest(user, file, behaviour, downloadType, activityName, packageName) + + if (requestedDownloads.isNotEmpty()) { + val msg = mServiceHandler?.obtainMessage() + msg?.arg1 = startId + msg?.obj = requestedDownloads + msg?.let { + mServiceHandler?.sendMessage(it) + } + } + + return START_NOT_STICKY + } + + @Suppress("LongParameterList") + private fun handleDownloadRequest( + user: User?, + file: OCFile?, + behaviour: String?, + downloadType: DownloadType?, + activityName: String?, + packageName: String? + ): List { + val requestedDownloads: MutableList = ArrayList() + + if (user == null || file == null) { + return requestedDownloads + } + + try { + val newDownload = DownloadFileOperation( + user, + file, + behaviour, + activityName, + packageName, + baseContext, + downloadType + ) + newDownload.addDatatransferProgressListener(this) + newDownload.addDatatransferProgressListener(mBinder as FileDownloaderBinder?) + + val putResult = mPendingDownloads.putIfAbsent(user.accountName, file.remotePath, newDownload) + + if (putResult != null) { + val downloadKey = putResult.first + requestedDownloads.add(downloadKey) + sendBroadcastNewDownload(newDownload, putResult.second) + } + } catch (e: IllegalArgumentException) { + Log_OC.e(TAG, "Not enough information provided in intent: " + e.message) + } + + return requestedDownloads + } + + /** + * Provides a binder object that clients can use to perform operations on the queue of downloads, + * excepting the addition of new files. + * + * Implemented to perform cancellation, pause and resume of existing downloads. + */ + override fun onBind(intent: Intent): IBinder? { + return mBinder + } + + /** + * Called when ALL the bound clients were onbound. + */ + override fun onUnbind(intent: Intent): Boolean { + (mBinder as FileDownloaderBinder?)!!.clearListeners() + return false // not accepting rebinding (default behaviour) + } + + override fun onAccountsUpdated(accounts: Array) { + // review the current download and cancel it if its account doesn't exist + if (mCurrentDownload != null && !accountManager!!.exists(mCurrentDownload!!.user.toPlatformAccount())) { + mCurrentDownload!!.cancel() + } + // The rest of downloads are cancelled when they try to start + } + + /** + * Binder to let client components to perform operations on the queue of downloads. + * + * + * It provides by itself the available operations. + */ + inner class FileDownloaderBinder : Binder(), OnDatatransferProgressListener { + /** + * Map of listeners that will be reported about progress of downloads from a + * [FileDownloaderBinder] + * instance. + */ + private val mBoundListeners: MutableMap = HashMap() + + /** + * Cancels a pending or current download of a remote file. + * + * @param account ownCloud account where the remote file is stored. + * @param file A file in the queue of pending downloads + */ + @Suppress("ComplexMethod") + fun cancel(account: Account, file: OCFile) { + val removeResult = mPendingDownloads.remove(account.name, file.remotePath) + val download = removeResult.first + + if (download != null) { + download.cancel() + } else { + mCurrentDownload?.takeIf { + it.remotePath.startsWith(file.remotePath) && account.name == currentUser?.get()?.accountName + }?.cancel() + } + } + + /** + * Cancels all the downloads for an account + */ + fun cancel(accountName: String?) { + if (mCurrentDownload != null && mCurrentDownload!!.user.nameEquals(accountName)) { + mCurrentDownload!!.cancel() + } + // Cancel pending downloads + cancelPendingDownloads(accountName) + } + + fun clearListeners() { + mBoundListeners.clear() + } + + /** + * Returns True when the file described by 'file' in the ownCloud account 'account' + * is downloading or waiting to download. + * + * If 'file' is a directory, returns 'true' if any of its descendant files is downloading or + * waiting to download. + * + * @param user user where the remote file is stored. + * @param file A file that could be in the queue of downloads. + */ + fun isDownloading(user: User?, file: OCFile?): Boolean { + return user != null && file != null && mPendingDownloads.contains(user.accountName, file.remotePath) + } + + /** + * Adds a listener interested in the progress of the download for a concrete file. + * + * @param listener Object to notify about progress of transfer. + * @param file [OCFile] of interest for listener. + */ + fun addDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) { + if (file == null || listener == null) { + return + } + mBoundListeners[file.fileId] = listener + } + + /** + * Removes a listener interested in the progress of the download for a concrete file. + * + * @param listener Object to notify about progress of transfer. + * @param file [OCFile] of interest for listener. + */ + fun removeDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) { + if (file == null || listener == null) { + return + } + val fileId = file.fileId + if (mBoundListeners[fileId] === listener) { + mBoundListeners.remove(fileId) + } + } + + override fun onTransferProgress( + progressRate: Long, + totalTransferredSoFar: Long, + totalToTransfer: Long, + fileName: String + ) { + val boundListener = mBoundListeners[mCurrentDownload!!.file.fileId] + boundListener?.onTransferProgress( + progressRate, + totalTransferredSoFar, + totalToTransfer, + fileName + ) + } + } + + /** + * Download worker. Performs the pending downloads in the order they were requested. + * + * Created with the Looper of a new thread, started in [FileUploader.onCreate]. + */ + private class ServiceHandler(looper: Looper?, service: FileDownloader?) : Handler(looper!!) { + // don't make it a final class, and don't remove the static ; lint will warn about a + // possible memory leak + var mService: FileDownloader + + init { + requireNotNull(service) { "Received invalid NULL in parameter 'service'" } + mService = service + } + + @Suppress("MagicNumber") + override fun handleMessage(msg: Message) { + val requestedDownloads = msg.obj as AbstractList + if (msg.obj != null) { + val it: Iterator = requestedDownloads.iterator() + while (it.hasNext()) { + val next = it.next() + mService.downloadFile(next) + } + } + mService.mStartedDownload = false + + Handler(Looper.getMainLooper()).postDelayed({ + if (!mService.mStartedDownload) { + mService.notificationManager!!.cancel(R.string.downloader_download_in_progress_ticker) + } + Log_OC.d(TAG, "Stopping after command with id " + msg.arg1) + mService.notificationManager!!.cancel(FOREGROUND_SERVICE_ID) + mService.stopForeground(true) + mService.stopSelf(msg.arg1) + }, 2000) + } + } + + /** + * Core download method: requests a file to download and stores it. + * + * @param downloadKey Key to access the download to perform, contained in mPendingDownloads + */ + @Suppress("NestedBlockDepth", "TooGenericExceptionCaught") + private fun downloadFile(downloadKey: String) { + mStartedDownload = true + mCurrentDownload = mPendingDownloads[downloadKey] + + if (mCurrentDownload != null) { + val isAccountExist = accountManager?.exists(mCurrentDownload!!.user.toPlatformAccount()) + + if (isAccountExist == true) { + notifyDownloadStart(mCurrentDownload!!) + var downloadResult: RemoteOperationResult<*>? = null + try { + // / prepare client object to send the request to the ownCloud server + val currentDownloadAccount = mCurrentDownload!!.user.toPlatformAccount() + val currentDownloadUser = accountManager!!.getUser(currentDownloadAccount.name) + if (currentUser != currentDownloadUser) { + currentUser = currentDownloadUser + mStorageManager = FileDataStorageManager(currentUser.get(), contentResolver) + } // else, reuse storage manager from previous operation + + // always get client from client manager, to get fresh credentials in case + // of update + val ocAccount = currentDownloadUser.get().toOwnCloudAccount() + mDownloadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, this) + + // / perform the download + downloadResult = mCurrentDownload!!.execute(mDownloadClient) + if (downloadResult.isSuccess && mCurrentDownload!!.downloadType === DownloadType.DOWNLOAD) { + saveDownloadedFile() + } + } catch (e: Exception) { + Log_OC.e(TAG, "Error downloading", e) + downloadResult = RemoteOperationResult(e) + } finally { + val removeResult = mPendingDownloads.removePayload( + mCurrentDownload!!.user.accountName, + mCurrentDownload!!.remotePath + ) + if (downloadResult == null) { + downloadResult = RemoteOperationResult(RuntimeException("Error downloading…")) + } + + // / notify result + notifyDownloadResult(mCurrentDownload!!, downloadResult) + sendBroadcastDownloadFinished(mCurrentDownload!!, downloadResult, removeResult.second) + } + } else { + cancelPendingDownloads(mCurrentDownload!!.user.accountName) + } + } + } + + /** + * Updates the OC File after a successful download. + * + * TODO move to DownloadFileOperation + * unify with code from [DocumentsStorageProvider] and [DownloadTask]. + */ + private fun saveDownloadedFile() { + var file = mStorageManager?.getFileById(mCurrentDownload!!.file.fileId) + if (file == null) { + // try to get file via path, needed for overwriting existing files on conflict dialog + file = mStorageManager?.getFileByDecryptedRemotePath(mCurrentDownload!!.file.remotePath) + } + if (file == null) { + Log_OC.e(this, "Could not save " + mCurrentDownload!!.file.remotePath) + return + } + + val syncDate = System.currentTimeMillis() + file.lastSyncDateForProperties = syncDate + file.lastSyncDateForData = syncDate + file.isUpdateThumbnailNeeded = true + file.modificationTimestamp = mCurrentDownload!!.modificationTimestamp + file.modificationTimestampAtLastSyncForData = mCurrentDownload!!.modificationTimestamp + file.etag = mCurrentDownload!!.etag + file.mimeType = mCurrentDownload!!.mimeType + file.storagePath = mCurrentDownload!!.savePath + file.fileLength = File(mCurrentDownload!!.savePath).length() + file.remoteId = mCurrentDownload!!.file.remoteId + mStorageManager!!.saveFile(file) + if (MimeTypeUtil.isMedia(mCurrentDownload!!.mimeType)) { + FileDataStorageManager.triggerMediaScan(file.storagePath, file) + } + mStorageManager!!.saveConflict(file, null) + } + + /** + * Creates a status notification to show the download progress + * + * @param download Download operation starting. + */ + @Suppress("MagicNumber") + private fun notifyDownloadStart(download: DownloadFileOperation) { + val fileName = download.file.getFileNameWithExtension(10) + val titlePrefix = getString(R.string.file_downloader_notification_title_prefix) + val title = titlePrefix + fileName + + // / update status notification with a progress bar + mLastPercent = 0 + notificationBuilder + ?.setContentTitle(title) + ?.setTicker(title) + ?.setProgress(100, 0, download.size < 0) + + // / includes a pending intent in the notification showing the details view of the file + val showDetailsIntent: Intent = if (PreviewImageFragment.canBePreviewed(download.file)) { + Intent(this, PreviewImageActivity::class.java) + } else { + Intent(this, FileDisplayActivity::class.java) + } + + showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, download.file) + showDetailsIntent.putExtra(FileActivity.EXTRA_USER, download.user) + showDetailsIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + notificationBuilder?.setContentIntent( + PendingIntent.getActivity( + this, + System.currentTimeMillis().toInt(), + showDetailsIntent, + PendingIntent.FLAG_IMMUTABLE + ) + ) + initNotificationManager() + notifyNotificationManager() + } + + /** + * Callback method to update the progress bar in the status notification. + */ + @Suppress("MagicNumber") + override fun onTransferProgress( + progressRate: Long, + totalTransferredSoFar: Long, + totalToTransfer: Long, + filePath: String + ) { + val percent = (100.0 * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt() + if (percent != mLastPercent) { + notificationBuilder?.setProgress(100, percent, totalToTransfer < 0) + initNotificationManager() + notifyNotificationManager() + } + mLastPercent = percent + } + + /** + * Updates the status notification with the result of a download operation. + * + * @param downloadResult Result of the download operation. + * @param download Finished download operation + */ + @SuppressFBWarnings("DMI") + @Suppress("MagicNumber") + private fun notifyDownloadResult( + download: DownloadFileOperation, + downloadResult: RemoteOperationResult<*> + ) { + initNotificationManager() + if (!downloadResult.isCancelled) { + if (downloadResult.isSuccess) { + if (conflictUploadId > 0) { + uploadsStorageManager!!.removeUpload(conflictUploadId) + } + // Don't show notification except an error has occurred. + return + } + + var tickerId = if (downloadResult.isSuccess) { + R.string.downloader_download_succeeded_ticker + } else { + R.string.downloader_download_failed_ticker + } + + val needsToUpdateCredentials = ResultCode.UNAUTHORIZED == downloadResult.code + + tickerId = if (needsToUpdateCredentials) { + R.string.downloader_download_failed_credentials_error + } else { + tickerId + } + + notificationBuilder + ?.setSmallIcon(R.drawable.notification_icon) + ?.setTicker(getString(tickerId)) + ?.setAutoCancel(true) + ?.setOngoing(false) + ?.setProgress(0, 0, false) + + if (needsToUpdateCredentials) { + configureUpdateCredentialsNotification(download.user) + } else { + // TODO put something smart in showDetailsIntent + val showDetailsIntent = Intent() + notificationBuilder?.setContentIntent( + PendingIntent.getActivity( + this, + System.currentTimeMillis().toInt(), + showDetailsIntent, + PendingIntent.FLAG_IMMUTABLE + ) + ) + } + notificationBuilder?.setContentText( + ErrorMessageAdapter.getErrorCauseMessage( + downloadResult, + download, + resources + ) + ) + if (notificationManager != null) { + notificationManager?.notify(SecureRandom().nextInt(), notificationBuilder?.build()) + + // Remove success notification + if (downloadResult.isSuccess) { + // Sleep 2 seconds, so show the notification before remove it + NotificationUtils.cancelWithDelay( + notificationManager, + R.string.downloader_download_succeeded_ticker, + 2000 + ) + } + } + } + } + + private fun configureUpdateCredentialsNotification(user: User) { + // let the user update credentials with one click + val updateAccountCredentials = Intent(this, AuthenticatorActivity::class.java) + updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount()) + updateAccountCredentials.putExtra( + AuthenticatorActivity.EXTRA_ACTION, + AuthenticatorActivity.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) + notificationBuilder!!.setContentIntent( + PendingIntent.getActivity( + this, + System.currentTimeMillis().toInt(), + updateAccountCredentials, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) + ) + } + + /** + * Sends a broadcast when a download finishes in order to the interested activities can + * update their view + * + * @param download Finished download operation + * @param downloadResult Result of the download operation + * @param unlinkedFromRemotePath Path in the downloads tree where the download was unlinked from + */ + private fun sendBroadcastDownloadFinished( + download: DownloadFileOperation, + downloadResult: RemoteOperationResult<*>, + unlinkedFromRemotePath: String? + ) { + val end = Intent(downloadFinishMessage) + end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess) + end.putExtra(ACCOUNT_NAME, download.user.accountName) + end.putExtra(EXTRA_REMOTE_PATH, download.remotePath) + end.putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.behaviour) + end.putExtra(SendShareDialog.ACTIVITY_NAME, download.activityName) + end.putExtra(SendShareDialog.PACKAGE_NAME, download.packageName) + if (unlinkedFromRemotePath != null) { + end.putExtra(EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath) + } + end.setPackage(packageName) + localBroadcastManager!!.sendBroadcast(end) + } + + /** + * Sends a broadcast when a new download is added to the queue. + * + * @param download Added download operation + * @param linkedToRemotePath Path in the downloads tree where the download was linked to + */ + private fun sendBroadcastNewDownload( + download: DownloadFileOperation, + linkedToRemotePath: String + ) { + val added = Intent(downloadAddedMessage) + added.putExtra(ACCOUNT_NAME, download.user.accountName) + added.putExtra(EXTRA_REMOTE_PATH, download.remotePath) + added.putExtra(EXTRA_LINKED_TO_PATH, linkedToRemotePath) + added.setPackage(packageName) + localBroadcastManager!!.sendBroadcast(added) + } + + private fun cancelPendingDownloads(accountName: String?) { + mPendingDownloads.remove(accountName) + } + + companion object { + const val EXTRA_USER = "USER" + const val EXTRA_FILE = "FILE" + private const val DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED" + private const val DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH" + const val EXTRA_DOWNLOAD_RESULT = "RESULT" + const val EXTRA_REMOTE_PATH = "REMOTE_PATH" + const val EXTRA_LINKED_TO_PATH = "LINKED_TO" + const val ACCOUNT_NAME = "ACCOUNT_NAME" + const val DOWNLOAD_TYPE = "DOWNLOAD_TYPE" + private const val FOREGROUND_SERVICE_ID = 412 + private val TAG = FileDownloader::class.java.simpleName + + @JvmStatic + val downloadAddedMessage: String + get() = FileDownloader::class.java.name + DOWNLOAD_ADDED_MESSAGE + + @JvmStatic + val downloadFinishMessage: String + get() = FileDownloader::class.java.name + DOWNLOAD_FINISH_MESSAGE + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index 5c55a80904f3..b7066e8a458d 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -204,8 +204,7 @@ public class FileDisplayActivity extends FileActivity public static final int REQUEST_CODE__SELECT_CONTENT_FROM_APPS = REQUEST_CODE__LAST_SHARED + 1; public static final int REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM = REQUEST_CODE__LAST_SHARED + 2; - public static final int REQUEST_CODE__MOVE_FILES = REQUEST_CODE__LAST_SHARED + 3; - public static final int REQUEST_CODE__COPY_FILES = REQUEST_CODE__LAST_SHARED + 4; + public static final int REQUEST_CODE__MOVE_OR_COPY_FILES = REQUEST_CODE__LAST_SHARED + 3; public static final int REQUEST_CODE__UPLOAD_FROM_CAMERA = REQUEST_CODE__LAST_SHARED + 5; public static final int REQUEST_CODE__UPLOAD_SCAN_DOC_FROM_CAMERA = REQUEST_CODE__LAST_SHARED + 6; @@ -437,12 +436,10 @@ public void onClick(DialogInterface dialog, int which) { private void checkOutdatedServer() { Optional user = getUser(); - OwnCloudVersion serverVersion = user.get().getServer().getVersion(); - // show outdated warning if (user.isPresent() && CapabilityUtils.checkOutdatedWarning(getResources(), - serverVersion, + user.get().getServer().getVersion(), getCapabilities().getExtendedSupport().isTrue())) { DisplayUtils.showServerOutdatedSnackbar(this, Snackbar.LENGTH_LONG); } @@ -886,32 +883,9 @@ public void onCheckAvailableSpaceFinish(boolean hasEnoughSpaceAvailable, String. FileUploader.LOCAL_BEHAVIOUR_DELETE); } } - }, new String[]{FileOperationsHelper.createImageFile(getActivity()).getAbsolutePath()}).execute(); - } else if (requestCode == REQUEST_CODE__MOVE_FILES && resultCode == RESULT_OK) { - exitSelectionMode(); - final Intent fData = data; - getHandler().postDelayed( - new Runnable() { - @Override - public void run() { - requestMoveOperation(fData); - } - }, - DELAY_TO_REQUEST_OPERATIONS_LATER - ); - - } else if (requestCode == REQUEST_CODE__COPY_FILES && resultCode == RESULT_OK) { + }, new String[] { FileOperationsHelper.createImageFile(getActivity()).getAbsolutePath() }).execute(); + } else if (requestCode == REQUEST_CODE__MOVE_OR_COPY_FILES && resultCode == RESULT_OK) { exitSelectionMode(); - final Intent fData = data; - getHandler().postDelayed( - new Runnable() { - @Override - public void run() { - requestCopyOperation(fData); - } - }, - DELAY_TO_REQUEST_OPERATIONS_LATER - ); } else if (requestCode == PermissionUtil.REQUEST_CODE_MANAGE_ALL_FILES) { syncAndUpdateFolder(true); } else { @@ -1018,28 +992,6 @@ private void requestUploadOfContentFromApps(Intent contentIntent, int resultCode } - /** - * Request the operation for moving the file/folder from one path to another - * - * @param data Intent received - */ - private void requestMoveOperation(Intent data) { - final OCFile folderToMoveAt = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER); - final List filePaths = data.getStringArrayListExtra(FolderPickerActivity.EXTRA_FILE_PATHS); - getFileOperationsHelper().moveFiles(filePaths, folderToMoveAt); - } - - /** - * Request the operation for copying the file/folder from one path to another - * - * @param data Intent received - */ - private void requestCopyOperation(Intent data) { - final OCFile targetFolder = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER); - final List filePaths = data.getStringArrayListExtra(FolderPickerActivity.EXTRA_FILE_PATHS); - getFileOperationsHelper().copyFiles(filePaths, targetFolder); - } - private boolean isSearchOpen() { if (searchView == null) { return false; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index a8f5988c5e2b..6e829d547b1e 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -44,6 +44,7 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.SearchRemoteOperation import com.owncloud.android.operations.CreateFolderOperation import com.owncloud.android.operations.RefreshFolderOperation +import com.owncloud.android.services.OperationsService import com.owncloud.android.syncadapter.FileSyncAdapter import com.owncloud.android.ui.dialog.CreateFolderDialogFragment import com.owncloud.android.ui.dialog.SortingOrderDialogFragment.OnSortingOrderListener @@ -73,7 +74,9 @@ open class FolderPickerActivity : var isDoNotEnterEncryptedFolder = false private set private var mCancelBtn: MaterialButton? = null - private var mChooseBtn: MaterialButton? = null + private var mCopyBtn: MaterialButton? = null + private var mMoveBtn: MaterialButton? = null + private var caption: String? = null private var mAction: String? = null @@ -85,6 +88,7 @@ open class FolderPickerActivity : override fun onCreate(savedInstanceState: Bundle?) { Log_OC.d(TAG, "onCreate() start") super.onCreate(savedInstanceState) + if (this is FilePickerActivity) { setContentView(R.layout.files_picker) } else { @@ -101,29 +105,15 @@ open class FolderPickerActivity : findViewById(R.id.switch_grid_view_button).visibility = View.GONE mAction = intent.getStringExtra(EXTRA_ACTION) + if (mAction != null) { - when (mAction) { - MOVE -> { - caption = resources.getText(R.string.move_to).toString() - mSearchOnlyFolders = true - isDoNotEnterEncryptedFolder = true - } - COPY -> { - caption = resources.getText(R.string.copy_to).toString() - mSearchOnlyFolders = true - isDoNotEnterEncryptedFolder = true - } - CHOOSE_LOCATION -> { - caption = resources.getText(R.string.choose_location).toString() - mSearchOnlyFolders = true - isDoNotEnterEncryptedFolder = true - mChooseBtn!!.text = resources.getString(R.string.common_select) - } - else -> caption = themeUtils.getDefaultDisplayNameForRootFolder(this) - } + caption = resources.getText(R.string.folder_picker_choose_caption_text).toString() + mSearchOnlyFolders = true + isDoNotEnterEncryptedFolder = true } else { caption = themeUtils.getDefaultDisplayNameForRootFolder(this) } + mTargetFilePaths = intent.getStringArrayListExtra(EXTRA_FILE_PATHS) if (savedInstanceState == null) { @@ -351,13 +341,14 @@ open class FolderPickerActivity : } private fun toggleChooseEnabled() { - mChooseBtn?.isEnabled = checkFolderSelectable() + mCopyBtn?.isEnabled = checkFolderSelectable() + mMoveBtn?.isEnabled = checkFolderSelectable() } // for copy and move, disable selecting parent folder of target files private fun checkFolderSelectable(): Boolean { return when { - mAction != COPY && mAction != MOVE -> true + mAction != MOVE_OR_COPY -> true mTargetFilePaths.isNullOrEmpty() -> true file?.isFolder != true -> true // all of the target files are already in the selected directory @@ -385,38 +376,57 @@ open class FolderPickerActivity : */ private fun initControls() { mCancelBtn = findViewById(R.id.folder_picker_btn_cancel) - mChooseBtn = findViewById(R.id.folder_picker_btn_choose) - if (mChooseBtn != null) { - viewThemeUtils.material.colorMaterialButtonPrimaryFilled(mChooseBtn!!) - mChooseBtn!!.setOnClickListener(this) + mCopyBtn = findViewById(R.id.folder_picker_btn_copy) + mMoveBtn = findViewById(R.id.folder_picker_btn_move) + + if (mCopyBtn != null) { + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(mCopyBtn!!) + mCopyBtn!!.setOnClickListener(this) + } + if (mMoveBtn != null) { + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(mMoveBtn!!) + mMoveBtn!!.setOnClickListener(this) } + if (mCancelBtn != null) { if (this is FilePickerActivity) { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(mCancelBtn!!) } else { - viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(mCancelBtn!!) + viewThemeUtils.material.colorMaterialButtonText(mCancelBtn!!) } mCancelBtn!!.setOnClickListener(this) } } override fun onClick(v: View) { - if (v == mCancelBtn) { - finish() - } else if (v == mChooseBtn) { - val i = intent - val resultData = Intent() - resultData.putExtra(EXTRA_FOLDER, listOfFilesFragment!!.currentFile) - val targetFiles = i.getParcelableArrayListExtra(EXTRA_FILES) - if (targetFiles != null) { - resultData.putParcelableArrayListExtra(EXTRA_FILES, targetFiles) - } - mTargetFilePaths.let { - resultData.putStringArrayListExtra(EXTRA_FILE_PATHS, it) + when (v) { + mCancelBtn -> finish() + mCopyBtn, mMoveBtn -> copyOrMove(v) + } + } + + private fun copyOrMove(v: View) { + val i = intent + val resultData = Intent() + resultData.putExtra(EXTRA_FOLDER, listOfFilesFragment?.currentFile) + + i.getParcelableArrayListExtra(EXTRA_FILES)?.let { targetFiles -> + resultData.putParcelableArrayListExtra(EXTRA_FILES, targetFiles) + } + + mTargetFilePaths?.let { + val action = when (v) { + mCopyBtn -> OperationsService.ACTION_COPY_FILE + mMoveBtn -> OperationsService.ACTION_MOVE_FILE + else -> throw IllegalArgumentException("Unknown operation") } - setResult(RESULT_OK, resultData) - finish() + + fileOperationsHelper.moveOrCopyFiles(action, it, file) + resultData.putStringArrayListExtra(EXTRA_FILE_PATHS, it) } + + setResult(RESULT_OK, resultData) + finish() } override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>) { @@ -571,8 +581,8 @@ open class FolderPickerActivity : } } - override fun onSortingOrderChosen(selection: FileSortOrder) { - listOfFilesFragment!!.sortFiles(selection) + override fun onSortingOrderChosen(selection: FileSortOrder?) { + listOfFilesFragment?.sortFiles(selection) } companion object { @@ -592,8 +602,7 @@ open class FolderPickerActivity : @JvmField val EXTRA_ACTION = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_ACTION") - const val MOVE = "MOVE" - const val COPY = "COPY" + const val MOVE_OR_COPY = "MOVE_OR_COPY" const val CHOOSE_LOCATION = "CHOOSE_LOCATION" private val TAG = FolderPickerActivity::class.java.simpleName protected const val TAG_LIST_OF_FOLDERS = "LIST_OF_FOLDERS" diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index aeca4a18d513..3b5b5e313dda 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -643,7 +643,11 @@ class SyncedFoldersActivity : } } - override fun onSaveSyncedFolderPreference(syncedFolder: SyncedFolderParcelable) { + override fun onSaveSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) { + if (syncedFolder == null) { + return + } + // custom folders newly created aren't in the list already, // so triggering a refresh if (MediaFolderType.CUSTOM == syncedFolder.type && syncedFolder.id == SyncedFolder.UNPERSISTED_ID) { @@ -730,7 +734,11 @@ class SyncedFoldersActivity : syncedFolderPreferencesDialogFragment = null } - override fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable) { + override fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) { + if (syncedFolder == null) { + return + } + syncedFolderProvider.deleteSyncedFolder(syncedFolder.id) adapter.removeItem(syncedFolder.section) } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java index 8a8572e3002f..bff39ae57e0e 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java @@ -528,9 +528,7 @@ public void onCheckAvailableSpaceFinish(boolean hasEnoughSpaceAvailable, String. // to the ownCloud folder instead of copying String[] args = {getString(R.string.app_name)}; ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance( - R.string.upload_query_move_foreign_files, args, 0, R.string.common_yes, -1, - R.string.common_no - ); + R.string.upload_query_move_foreign_files, args, 0, R.string.common_yes, R.string.common_no, -1); dialog.setOnConfirmationListener(this); dialog.show(getSupportFragmentManager(), QUERY_TO_MOVE_DIALOG_TAG); } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/StoragePathAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/StoragePathAdapter.java deleted file mode 100644 index 15ff5de565c5..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/adapter/StoragePathAdapter.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Andy Scherzinger - * Copyright (C) 2019 Andy Scherzinger - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * 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 AFFERO GENERAL PUBLIC LICENSE for more details. - * - * You should have received a copy of the GNU Affero General Public - * License along with this program. If not, see . - */ - -package com.owncloud.android.ui.adapter; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.owncloud.android.databinding.StoragePathItemBinding; - -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -public class StoragePathAdapter extends RecyclerView.Adapter { - private List pathList; - private StoragePathAdapterListener storagePathAdapterListener; - - public StoragePathAdapter(List pathList, StoragePathAdapterListener storagePathAdapterListener) { - this.pathList = pathList; - this.storagePathAdapterListener = storagePathAdapterListener; - } - - @NonNull - @Override - public StoragePathViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new StoragePathAdapter.StoragePathViewHolder( - StoragePathItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) - ); - } - - @Override - public void onBindViewHolder(@NonNull StoragePathViewHolder holder, int position) { - if (pathList != null && pathList.size() > position) { - StoragePathItem storagePathItem = pathList.get(position); - - holder.binding.icon.setImageResource(storagePathItem.getIcon()); - holder.binding.name.setText(storagePathItem.getName()); - } - } - - @Override - public int getItemCount() { - return pathList.size(); - } - - public interface StoragePathAdapterListener { - /** - * sets the chosen path. - * - * @param path chosen path - */ - void chosenPath(String path); - } - - class StoragePathViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { - StoragePathItemBinding binding; - - public StoragePathViewHolder(StoragePathItemBinding binding) { - super(binding.getRoot()); - this.binding = binding; - this.binding.getRoot().setOnClickListener(this); - } - - @Override - public void onClick(View view) { - storagePathAdapterListener.chosenPath(pathList.get(getAdapterPosition()).getPath()); - } - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/StoragePathAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/StoragePathAdapter.kt new file mode 100644 index 000000000000..968a6a2cf2a7 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/StoragePathAdapter.kt @@ -0,0 +1,80 @@ +/* + * Nextcloud Android client application + * + * @author Andy Scherzinger + * Copyright (C) 2019 Andy Scherzinger + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * 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 AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this program. If not, see . + */ +package com.owncloud.android.ui.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.databinding.StoragePathItemBinding +import com.owncloud.android.ui.adapter.StoragePathAdapter.StoragePathViewHolder +import com.owncloud.android.utils.theme.ViewThemeUtils + +class StoragePathAdapter( + private val pathList: List?, + private val storagePathAdapterListener: StoragePathAdapterListener, + private val viewThemeUtils: ViewThemeUtils +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StoragePathViewHolder { + return StoragePathViewHolder( + StoragePathItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + } + + override fun onBindViewHolder(holder: StoragePathViewHolder, position: Int) { + if (pathList != null && pathList.size > position) { + val storagePathItem = pathList[position] + holder.binding.btnStoragePath.setIconResource(storagePathItem.icon) + holder.binding.btnStoragePath.text = storagePathItem.name + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(holder.binding.btnStoragePath) + } + } + + override fun getItemCount(): Int { + return pathList?.size ?: 0 + } + + interface StoragePathAdapterListener { + /** + * sets the chosen path. + * + * @param path chosen path + */ + fun chosenPath(path: String) + } + + inner class StoragePathViewHolder(var binding: StoragePathItemBinding) : + RecyclerView.ViewHolder( + binding.root + ), + View.OnClickListener { + init { + binding.root.setOnClickListener(this) + } + + override fun onClick(view: View) { + val path = pathList?.get(absoluteAdapterPosition)?.path + path?.let { + storagePathAdapterListener.chosenPath(it) + } + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.java deleted file mode 100644 index 7c0c19001daf..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * ownCloud Android client application - * - * Copyright (C) 2012 Bartek Przybylski Copyright (C) 2015 ownCloud Inc. - * - * 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 android.app.Activity; -import android.app.Dialog; -import android.os.Bundle; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.nextcloud.client.di.Injectable; -import com.owncloud.android.R; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; - - -public class ConfirmationDialogFragment extends DialogFragment implements Injectable { - - final static String ARG_MESSAGE_RESOURCE_ID = "resource_id"; - final static String ARG_MESSAGE_ARGUMENTS = "string_array"; - final static String ARG_TITLE_ID = "title_id"; - - final static String ARG_POSITIVE_BTN_RES = "positive_btn_res"; - final static String ARG_NEUTRAL_BTN_RES = "neutral_btn_res"; - final static String ARG_NEGATIVE_BTN_RES = "negative_btn_res"; - - public static final String FTAG_CONFIRMATION = "CONFIRMATION_FRAGMENT"; - - @Inject ViewThemeUtils viewThemeUtils; - - - private ConfirmationDialogFragmentListener mListener; - - /** - * Public factory method to create new ConfirmationDialogFragment instances. - * - * @param messageResId Resource id for a message to show in the dialog. - * @param messageArguments Arguments to complete the message, if it's a format string. May be null. - * @param titleResId Resource id for a text to show in the title. 0 for default alert title, -1 for no title. - * @param posBtn Resource id for the text of the positive button. -1 for no positive button. - * @param neuBtn Resource id for the text of the neutral button. -1 for no neutral button. - * @param negBtn Resource id for the text of the negative button. -1 for no negative button. - * @return Dialog ready to show. - */ - public static ConfirmationDialogFragment newInstance(int messageResId, String[] messageArguments, int titleResId, - int posBtn, int neuBtn, int negBtn) { - if (messageResId == -1) { - throw new IllegalStateException("Calling confirmation dialog without message resource"); - } - - ConfirmationDialogFragment frag = new ConfirmationDialogFragment(); - Bundle args = new Bundle(); - args.putInt(ARG_MESSAGE_RESOURCE_ID, messageResId); - args.putStringArray(ARG_MESSAGE_ARGUMENTS, messageArguments); - args.putInt(ARG_TITLE_ID, titleResId); - args.putInt(ARG_POSITIVE_BTN_RES, posBtn); - args.putInt(ARG_NEUTRAL_BTN_RES, neuBtn); - args.putInt(ARG_NEGATIVE_BTN_RES, negBtn); - frag.setArguments(args); - return frag; - } - - @Override - public void onStart() { - super.onStart(); - - AlertDialog alertDialog = (AlertDialog) getDialog(); - - if(alertDialog != null) { - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE), - alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE), - alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL)); - } - } - - public void setOnConfirmationListener(ConfirmationDialogFragmentListener listener) { - mListener = listener; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Bundle arguments = getArguments(); - - if (arguments == null) { - throw new IllegalArgumentException("Arguments may not be null"); - } - - Activity activity = getActivity(); - - if (activity == null) { - throw new IllegalArgumentException("Activity may not be null"); - } - - Object[] messageArguments = arguments.getStringArray(ARG_MESSAGE_ARGUMENTS); - int messageId = arguments.getInt(ARG_MESSAGE_RESOURCE_ID, -1); - int titleId = arguments.getInt(ARG_TITLE_ID, -1); - int posBtn = arguments.getInt(ARG_POSITIVE_BTN_RES, -1); - int neuBtn = arguments.getInt(ARG_NEUTRAL_BTN_RES, -1); - int negBtn = arguments.getInt(ARG_NEGATIVE_BTN_RES, -1); - - if (messageArguments == null) { - messageArguments = new String[]{}; - } - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity) - .setIcon(R.drawable.ic_warning) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setMessage(String.format(getString(messageId), messageArguments)); - - if (titleId == 0) { - builder.setTitle(android.R.string.dialog_alert_title); - } else if (titleId != -1) { - builder.setTitle(titleId); - } - - if (posBtn != -1) { - builder.setPositiveButton(posBtn, (dialog, whichButton) -> { - if (mListener != null) { - mListener.onConfirmation(getTag()); - } - dialog.dismiss(); - }); - } - if (neuBtn != -1) { - builder.setNeutralButton(neuBtn, (dialog, whichButton) -> { - if (mListener != null) { - mListener.onNeutral(getTag()); - } - dialog.dismiss(); - }); - } - if (negBtn != -1) { - builder.setNegativeButton(negBtn, (dialog, which) -> { - if (mListener != null) { - mListener.onCancel(getTag()); - } - dialog.dismiss(); - }); - } - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(activity, builder); - - return builder.create(); - } - - public interface ConfirmationDialogFragmentListener { - void onConfirmation(String callerTag); - - void onNeutral(String callerTag); - - void onCancel(String callerTag); - } -} - diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.kt new file mode 100644 index 000000000000..20dd1b943ca5 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.kt @@ -0,0 +1,156 @@ +/* + * ownCloud Android client application + * + * Copyright (C) 2012 Bartek Przybylski Copyright (C) 2015 ownCloud Inc. + * + * 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 + +//noinspection SuspiciousImport +import android.R +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.di.Injectable +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject + +open class ConfirmationDialogFragment : DialogFragment(), Injectable { + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + private var mListener: ConfirmationDialogFragmentListener? = null + + override fun onStart() { + super.onStart() + + val alertDialog = dialog as AlertDialog? + + if (alertDialog != null) { + val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton? + if (positiveButton != null) { + viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton) + } + + val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton? + if (negativeButton != null) { + viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton) + } + + val neutralButton = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL) as MaterialButton? + if (neutralButton != null) { + viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(neutralButton) + } + } + } + + fun setOnConfirmationListener(listener: ConfirmationDialogFragmentListener?) { + mListener = listener + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val messageArguments = requireArguments().getStringArray(ARG_MESSAGE_ARGUMENTS) ?: arrayOf() + val titleId = requireArguments().getInt(ARG_TITLE_ID, -1) + val messageId = requireArguments().getInt(ARG_MESSAGE_RESOURCE_ID, -1) + val positiveButtonTextId = requireArguments().getInt(ARG_POSITIVE_BTN_RES, -1) + val negativeButtonTextId = requireArguments().getInt(ARG_NEGATIVE_BTN_RES, -1) + val neutralButtonTextId = requireArguments().getInt(ARG_NEUTRAL_BTN_RES, -1) + + @Suppress("SpreadOperator") + val message = getString(messageId, *messageArguments) + + val builder = MaterialAlertDialogBuilder(requireActivity()) + .setTitle(if (titleId == 0) { R.string.dialog_alert_title } else { titleId }) + .setIcon(com.owncloud.android.R.drawable.ic_warning) + .setIconAttribute(R.attr.alertDialogIcon) + .setMessage(message) + + if (positiveButtonTextId != -1) { + builder.setPositiveButton(positiveButtonTextId) { dialog: DialogInterface, _: Int -> + mListener?.onConfirmation(tag) + dialog.dismiss() + } + } + if (negativeButtonTextId != -1) { + builder.setNegativeButton(negativeButtonTextId) { dialog: DialogInterface, _: Int -> + mListener?.onCancel(tag) + dialog.dismiss() + } + } + if (neutralButtonTextId != -1) { + builder.setNeutralButton(neutralButtonTextId) { dialog: DialogInterface, _: Int -> + mListener?.onNeutral(tag) + dialog.dismiss() + } + } + + viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireActivity(), builder) + + return builder.create() + } + + interface ConfirmationDialogFragmentListener { + fun onConfirmation(callerTag: String?) + fun onNeutral(callerTag: String?) + fun onCancel(callerTag: String?) + } + + companion object { + const val ARG_MESSAGE_RESOURCE_ID = "resource_id" + const val ARG_MESSAGE_ARGUMENTS = "string_array" + const val ARG_TITLE_ID = "title_id" + const val ARG_POSITIVE_BTN_RES = "positive_btn_res" + const val ARG_NEUTRAL_BTN_RES = "neutral_btn_res" + const val ARG_NEGATIVE_BTN_RES = "negative_btn_res" + const val FTAG_CONFIRMATION = "CONFIRMATION_FRAGMENT" + + /** + * Public factory method to create new ConfirmationDialogFragment instances. + * + * @param messageResId Resource id for a message to show in the dialog. + * @param messageArguments Arguments to complete the message, if it's a format string. May be null. + * @param titleResId Resource id for a text to show in the title. 0 for default alert title, -1 for no + * title. + * @param positiveButtonTextId Resource id for the text of the positive button. -1 for no positive button. + * @param neutralButtonTextId Resource id for the text of the neutral button. -1 for no neutral button. + * @param negativeButtonTextId Resource id for the text of the negative button. -1 for no negative button. + * @return Dialog ready to show. + */ + @JvmStatic + fun newInstance( + messageResId: Int, + messageArguments: Array?, + titleResId: Int, + positiveButtonTextId: Int, + negativeButtonTextId: Int, + neutralButtonTextId: Int + ): ConfirmationDialogFragment { + check(messageResId != -1) { "Calling confirmation dialog without message resource" } + val frag = ConfirmationDialogFragment() + val args = Bundle() + args.putInt(ARG_MESSAGE_RESOURCE_ID, messageResId) + args.putStringArray(ARG_MESSAGE_ARGUMENTS, messageArguments) + args.putInt(ARG_TITLE_ID, titleResId) + args.putInt(ARG_POSITIVE_BTN_RES, positiveButtonTextId) + args.putInt(ARG_NEGATIVE_BTN_RES, negativeButtonTextId) + args.putInt(ARG_NEUTRAL_BTN_RES, neutralButtonTextId) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.java b/app/src/main/java/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.java deleted file mode 100644 index 891249e9b09a..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.java +++ /dev/null @@ -1,105 +0,0 @@ -/** - * ownCloud Android client application - * - * Copyright (C) 2015 ownCloud Inc. - * - * 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 android.app.Dialog; -import android.app.ProgressDialog; -import android.content.DialogInterface; -import android.content.DialogInterface.OnKeyListener; -import android.os.Bundle; -import android.view.KeyEvent; -import android.widget.ProgressBar; - -import com.nextcloud.client.di.Injectable; -import com.owncloud.android.R; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; - - -public class IndeterminateProgressDialog extends DialogFragment implements Injectable { - - private static final String ARG_MESSAGE_ID = IndeterminateProgressDialog.class.getCanonicalName() + ".ARG_MESSAGE_ID"; - private static final String ARG_CANCELABLE = IndeterminateProgressDialog.class.getCanonicalName() + ".ARG_CANCELABLE"; - - @Inject ViewThemeUtils viewThemeUtils; - - /** - * Public factory method to get dialog instances. - * - * @param messageId Resource id for a message to show in the dialog. - * @param cancelable If 'true', the dialog can be cancelled by the user input (BACK button, touch outside...) - * @return New dialog instance, ready to show. - */ - public static IndeterminateProgressDialog newInstance(int messageId, boolean cancelable) { - IndeterminateProgressDialog fragment = new IndeterminateProgressDialog(); - fragment.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.ownCloud_AlertDialog); - Bundle args = new Bundle(); - args.putInt(ARG_MESSAGE_ID, messageId); - args.putBoolean(ARG_CANCELABLE, cancelable); - fragment.setArguments(args); - return fragment; - } - - - /** - * {@inheritDoc} - */ - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - /// create indeterminate progress dialog - final ProgressDialog progressDialog = new ProgressDialog(getActivity(), R.style.ProgressDialogTheme); - progressDialog.setIndeterminate(true); - progressDialog.setOnShowListener(dialog -> { - ProgressBar v = progressDialog.findViewById(android.R.id.progress); - viewThemeUtils.platform.tintDrawable(requireContext(), v.getIndeterminateDrawable()); - }); - - /// set message - int messageId = getArguments().getInt(ARG_MESSAGE_ID, R.string.placeholder_sentence); - progressDialog.setMessage(getString(messageId)); - - /// set cancellation behavior - boolean cancelable = getArguments().getBoolean(ARG_CANCELABLE, false); - if (!cancelable) { - progressDialog.setCancelable(false); - // disable the back button - OnKeyListener keyListener = new OnKeyListener() { - @Override - public boolean onKey(DialogInterface dialog, int keyCode, - KeyEvent event) { - - return keyCode == KeyEvent.KEYCODE_BACK; - } - - }; - progressDialog.setOnKeyListener(keyListener); - } - - return progressDialog; - } - -} - - diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.kt b/app/src/main/java/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.kt new file mode 100644 index 000000000000..4722796e9c57 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.kt @@ -0,0 +1,92 @@ +/** + * ownCloud Android client application + * + * Copyright (C) 2015 ownCloud Inc. + * + * 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 //www.gnu.org/licenses/>. + * + */ + +@file:Suppress("DEPRECATION") + +package com.owncloud.android.ui.dialog + +import android.app.Dialog +import android.app.ProgressDialog +import android.content.DialogInterface +import android.os.Bundle +import android.view.KeyEvent +import android.widget.ProgressBar +import androidx.fragment.app.DialogFragment +import com.nextcloud.client.di.Injectable +import com.owncloud.android.R +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject + +class IndeterminateProgressDialog : DialogFragment(), Injectable { + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + /** + * {@inheritDoc} + */ + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + // / create indeterminate progress dialog + val progressDialog = ProgressDialog(requireActivity(), R.style.ProgressDialogTheme) + progressDialog.isIndeterminate = true + progressDialog.setOnShowListener { + val v = progressDialog.findViewById(android.R.id.progress) + viewThemeUtils?.platform?.tintDrawable(requireContext(), v.indeterminateDrawable) + } + + // / set message + val messageId = requireArguments().getInt(ARG_MESSAGE_ID, R.string.placeholder_sentence) + progressDialog.setMessage(getString(messageId)) + + // / set cancellation behavior + val cancelable = requireArguments().getBoolean(ARG_CANCELABLE, false) + if (!cancelable) { + progressDialog.setCancelable(false) + // disable the back button + val keyListener = + DialogInterface.OnKeyListener { _, keyCode, _ -> keyCode == KeyEvent.KEYCODE_BACK } + progressDialog.setOnKeyListener(keyListener) + } + return progressDialog + } + + companion object { + private val ARG_MESSAGE_ID = IndeterminateProgressDialog::class.java.canonicalName?.plus(".ARG_MESSAGE_ID") + private val ARG_CANCELABLE = IndeterminateProgressDialog::class.java.canonicalName?.plus(".ARG_CANCELABLE") + + /** + * Public factory method to get dialog instances. + * + * @param messageId Resource id for a message to show in the dialog. + * @param cancelable If 'true', the dialog can be cancelled by the user input (BACK button, touch outside...) + * @return New dialog instance, ready to show. + */ + @JvmStatic + fun newInstance(messageId: Int, cancelable: Boolean): IndeterminateProgressDialog { + val fragment = IndeterminateProgressDialog() + fragment.setStyle(STYLE_NO_FRAME, R.style.ownCloud_AlertDialog) + val args = Bundle() + args.putInt(ARG_MESSAGE_ID, messageId) + args.putBoolean(ARG_CANCELABLE, cancelable) + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.java b/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.java deleted file mode 100644 index 016d8ea7f83b..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * ownCloud Android client application - * - * Copyright (C) 2015 ownCloud Inc. - * - * 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 android.app.Dialog; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.nextcloud.client.di.Injectable; -import com.owncloud.android.R; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; - -public class LoadingDialog extends DialogFragment implements Injectable { - - @Inject ViewThemeUtils viewThemeUtils; - private String mMessage; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - setCancelable(false); - } - - public static LoadingDialog newInstance(String message) { - LoadingDialog loadingDialog = new LoadingDialog(); - loadingDialog.mMessage = message; - return loadingDialog; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - // Create a view by inflating desired layout - View v = inflater.inflate(R.layout.loading_dialog, container, false); - - // set value - TextView tv = v.findViewById(R.id.loadingText); - tv.setText(mMessage); - - // set progress wheel color - ProgressBar progressBar = v.findViewById(R.id.loadingBar); - viewThemeUtils.platform.tintDrawable(requireContext(), progressBar.getIndeterminateDrawable()); - - return v; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Dialog dialog = super.onCreateDialog(savedInstanceState); - dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - return dialog; - } - - @Override - public void onDestroyView() { - if (getDialog() != null && getRetainInstance()) { - getDialog().setDismissMessage(null); - } - super.onDestroyView(); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt b/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt new file mode 100644 index 000000000000..5383c95dac3f --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt @@ -0,0 +1,79 @@ +/* + * ownCloud Android client application + * + * Copyright (C) 2015 ownCloud Inc. + * + * 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 android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.client.di.Injectable +import com.owncloud.android.databinding.LoadingDialogBinding +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject + +class LoadingDialog : DialogFragment(), Injectable { + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + private var mMessage: String? = null + private lateinit var binding: LoadingDialogBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + retainInstance = true + isCancelable = false + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = LoadingDialogBinding.inflate(inflater, container, false) + binding.loadingText.text = mMessage + + val loadingDrawable = binding.loadingBar.indeterminateDrawable + if (loadingDrawable != null) { + viewThemeUtils?.platform?.tintDrawable(requireContext(), loadingDrawable) + } + + viewThemeUtils?.platform?.colorViewBackground(binding.loadingLayout, ColorRole.SURFACE_VARIANT) + + return binding.root + } + + override fun onDestroyView() { + if (dialog != null && retainInstance) { + dialog?.setDismissMessage(null) + } + + super.onDestroyView() + } + + companion object { + + @JvmStatic + fun newInstance(message: String?): LoadingDialog { + val loadingDialog = LoadingDialog() + loadingDialog.mMessage = message + return loadingDialog + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/LocalStoragePathPickerDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/LocalStoragePathPickerDialogFragment.java deleted file mode 100644 index 96e39d372ade..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/LocalStoragePathPickerDialogFragment.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Andy Scherzinger - * Copyright (C) 2019 Andy Scherzinger - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.os.Environment; -import android.view.LayoutInflater; -import android.view.View; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.nextcloud.client.di.Injectable; -import com.owncloud.android.R; -import com.owncloud.android.databinding.StoragePathDialogBinding; -import com.owncloud.android.ui.adapter.StoragePathAdapter; -import com.owncloud.android.ui.adapter.StoragePathItem; -import com.owncloud.android.utils.FileStorageUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; -import androidx.recyclerview.widget.LinearLayoutManager; - -/** - * Picker dialog for choosing a (storage) path. - */ -public class LocalStoragePathPickerDialogFragment extends DialogFragment - implements DialogInterface.OnClickListener, StoragePathAdapter.StoragePathAdapterListener, Injectable { - - public static final String LOCAL_STORAGE_PATH_PICKER_FRAGMENT = "LOCAL_STORAGE_PATH_PICKER_FRAGMENT"; - - private static Set internalStoragePaths = new HashSet<>(); - - @Inject ViewThemeUtils viewThemeUtils; - - static { - internalStoragePaths.add("/storage/emulated/legacy"); - internalStoragePaths.add("/storage/emulated/0"); - internalStoragePaths.add("/mnt/sdcard"); - } - - private StoragePathDialogBinding binding; - - public static LocalStoragePathPickerDialogFragment newInstance() { - return new LocalStoragePathPickerDialogFragment(); - } - - @Override - public void onStart() { - super.onStart(); - - AlertDialog alertDialog = (AlertDialog) getDialog(); - - if (alertDialog != null) { - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)); - } - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - if (!(getActivity() instanceof StoragePathAdapter.StoragePathAdapterListener)) { - throw new IllegalArgumentException("Calling activity must implement " + - "StoragePathAdapter.StoragePathAdapterListener"); - } - - // Inflate the layout for the dialog - LayoutInflater inflater = requireActivity().getLayoutInflater(); - binding = StoragePathDialogBinding.inflate(inflater, null, false); - View view = binding.getRoot(); - - StoragePathAdapter adapter = new StoragePathAdapter(getPathList(), this); - - binding.storagePathRecyclerView.setAdapter(adapter); - binding.storagePathRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); - - // Build the dialog - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(binding.getRoot().getContext()); - builder.setView(view) - .setNegativeButton(R.string.common_cancel, this) - .setTitle(R.string.storage_choose_location); - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.getRoot().getContext(), builder); - - return builder.create(); - } - - @Override - public void onDestroyView() { - binding = null; - super.onDestroyView(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == AlertDialog.BUTTON_NEGATIVE) { - dismissAllowingStateLoss(); - } - } - - private List getPathList() { - List storagePathItems = new ArrayList<>(); - - for (FileStorageUtils.StandardDirectory standardDirectory : FileStorageUtils.StandardDirectory.getStandardDirectories()) { - addIfExists(storagePathItems, standardDirectory.getIcon(), getString(standardDirectory.getDisplayName()), - Environment.getExternalStoragePublicDirectory(standardDirectory.getName()).getAbsolutePath()); - } - - String sdCard = getString(R.string.storage_internal_storage); - for (String dir : FileStorageUtils.getStorageDirectories(requireActivity())) { - if (internalStoragePaths.contains(dir)) { - addIfExists(storagePathItems, R.drawable.ic_sd_grey600, sdCard, dir); - } else { - addIfExists(storagePathItems, R.drawable.ic_sd_grey600, new File(dir).getName(), dir); - } - } - - return storagePathItems; - } - - private void addIfExists(List storagePathItems, int icon, String name, String path) { - File file = new File(path); - if (file.exists() && file.canRead()) { - storagePathItems.add(new StoragePathItem(icon, name, path)); - } - } - - @Override - public void chosenPath(String path) { - if (getActivity() != null) { - ((StoragePathAdapter.StoragePathAdapterListener) getActivity()).chosenPath(path); - } - dismissAllowingStateLoss(); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/LocalStoragePathPickerDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/LocalStoragePathPickerDialogFragment.kt new file mode 100644 index 000000000000..54a2baf1b38c --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/LocalStoragePathPickerDialogFragment.kt @@ -0,0 +1,148 @@ +/* + * Nextcloud Android client application + * + * @author Andy Scherzinger + * Copyright (C) 2019 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.os.Environment +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.di.Injectable +import com.owncloud.android.R +import com.owncloud.android.databinding.StoragePathDialogBinding +import com.owncloud.android.ui.adapter.StoragePathAdapter +import com.owncloud.android.ui.adapter.StoragePathAdapter.StoragePathAdapterListener +import com.owncloud.android.ui.adapter.StoragePathItem +import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.FileStorageUtils.StandardDirectory +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.io.File +import javax.inject.Inject + +class LocalStoragePathPickerDialogFragment : + DialogFragment(), + DialogInterface.OnClickListener, + StoragePathAdapterListener, + Injectable { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private lateinit var binding: StoragePathDialogBinding + + override fun onStart() { + super.onStart() + + val alertDialog = dialog as AlertDialog? + + val positiveButton = alertDialog?.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton? + positiveButton?.let { + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton) + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + require(activity is StoragePathAdapterListener) { + "Calling activity must implement " + + "StoragePathAdapter.StoragePathAdapterListener" + } + + // Inflate the layout for the dialog + val inflater = requireActivity().layoutInflater + binding = StoragePathDialogBinding.inflate(inflater, null, false) + + val adapter = StoragePathAdapter(pathList, this, viewThemeUtils) + binding.storagePathRecyclerView.adapter = adapter + binding.storagePathRecyclerView.layoutManager = LinearLayoutManager(requireActivity()) + + // Build the dialog + val builder = MaterialAlertDialogBuilder(requireContext()) + builder + .setView(binding.root) + .setPositiveButton(R.string.common_cancel, this) + .setTitle(R.string.storage_choose_location) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(requireContext(), builder) + + return builder.create() + } + + override fun onClick(dialog: DialogInterface, which: Int) { + if (which == AlertDialog.BUTTON_POSITIVE) { + dismissAllowingStateLoss() + } + } + + private val pathList: List + get() { + val storagePathItems: MutableList = ArrayList() + for (standardDirectory in StandardDirectory.getStandardDirectories()) { + addIfExists( + storagePathItems, + standardDirectory.icon, + getString(standardDirectory.displayName), + Environment.getExternalStoragePublicDirectory(standardDirectory.name).absolutePath + ) + } + val sdCard = getString(R.string.storage_internal_storage) + for (dir in FileStorageUtils.getStorageDirectories(requireActivity())) { + if (internalStoragePaths.contains(dir)) { + addIfExists(storagePathItems, R.drawable.ic_sd_grey600, sdCard, dir) + } else { + addIfExists(storagePathItems, R.drawable.ic_sd_grey600, File(dir).name, dir) + } + } + return storagePathItems + } + + private fun addIfExists(storagePathItems: MutableList, icon: Int, name: String, path: String) { + val file = File(path) + if (file.exists() && file.canRead()) { + storagePathItems.add(StoragePathItem(icon, name, path)) + } + } + + override fun chosenPath(path: String) { + if (activity != null) { + (activity as StoragePathAdapterListener?)!!.chosenPath(path) + } + dismissAllowingStateLoss() + } + + companion object { + const val LOCAL_STORAGE_PATH_PICKER_FRAGMENT = "LOCAL_STORAGE_PATH_PICKER_FRAGMENT" + private val internalStoragePaths: MutableSet = HashSet() + + init { + internalStoragePaths.add("/storage/emulated/legacy") + internalStoragePaths.add("/storage/emulated/0") + internalStoragePaths.add("/mnt/sdcard") + } + + @JvmStatic + fun newInstance(): LocalStoragePathPickerDialogFragment { + return LocalStoragePathPickerDialogFragment() + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.java b/app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.java deleted file mode 100644 index 350dcf109b24..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * - * Nextcloud Android client application - * - * @author Tobias Kaminsky - * @author Chris Narkiewicz - * - * Copyright (C) 2019 Tobias Kaminsky - * Copyright (C) 2019 Nextcloud GmbH - * Copyright (C) 2020 Chris Narkiewicz - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -package com.owncloud.android.ui.dialog; - -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.di.Injectable; -import com.owncloud.android.R; -import com.owncloud.android.databinding.MultipleAccountsBinding; -import com.owncloud.android.ui.adapter.UserListAdapter; -import com.owncloud.android.ui.adapter.UserListItem; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; -import androidx.recyclerview.widget.LinearLayoutManager; - -public class MultipleAccountsDialog extends DialogFragment implements Injectable, UserListAdapter.ClickListener { - - @Inject UserAccountManager accountManager; - @Inject ViewThemeUtils viewThemeUtils; - public boolean highlightCurrentlyActiveAccount = true; - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Activity activity = getActivity(); - if (activity == null) { - throw new IllegalArgumentException("Activity may not be null"); - } - - // Inflate the layout for the dialog - LayoutInflater inflater = activity.getLayoutInflater(); - MultipleAccountsBinding binding = MultipleAccountsBinding.inflate(inflater, null, false); - - final Context parent = getActivity(); - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(binding.getRoot().getContext()); - - UserListAdapter adapter = new UserListAdapter(parent, - accountManager, - getAccountListItems(), - this, - false, - highlightCurrentlyActiveAccount, - false, - viewThemeUtils); - - binding.list.setHasFixedSize(true); - binding.list.setLayoutManager(new LinearLayoutManager(activity)); - binding.list.setAdapter(adapter); - - builder.setView(binding.getRoot()).setTitle(R.string.common_choose_account); - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.getRoot().getContext(), builder); - - return builder.create(); - } - - /** - * creates the account list items list including the add-account action in case - * multiaccount_support is enabled. - * - * @return list of account list items - */ - private List getAccountListItems() { - List users = accountManager.getAllUsers(); - List adapterUserList = new ArrayList<>(users.size()); - for (User user : users) { - adapterUserList.add(new UserListItem(user)); - } - - return adapterUserList; - } - - @Override - public void onOptionItemClicked(User user, View view) { - // By default, access account if option is clicked - onAccountClicked(user); - } - - @Override - public void onAccountClicked(User user) { - final AccountChooserInterface parentActivity = (AccountChooserInterface) getActivity(); - if (parentActivity != null) { - parentActivity.onAccountChosen(user); - } - dismiss(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.kt b/app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.kt new file mode 100644 index 000000000000..94779962cdba --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.kt @@ -0,0 +1,108 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * @author Chris Narkiewicz + * + * Copyright (C) 2019 Tobias Kaminsky + * Copyright (C) 2019 Nextcloud GmbH + * Copyright (C) 2020 Chris Narkiewicz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +package com.owncloud.android.ui.dialog + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import androidx.fragment.app.DialogFragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.di.Injectable +import com.owncloud.android.R +import com.owncloud.android.databinding.MultipleAccountsBinding +import com.owncloud.android.ui.adapter.UserListAdapter +import com.owncloud.android.ui.adapter.UserListItem +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject + +class MultipleAccountsDialog : DialogFragment(), Injectable, UserListAdapter.ClickListener { + @JvmField + @Inject + var accountManager: UserAccountManager? = null + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + var highlightCurrentlyActiveAccount = true + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val inflater = requireActivity().layoutInflater + val binding = MultipleAccountsBinding.inflate(inflater, null, false) + + val builder = MaterialAlertDialogBuilder(binding.root.context) + val adapter = UserListAdapter( + requireActivity(), + accountManager, + accountListItems, + this, + false, + highlightCurrentlyActiveAccount, + false, + viewThemeUtils + ) + + binding.list.setHasFixedSize(true) + binding.list.layoutManager = LinearLayoutManager(requireActivity()) + binding.list.adapter = adapter + + builder.setView(binding.root).setTitle(R.string.common_choose_account) + + viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireContext(), builder) + + return builder.create() + } + + private val accountListItems: List + /** + * creates the account list items list including the add-account action in case + * multiaccount_support is enabled. + * + * @return list of account list items + */ + get() { + val users = accountManager?.allUsers ?: listOf() + + val adapterUserList: MutableList = ArrayList(users.size) + for (user in users) { + adapterUserList.add(UserListItem(user)) + } + return adapterUserList + } + + override fun onOptionItemClicked(user: User, view: View) { + // By default, access account if option is clicked + onAccountClicked(user) + } + + override fun onAccountClicked(user: User) { + val parentActivity = activity as AccountChooserInterface? + parentActivity?.onAccountChosen(user) + dismiss() + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.java deleted file mode 100644 index ca73f2862f5e..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * ownCloud Android client application - * - * @author masensio - * @author Andy Scherzinger - * Copyright (C) 2015 ownCloud GmbH. - * Copyright (C) 2018 Andy Scherzinger - * - * 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 android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.nextcloud.client.di.Injectable; -import com.owncloud.android.R; -import com.owncloud.android.databinding.PasswordDialogBinding; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.resources.shares.OCShare; -import com.owncloud.android.ui.activity.FileActivity; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.KeyboardUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; - -/** - * Dialog to input the password for sharing a file/folder. - *

- * Triggers the share when the password is introduced. - */ -public class SharePasswordDialogFragment extends DialogFragment implements DialogInterface.OnClickListener, Injectable { - - private static final String ARG_FILE = "FILE"; - private static final String ARG_SHARE = "SHARE"; - private static final String ARG_CREATE_SHARE = "CREATE_SHARE"; - private static final String ARG_ASK_FOR_PASSWORD = "ASK_FOR_PASSWORD"; - public static final String PASSWORD_FRAGMENT = "PASSWORD_FRAGMENT"; - - @Inject ViewThemeUtils viewThemeUtils; - @Inject KeyboardUtils keyboardUtils; - - private PasswordDialogBinding binding; - private OCFile file; - private OCShare share; - private boolean createShare; - private boolean askForPassword; - - @Override - public void onStart() { - super.onStart(); - - AlertDialog alertDialog = (AlertDialog) getDialog(); - if (alertDialog != null) { - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE), - alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)); - viewThemeUtils.platform.colorTextButtons(getResources().getColor(R.color.highlight_textColor_Warning), - alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL)); - - alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { - String password = binding.sharePassword.getText().toString(); - - if (!askForPassword && TextUtils.isEmpty(password)) { - DisplayUtils.showSnackMessage(binding.getRoot(), R.string.share_link_empty_password); - return; - } - - if (share == null) { - setPassword(createShare, file, password); - } else { - setPassword(share, password); - } - - alertDialog.dismiss(); - }); - } - } - - @Override - public void onResume() { - super.onResume(); - keyboardUtils.showKeyboardForEditText(requireDialog().getWindow(), binding.sharePassword); - } - - /** - * Public factory method to create new SharePasswordDialogFragment instances. - * - * @param file OCFile bound to the public share that which password will be set or updated - * @param createShare When 'true', the request for password will be followed by the creation of a new public link; - * when 'false', a public share is assumed to exist, and the password is bound to it. - * @return Dialog ready to show. - */ - public static SharePasswordDialogFragment newInstance(OCFile file, boolean createShare, boolean askForPassword) { - SharePasswordDialogFragment frag = new SharePasswordDialogFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_FILE, file); - args.putBoolean(ARG_CREATE_SHARE, createShare); - args.putBoolean(ARG_ASK_FOR_PASSWORD, askForPassword); - frag.setArguments(args); - return frag; - } - - /** - * Public factory method to create new SharePasswordDialogFragment instances. - * - * @param share OCFile bound to the public share that which password will be set or updated - * @return Dialog ready to show. - */ - public static SharePasswordDialogFragment newInstance(OCShare share, boolean askForPassword) { - SharePasswordDialogFragment frag = new SharePasswordDialogFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_SHARE, share); - args.putBoolean(ARG_ASK_FOR_PASSWORD, askForPassword); - frag.setArguments(args); - return frag; - } - - /** - * Public factory method to create new SharePasswordDialogFragment instances. - * - * @param share OCFile bound to the public share that which password will be set or updated - * @return Dialog ready to show. - */ - public static SharePasswordDialogFragment newInstance(OCShare share) { - SharePasswordDialogFragment frag = new SharePasswordDialogFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_SHARE, share); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - file = getArguments().getParcelable(ARG_FILE); - share = getArguments().getParcelable(ARG_SHARE); - createShare = getArguments().getBoolean(ARG_CREATE_SHARE, false); - askForPassword = getArguments().getBoolean(ARG_ASK_FOR_PASSWORD, false); - - // Inflate the layout for the dialog - LayoutInflater inflater = requireActivity().getLayoutInflater(); - binding = PasswordDialogBinding.inflate(inflater, null, false); - View view = binding.getRoot(); - - // Setup layout - binding.sharePassword.setText(""); - viewThemeUtils.material.colorTextInputLayout(binding.sharePasswordContainer); - - int negativeButtonCaption; - int title; - if (askForPassword) { - title = R.string.share_link_optional_password_title; - negativeButtonCaption = R.string.common_skip; - } else { - title = R.string.share_link_password_title; - negativeButtonCaption = R.string.common_cancel; - } - - // Build the dialog - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(view.getContext()); - - builder.setView(view) - .setPositiveButton(R.string.common_ok, null) - .setNegativeButton(negativeButtonCaption, this) - .setNeutralButton(R.string.common_delete, this) - .setTitle(title); - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(view.getContext(), builder); - - return builder.create(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == AlertDialog.BUTTON_NEUTRAL) { - if (share == null) { - setPassword(createShare, file, null); - } else { - setPassword(share, null); - } - } else if (which == AlertDialog.BUTTON_NEGATIVE && askForPassword) { - if (share == null) { - setPassword(createShare, file, null); - } else { - setPassword(share, null); - } - } - } - - private void setPassword(boolean createShare, OCFile file, String password) { - if (createShare) { - ((FileActivity) getActivity()).getFileOperationsHelper().shareFileViaPublicShare(file, password); - } else { - ((FileActivity) getActivity()).getFileOperationsHelper().setPasswordToShare(share, password); - } - } - - private void setPassword(OCShare share, String password) { - ((FileActivity) getActivity()).getFileOperationsHelper().setPasswordToShare(share, password); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.kt new file mode 100644 index 000000000000..6f7cdd08705b --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.kt @@ -0,0 +1,250 @@ +/* + * ownCloud Android client application + * + * @author masensio + * @author Andy Scherzinger + * Copyright (C) 2015 ownCloud GmbH. + * Copyright (C) 2018 Andy Scherzinger + * + * 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 android.app.Dialog +import android.content.DialogInterface +import android.os.Build +import android.os.Bundle +import android.text.TextUtils +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.di.Injectable +import com.owncloud.android.R +import com.owncloud.android.databinding.PasswordDialogBinding +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.KeyboardUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject + +/** + * Dialog to input the password for sharing a file/folder. + * + * + * Triggers the share when the password is introduced. + */ +class SharePasswordDialogFragment : DialogFragment(), Injectable { + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + @JvmField + @Inject + var keyboardUtils: KeyboardUtils? = null + + private var binding: PasswordDialogBinding? = null + private var file: OCFile? = null + private var share: OCShare? = null + private var createShare = false + private var askForPassword = false + + override fun onStart() { + super.onStart() + + val alertDialog = dialog as AlertDialog? + + if (alertDialog != null) { + val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton? + if (positiveButton != null) { + viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton) + positiveButton.setOnClickListener { + val sharePassword = binding?.sharePassword?.text + + if (sharePassword != null) { + val password = sharePassword.toString() + if (!askForPassword && TextUtils.isEmpty(password)) { + DisplayUtils.showSnackMessage(binding?.root, R.string.share_link_empty_password) + return@setOnClickListener + } + if (share == null) { + setPassword(createShare, file, password) + } else { + setPassword(share!!, password) + } + } + + alertDialog.dismiss() + } + } + + val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton? + if (negativeButton != null) { + viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton) + } + + val neutralButton = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL) as MaterialButton? + if (neutralButton != null) { + val warningColorId = ContextCompat.getColor(requireContext(), R.color.highlight_textColor_Warning) + viewThemeUtils?.platform?.colorTextButtons(warningColorId, neutralButton) + } + } + } + + override fun onResume() { + super.onResume() + keyboardUtils?.showKeyboardForEditText(requireDialog().window, binding!!.sharePassword) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + file = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requireArguments().getParcelable(ARG_FILE, OCFile::class.java) + } else { + @Suppress("DEPRECATION") + requireArguments().getParcelable(ARG_FILE) + } + + share = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requireArguments().getParcelable(ARG_SHARE, OCShare::class.java) + } else { + @Suppress("DEPRECATION") + requireArguments().getParcelable(ARG_SHARE) + } + + createShare = requireArguments().getBoolean(ARG_CREATE_SHARE, false) + askForPassword = requireArguments().getBoolean(ARG_ASK_FOR_PASSWORD, false) + + // Inflate the layout for the dialog + val inflater = requireActivity().layoutInflater + binding = PasswordDialogBinding.inflate(inflater, null, false) + + // Setup layout + binding?.sharePassword?.setText("") + viewThemeUtils?.material?.colorTextInputLayout(binding!!.sharePasswordContainer) + + val neutralButtonTextId: Int + val title: Int + if (askForPassword) { + title = R.string.share_link_optional_password_title + neutralButtonTextId = R.string.common_skip + } else { + title = R.string.share_link_password_title + neutralButtonTextId = R.string.common_cancel + } + + // Build the dialog + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setView(binding!!.root) + .setPositiveButton(R.string.common_ok, null) + .setNegativeButton(R.string.common_delete) { _: DialogInterface?, _: Int -> callSetPassword() } + .setNeutralButton(neutralButtonTextId) { _: DialogInterface?, _: Int -> + if (askForPassword) { + callSetPassword() + } + } + .setTitle(title) + + viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireContext(), builder) + + return builder.create() + } + + private fun callSetPassword() { + if (share == null) { + setPassword(createShare, file, null) + } else { + setPassword(share!!, null) + } + } + + private fun setPassword(createShare: Boolean, file: OCFile?, password: String?) { + val fileOperationsHelper = (requireActivity() as FileActivity).fileOperationsHelper ?: return + if (createShare) { + fileOperationsHelper.shareFileViaPublicShare(file, password) + } else { + fileOperationsHelper.setPasswordToShare(share, password) + } + } + + private fun setPassword(share: OCShare, password: String?) { + val fileOperationsHelper = (requireActivity() as FileActivity).fileOperationsHelper ?: return + fileOperationsHelper.setPasswordToShare(share, password) + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + companion object { + private const val ARG_FILE = "FILE" + private const val ARG_SHARE = "SHARE" + private const val ARG_CREATE_SHARE = "CREATE_SHARE" + private const val ARG_ASK_FOR_PASSWORD = "ASK_FOR_PASSWORD" + const val PASSWORD_FRAGMENT = "PASSWORD_FRAGMENT" + + /** + * Public factory method to create new SharePasswordDialogFragment instances. + * + * @param file OCFile bound to the public share that which + * password will be set or updated + * @param createShare When 'true', the request for password will be + * followed by the creation of a new public link + * when 'false', a public share is assumed to exist, and the password is bound to it. + * @return Dialog ready to show. + */ + @JvmStatic + fun newInstance(file: OCFile?, createShare: Boolean, askForPassword: Boolean): SharePasswordDialogFragment { + val frag = SharePasswordDialogFragment() + val args = Bundle() + args.putParcelable(ARG_FILE, file) + args.putBoolean(ARG_CREATE_SHARE, createShare) + args.putBoolean(ARG_ASK_FOR_PASSWORD, askForPassword) + frag.arguments = args + return frag + } + + /** + * Public factory method to create new SharePasswordDialogFragment instances. + * + * @param share OCFile bound to the public share that which password will be set or updated + * @return Dialog ready to show. + */ + @JvmStatic + fun newInstance(share: OCShare?, askForPassword: Boolean): SharePasswordDialogFragment { + val frag = SharePasswordDialogFragment() + val args = Bundle() + args.putParcelable(ARG_SHARE, share) + args.putBoolean(ARG_ASK_FOR_PASSWORD, askForPassword) + frag.arguments = args + return frag + } + + /** + * Public factory method to create new SharePasswordDialogFragment instances. + * + * @param share OCFile bound to the public share that which password will be set or updated + * @return Dialog ready to show. + */ + fun newInstance(share: OCShare?): SharePasswordDialogFragment { + val frag = SharePasswordDialogFragment() + val args = Bundle() + args.putParcelable(ARG_SHARE, share) + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.java deleted file mode 100644 index 764ca5c5e480..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Andy Scherzinger - * Copyright (C) 2017 Andy Scherzinger - * Copyright (C) 2017 Nextcloud - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * 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 AFFERO GENERAL PUBLIC LICENSE for more details. - * - * You should have received a copy of the GNU Affero General Public - * License along with this program. If not, see . - */ - -package com.owncloud.android.ui.dialog; - -import android.app.Dialog; -import android.graphics.Typeface; -import android.os.Bundle; -import android.view.View; -import android.widget.ImageButton; -import android.widget.TextView; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.nextcloud.client.di.Injectable; -import com.owncloud.android.R; -import com.owncloud.android.databinding.SortingOrderFragmentBinding; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.utils.FileSortOrder; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; - -/** - * Dialog to show and choose the sorting order for the file listing. - */ -public class SortingOrderDialogFragment extends DialogFragment implements Injectable { - - private final static String TAG = SortingOrderDialogFragment.class.getSimpleName(); - - public static final String SORTING_ORDER_FRAGMENT = "SORTING_ORDER_FRAGMENT"; - private static final String KEY_SORT_ORDER = "SORT_ORDER"; - - private SortingOrderFragmentBinding binding; - private View[] mTaggedViews; - private String mCurrentSortOrderName; - - - @Inject ViewThemeUtils viewThemeUtils; - - public static SortingOrderDialogFragment newInstance(FileSortOrder sortOrder) { - SortingOrderDialogFragment dialogFragment = new SortingOrderDialogFragment(); - - Bundle args = new Bundle(); - args.putString(KEY_SORT_ORDER, sortOrder.name); - dialogFragment.setArguments(args); - - dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog); - - return dialogFragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // keep the state of the fragment on configuration changes - setRetainInstance(true); - - binding = null; - mCurrentSortOrderName = getArguments().getString(KEY_SORT_ORDER, FileSortOrder.sort_a_to_z.name); - } - - /** - * find all relevant UI elements and set their values. - * - * @param binding the parent binding - */ - private void setupDialogElements(SortingOrderFragmentBinding binding) { - viewThemeUtils.platform.colorTextButtons(binding.cancel); - - mTaggedViews = new View[12]; - mTaggedViews[0] = binding.sortByNameAscending; - mTaggedViews[0].setTag(FileSortOrder.sort_a_to_z); - mTaggedViews[1] = binding.sortByNameAZText; - mTaggedViews[1].setTag(FileSortOrder.sort_a_to_z); - mTaggedViews[2] = binding.sortByNameDescending; - mTaggedViews[2].setTag(FileSortOrder.sort_z_to_a); - mTaggedViews[3] = binding.sortByNameZAText; - mTaggedViews[3].setTag(FileSortOrder.sort_z_to_a); - mTaggedViews[4] = binding.sortByModificationDateAscending; - mTaggedViews[4].setTag(FileSortOrder.sort_old_to_new); - mTaggedViews[5] = binding.sortByModificationDateOldestFirstText; - mTaggedViews[5].setTag(FileSortOrder.sort_old_to_new); - mTaggedViews[6] = binding.sortByModificationDateDescending; - mTaggedViews[6].setTag(FileSortOrder.sort_new_to_old); - mTaggedViews[7] = binding.sortByModificationDateNewestFirstText; - mTaggedViews[7].setTag(FileSortOrder.sort_new_to_old); - mTaggedViews[8] = binding.sortBySizeAscending; - mTaggedViews[8].setTag(FileSortOrder.sort_small_to_big); - mTaggedViews[9] = binding.sortBySizeSmallestFirstText; - mTaggedViews[9].setTag(FileSortOrder.sort_small_to_big); - mTaggedViews[10] = binding.sortBySizeDescending; - mTaggedViews[10].setTag(FileSortOrder.sort_big_to_small); - mTaggedViews[11] = binding.sortBySizeBiggestFirstText; - mTaggedViews[11].setTag(FileSortOrder.sort_big_to_small); - - setupActiveOrderSelection(); - } - - /** - * tints the icon reflecting the actual sorting choice in the apps primary color. - */ - private void setupActiveOrderSelection() { - for (View view : mTaggedViews) { - if (!((FileSortOrder) view.getTag()).name.equals(mCurrentSortOrderName)) { - continue; - } - if (view instanceof ImageButton) { - viewThemeUtils.platform.themeImageButton((ImageButton) view); - ((ImageButton) view).setSelected(true); - } - if (view instanceof TextView) { - viewThemeUtils.platform.colorPrimaryTextViewElement((TextView) view); - ((TextView) view).setTypeface(Typeface.DEFAULT_BOLD); - } - } - } - - /** - * setup all listeners. - */ - private void setupListeners() { - binding.cancel.setOnClickListener(view -> dismiss()); - - OnSortOrderClickListener sortOrderClickListener = new OnSortOrderClickListener(); - - for (View view : mTaggedViews) { - view.setOnClickListener(sortOrderClickListener); - } - } - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - binding = SortingOrderFragmentBinding.inflate(requireActivity().getLayoutInflater(), null, false); - - setupDialogElements(binding); - setupListeners(); - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(binding.getRoot().getContext()); - builder.setView(binding.getRoot()); - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.getRoot().getContext(), builder); - - return builder.create(); - } - - @Override - public void onDestroyView() { - Log_OC.d(TAG, "destroy SortingOrderDialogFragment view"); - if (getDialog() != null && getRetainInstance()) { - getDialog().setDismissMessage(null); - } - super.onDestroyView(); - } - - private class OnSortOrderClickListener implements View.OnClickListener { - @Override - public void onClick(View v) { - dismissAllowingStateLoss(); - ((SortingOrderDialogFragment.OnSortingOrderListener) getActivity()) - .onSortingOrderChosen((FileSortOrder) v.getTag()); - } - } - - public interface OnSortingOrderListener { - void onSortingOrderChosen(FileSortOrder selection); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.kt new file mode 100644 index 000000000000..3c1a84871e10 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.kt @@ -0,0 +1,134 @@ +/* + * Nextcloud Android client application + * + * @author Andy Scherzinger + * Copyright (C) 2017 Andy Scherzinger + * Copyright (C) 2017 Nextcloud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * 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 AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this program. If not, see . + */ +package com.owncloud.android.ui.dialog + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.di.Injectable +import com.owncloud.android.R +import com.owncloud.android.databinding.SortingOrderFragmentBinding +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.FileSortOrder +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject + +/** + * Dialog to show and choose the sorting order for the file listing. + */ +class SortingOrderDialogFragment : DialogFragment(), Injectable { + + private var binding: SortingOrderFragmentBinding? = null + + private var currentSortOrderName: String? = null + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // keep the state of the fragment on configuration changes + retainInstance = true + + binding = null + currentSortOrderName = requireArguments().getString(KEY_SORT_ORDER, FileSortOrder.sort_a_to_z.name) + } + + /** + * find all relevant UI elements and set their values. + * + * @param binding the parent binding + */ + private fun setupDialogElements(binding: SortingOrderFragmentBinding) { + val bindings = listOf( + binding.sortByNameAscending to FileSortOrder.sort_a_to_z, + binding.sortByNameDescending to FileSortOrder.sort_z_to_a, + binding.sortByModificationDateAscending to FileSortOrder.sort_old_to_new, + binding.sortByModificationDateDescending to FileSortOrder.sort_new_to_old, + binding.sortBySizeAscending to FileSortOrder.sort_small_to_big, + binding.sortBySizeDescending to FileSortOrder.sort_big_to_small + ) + + bindings.forEach { (view, sortOrder) -> + view.tag = sortOrder + view.let { + it.setOnClickListener(OnSortOrderClickListener()) + viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(it) + } + } + + viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(binding.cancel) + binding.cancel.setOnClickListener { dismiss() } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = SortingOrderFragmentBinding.inflate(requireActivity().layoutInflater, null, false) + setupDialogElements(binding!!) + + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setView(binding?.root) + + viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireContext(), builder) + + return builder.create() + } + + override fun onDestroyView() { + Log_OC.d(TAG, "destroy SortingOrderDialogFragment view") + + if (dialog != null && retainInstance) { + dialog?.setDismissMessage(null) + } + + super.onDestroyView() + } + + private inner class OnSortOrderClickListener : View.OnClickListener { + override fun onClick(v: View) { + dismissAllowingStateLoss() + (activity as OnSortingOrderListener?)?.onSortingOrderChosen(v.tag as FileSortOrder) + } + } + + interface OnSortingOrderListener { + fun onSortingOrderChosen(selection: FileSortOrder?) + } + + companion object { + + private val TAG = SortingOrderDialogFragment::class.java.simpleName + const val SORTING_ORDER_FRAGMENT = "SORTING_ORDER_FRAGMENT" + private const val KEY_SORT_ORDER = "SORT_ORDER" + + @JvmStatic + fun newInstance(sortOrder: FileSortOrder): SortingOrderDialogFragment { + val dialogFragment = SortingOrderDialogFragment() + val args = Bundle() + args.putString(KEY_SORT_ORDER, sortOrder.name) + dialogFragment.arguments = args + dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog) + return dialogFragment + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.java deleted file mode 100644 index a614a8a8a6f9..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Kilian Périsset - * Copyright (C) 2020 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License (GPLv3), - * 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 android.app.Dialog; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.os.storage.StorageManager; - -import com.nextcloud.client.di.Injectable; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener; -import com.owncloud.android.ui.fragment.OCFileListFragment; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.appcompat.app.AlertDialog; - -/** - * Dialog requiring confirmation when a file/folder is too "big" to be synchronized/downloaded on device. - */ -public class SyncFileNotEnoughSpaceDialogFragment extends ConfirmationDialogFragment implements - ConfirmationDialogFragmentListener, Injectable { - - private static final String ARG_PASSED_FILE = "fragment_parent_caller"; - private static final int REQUEST_CODE_STORAGE = 20; - - private OCFile targetFile; - - @Inject ViewThemeUtils viewThemeUtils; - - public static SyncFileNotEnoughSpaceDialogFragment newInstance(OCFile file, long availableDeviceSpace) { - Bundle args = new Bundle(); - SyncFileNotEnoughSpaceDialogFragment frag = new SyncFileNotEnoughSpaceDialogFragment(); - String properFileSize = DisplayUtils.bytesToHumanReadable(file.getFileLength()); - String properDiskAvailableSpace = DisplayUtils.bytesToHumanReadable(availableDeviceSpace); - - // Defining title, message and resources - args.putInt(ARG_TITLE_ID, R.string.sync_not_enough_space_dialog_title); - args.putInt(ARG_MESSAGE_RESOURCE_ID, R.string.sync_not_enough_space_dialog_placeholder); - args.putStringArray(ARG_MESSAGE_ARGUMENTS, - new String[]{ - file.getFileName(), - properFileSize, - properDiskAvailableSpace}); - args.putParcelable(ARG_PASSED_FILE, file); - - // Defining buttons - if (file.isFolder()) { - args.putInt(ARG_POSITIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_choose); - } - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) { - args.putInt(ARG_NEGATIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_free_space); - } - args.putInt(ARG_NEUTRAL_BTN_RES, R.string.common_cancel); - - frag.setArguments(args); - return frag; - } - - @Override - public void onStart() { - super.onStart(); - - AlertDialog alertDialog = (AlertDialog) getDialog(); - - if (alertDialog != null) { - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE), - alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL), - alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)); - } - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Bundle arguments = getArguments(); - - if (arguments == null) { - throw new IllegalArgumentException("Arguments may not be null"); - } - - targetFile = arguments.getParcelable(ARG_PASSED_FILE); - setOnConfirmationListener(this); - - return super.onCreateDialog(savedInstanceState); - } - - /** - * (Only if file is a folder), will access the destination folder to allow user to choose what to synchronize - */ - @Override - public void onConfirmation(String callerTag) { - OCFileListFragment frag = (OCFileListFragment) getTargetFragment(); - if (frag != null && targetFile != null) { - frag.onItemClicked(targetFile); - } - } - - /** - * Will abort/cancel the process (is neutral to "hack" android button position ._.) - */ - @Override - public void onNeutral(String callerTag) { - // Nothing - } - - /** - * Will access to storage manager in order to empty useless files - */ - @RequiresApi(api = Build.VERSION_CODES.N_MR1) - @Override - public void onCancel(String callerTag) { - Intent storageIntent = new Intent(StorageManager.ACTION_MANAGE_STORAGE); - startActivityForResult(storageIntent, REQUEST_CODE_STORAGE); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.kt new file mode 100644 index 000000000000..ed1f7c4d251c --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.kt @@ -0,0 +1,119 @@ +/* + * Nextcloud Android client application + * + * @author Kilian Périsset + * Copyright (C) 2020 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License (GPLv3), + * 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 android.app.Dialog +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.storage.StorageManager +import androidx.annotation.RequiresApi +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener +import com.owncloud.android.ui.fragment.OCFileListFragment +import com.owncloud.android.utils.DisplayUtils + +/** + * Dialog requiring confirmation when a file/folder is too "big" to be synchronized/downloaded on device. + */ +class SyncFileNotEnoughSpaceDialogFragment : + ConfirmationDialogFragment(), + ConfirmationDialogFragmentListener { + + private var targetFile: OCFile? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + targetFile = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requireArguments().getParcelable(ARG_PASSED_FILE, OCFile::class.java) + } else { + @Suppress("DEPRECATION") + requireArguments().getParcelable(ARG_PASSED_FILE) + } + + setOnConfirmationListener(this) + + return super.onCreateDialog(savedInstanceState) + } + + /** + * (Only if file is a folder), will access the destination folder to allow user to choose what to synchronize + */ + override fun onConfirmation(callerTag: String?) { + val frag = targetFragment as OCFileListFragment? + + if (frag != null && targetFile != null) { + frag.onItemClicked(targetFile) + } + } + + /** + * Will abort/cancel the process (is neutral to "hack" android button position ._.) + */ + override fun onNeutral(callerTag: String?) { + // Nothing + } + + /** + * Will access to storage manager in order to empty useless files + */ + @RequiresApi(api = Build.VERSION_CODES.N_MR1) + override fun onCancel(callerTag: String?) { + val storageIntent = Intent(StorageManager.ACTION_MANAGE_STORAGE) + startActivityForResult(storageIntent, REQUEST_CODE_STORAGE) + } + + companion object { + private const val ARG_PASSED_FILE = "fragment_parent_caller" + private const val REQUEST_CODE_STORAGE = 20 + + @JvmStatic + fun newInstance(file: OCFile, availableDeviceSpace: Long): SyncFileNotEnoughSpaceDialogFragment { + val args = Bundle() + val frag = SyncFileNotEnoughSpaceDialogFragment() + val properFileSize = DisplayUtils.bytesToHumanReadable(file.fileLength) + val properDiskAvailableSpace = DisplayUtils.bytesToHumanReadable(availableDeviceSpace) + + // Defining title, message and resources + args.putInt(ARG_TITLE_ID, R.string.sync_not_enough_space_dialog_title) + args.putInt(ARG_MESSAGE_RESOURCE_ID, R.string.sync_not_enough_space_dialog_placeholder) + args.putStringArray( + ARG_MESSAGE_ARGUMENTS, + arrayOf( + file.fileName, + properFileSize, + properDiskAvailableSpace + ) + ) + args.putParcelable(ARG_PASSED_FILE, file) + + // Defining buttons + if (file.isFolder) { + args.putInt(ARG_POSITIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_choose) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + args.putInt(ARG_NEGATIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_free_space) + } + args.putInt(ARG_NEUTRAL_BTN_RES, R.string.common_cancel) + + frag.arguments = args + return frag + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java deleted file mode 100644 index 48a13f8f3281..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java +++ /dev/null @@ -1,655 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Andy Scherzinger - * Copyright (C) 2016 Andy Scherzinger - * Copyright (C) 2016 Nextcloud - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * 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 AFFERO GENERAL PUBLIC LICENSE for more details. - * - * You should have received a copy of the GNU Affero General Public - * License along with this program. If not, see . - */ -package com.owncloud.android.ui.dialog; - -import android.app.Activity; -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.graphics.Typeface; -import android.os.Bundle; -import android.text.TextUtils; -import android.text.style.StyleSpan; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.AdapterView; -import android.widget.LinearLayout; -import android.widget.Spinner; -import android.widget.TextView; - -import com.google.android.material.button.MaterialButton; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.nextcloud.client.di.Injectable; -import com.nextcloud.client.preferences.SubFolderRule; -import com.owncloud.android.R; -import com.owncloud.android.databinding.SyncedFoldersSettingsLayoutBinding; -import com.owncloud.android.datamodel.MediaFolderType; -import com.owncloud.android.datamodel.SyncedFolder; -import com.owncloud.android.datamodel.SyncedFolderDisplayItem; -import com.owncloud.android.files.services.NameCollisionPolicy; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.ui.activity.FolderPickerActivity; -import com.owncloud.android.ui.activity.UploadFilesActivity; -import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.FileStorageUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.io.File; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.AppCompatCheckBox; -import androidx.appcompat.widget.SwitchCompat; -import androidx.fragment.app.DialogFragment; - -import static com.owncloud.android.datamodel.SyncedFolderDisplayItem.UNPERSISTED_ID; -import static com.owncloud.android.ui.activity.UploadFilesActivity.REQUEST_CODE_KEY; - -/** - * Dialog to show the preferences/configuration of a synced folder allowing the user to change the different - * parameters. - */ -public class SyncedFolderPreferencesDialogFragment extends DialogFragment implements Injectable { - - public static final String SYNCED_FOLDER_PARCELABLE = "SyncedFolderParcelable"; - public static final int REQUEST_CODE__SELECT_REMOTE_FOLDER = 0; - public static final int REQUEST_CODE__SELECT_LOCAL_FOLDER = 1; - - private final static String TAG = SyncedFolderPreferencesDialogFragment.class.getSimpleName(); - private static final String BEHAVIOUR_DIALOG_STATE = "BEHAVIOUR_DIALOG_STATE"; - private static final String NAME_COLLISION_POLICY_DIALOG_STATE = "NAME_COLLISION_POLICY_DIALOG_STATE"; - private final static float alphaEnabled = 1.0f; - private final static float alphaDisabled = 0.7f; - - @Inject ViewThemeUtils viewThemeUtils; - - private CharSequence[] mUploadBehaviorItemStrings; - private CharSequence[] mNameCollisionPolicyItemStrings; - private SwitchCompat mEnabledSwitch; - private AppCompatCheckBox mUploadOnWifiCheckbox; - private AppCompatCheckBox mUploadOnChargingCheckbox; - private AppCompatCheckBox mUploadExistingCheckbox; - private AppCompatCheckBox mUploadUseSubfoldersCheckbox; - private Spinner mUploadSubfolderRuleSpinner; - private TextView mUploadBehaviorSummary; - private TextView mNameCollisionPolicySummary; - private TextView mLocalFolderPath; - private TextView mLocalFolderSummary; - private TextView mRemoteFolderSummary; - private LinearLayout mUploadSubfolderRulesContainer; - - private SyncedFolderParcelable mSyncedFolder; - private MaterialButton mCancel; - private MaterialButton mSave; - private boolean behaviourDialogShown; - private boolean nameCollisionPolicyDialogShown; - private AlertDialog behaviourDialog; - private SyncedFoldersSettingsLayoutBinding binding; - - public static SyncedFolderPreferencesDialogFragment newInstance(SyncedFolderDisplayItem syncedFolder, int section) { - if (syncedFolder == null) { - throw new IllegalArgumentException("SyncedFolder is mandatory but NULL!"); - } - - Bundle args = new Bundle(); - args.putParcelable(SYNCED_FOLDER_PARCELABLE, new SyncedFolderParcelable(syncedFolder, section)); - - SyncedFolderPreferencesDialogFragment dialogFragment = new SyncedFolderPreferencesDialogFragment(); - dialogFragment.setArguments(args); - dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog); - - return dialogFragment; - } - - @Override - public void onAttach(@NonNull Activity activity) { - super.onAttach(activity); - if (!(activity instanceof OnSyncedFolderPreferenceListener)) { - throw new IllegalArgumentException("The host activity must implement " - + OnSyncedFolderPreferenceListener.class.getCanonicalName()); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // keep the state of the fragment on configuration changes - setRetainInstance(true); - - binding = null; - - mSyncedFolder = getArguments().getParcelable(SYNCED_FOLDER_PARCELABLE); - mUploadBehaviorItemStrings = getResources().getTextArray(R.array.pref_behaviour_entries); - mNameCollisionPolicyItemStrings = getResources().getTextArray(R.array.pref_name_collision_policy_entries); - } - - /** - * find all relevant UI elements and set their values. - * - * @param binding the parent binding - */ - private void setupDialogElements(SyncedFoldersSettingsLayoutBinding binding) { - if (mSyncedFolder.getType().getId() > MediaFolderType.CUSTOM.getId()) { - // hide local folder chooser and delete for non-custom folders - binding.localFolderContainer.setVisibility(View.GONE); - binding.delete.setVisibility(View.GONE); - } else if (mSyncedFolder.getId() <= UNPERSISTED_ID) { - // Hide delete/enabled for unpersisted custom folders - binding.delete.setVisibility(View.GONE); - binding.syncEnabled.setVisibility(View.GONE); - - // auto set custom folder to enabled - mSyncedFolder.setEnabled(true); - - // switch text to create headline - binding.syncedFoldersSettingsTitle.setText(R.string.autoupload_create_new_custom_folder); - - // disable save button - binding.save.setEnabled(false); - } else { - binding.localFolderContainer.setVisibility(View.GONE); - } - - // find/saves UI elements - mEnabledSwitch = binding.syncEnabled; - viewThemeUtils.androidx.colorSwitchCompat(mEnabledSwitch); - - mLocalFolderPath = binding.syncedFoldersSettingsLocalFolderPath; - - mLocalFolderSummary = binding.localFolderSummary; - mRemoteFolderSummary = binding.remoteFolderSummary; - - mUploadOnWifiCheckbox = binding.settingInstantUploadOnWifiCheckbox; - - mUploadOnChargingCheckbox = binding.settingInstantUploadOnChargingCheckbox; - - mUploadExistingCheckbox = binding.settingInstantUploadExistingCheckbox; - - mUploadUseSubfoldersCheckbox = binding.settingInstantUploadPathUseSubfoldersCheckbox; - - mUploadSubfolderRuleSpinner = binding.settingInstantUploadSubfolderRuleSpinner; - mUploadSubfolderRulesContainer = binding.settingInstantUploadSubfolderRuleContainer; - - - - viewThemeUtils.platform.themeCheckbox(mUploadOnWifiCheckbox, - mUploadOnChargingCheckbox, - mUploadExistingCheckbox, - mUploadUseSubfoldersCheckbox); - - mUploadBehaviorSummary = binding.settingInstantBehaviourSummary; - - mNameCollisionPolicySummary = binding.settingInstantNameCollisionPolicySummary; - - mCancel = binding.cancel; - mSave = binding.save; - - viewThemeUtils.platform.colorTextButtons(mCancel, mSave); - - // Set values - setEnabled(mSyncedFolder.isEnabled()); - - if (!TextUtils.isEmpty(mSyncedFolder.getLocalPath())) { - mLocalFolderPath.setText( - DisplayUtils.createTextWithSpan( - String.format( - getString(R.string.synced_folders_preferences_folder_path), - mSyncedFolder.getLocalPath()), - mSyncedFolder.getFolderName(), - new StyleSpan(Typeface.BOLD))); - mLocalFolderSummary.setText(FileStorageUtils.pathToUserFriendlyDisplay( - mSyncedFolder.getLocalPath(), - getActivity(), - getResources())); - } else { - mLocalFolderSummary.setText(R.string.choose_local_folder); - } - - if (!TextUtils.isEmpty(mSyncedFolder.getLocalPath())) { - mRemoteFolderSummary.setText(mSyncedFolder.getRemotePath()); - } else { - mRemoteFolderSummary.setText(R.string.choose_remote_folder); - } - - mUploadOnWifiCheckbox.setChecked(mSyncedFolder.isWifiOnly()); - mUploadOnChargingCheckbox.setChecked(mSyncedFolder.isChargingOnly()); - - mUploadExistingCheckbox.setChecked(mSyncedFolder.isExisting()); - mUploadUseSubfoldersCheckbox.setChecked(mSyncedFolder.isSubfolderByDate()); - - mUploadSubfolderRuleSpinner.setSelection(mSyncedFolder.getSubFolderRule().ordinal()); - if (mUploadUseSubfoldersCheckbox.isChecked()) { - mUploadSubfolderRulesContainer.setVisibility(View.VISIBLE); - } else { - mUploadSubfolderRulesContainer.setVisibility(View.GONE); - } - - mUploadBehaviorSummary.setText(mUploadBehaviorItemStrings[mSyncedFolder.getUploadActionInteger()]); - - final int nameCollisionPolicyIndex = - getSelectionIndexForNameCollisionPolicy(mSyncedFolder.getNameCollisionPolicy()); - mNameCollisionPolicySummary.setText(mNameCollisionPolicyItemStrings[nameCollisionPolicyIndex]); - } - - /** - * set correct icon/flag. - * - * @param enabled if enabled or disabled - */ - private void setEnabled(boolean enabled) { - mSyncedFolder.setEnabled(enabled); - mEnabledSwitch.setChecked(enabled); - - setupViews(binding, enabled); - } - - /** - * set (new) remote path on activity result of the folder picker activity. The result gets originally propagated to - * the underlying activity since the picker is an activity and the result can't get passed to the dialog fragment - * directly. - * - * @param path the remote path to be set - */ - public void setRemoteFolderSummary(String path) { - mSyncedFolder.setRemotePath(path); - mRemoteFolderSummary.setText(path); - checkAndUpdateSaveButtonState(); - } - - /** - * set (new) local path on activity result of the folder picker activity. The result gets originally propagated to - * the underlying activity since the picker is an activity and the result can't get passed to the dialog fragment - * directly. - * - * @param path the local path to be set - */ - public void setLocalFolderSummary(String path) { - mSyncedFolder.setLocalPath(path); - mLocalFolderSummary.setText(FileStorageUtils.pathToUserFriendlyDisplay(path, getActivity(), getResources())); - mLocalFolderPath.setText( - DisplayUtils.createTextWithSpan( - String.format( - getString(R.string.synced_folders_preferences_folder_path), - mSyncedFolder.getLocalPath()), - new File(mSyncedFolder.getLocalPath()).getName(), - new StyleSpan(Typeface.BOLD))); - checkAndUpdateSaveButtonState(); - } - - private void checkAndUpdateSaveButtonState() { - if (mSyncedFolder.getLocalPath() != null && mSyncedFolder.getRemotePath() != null) { - binding.save.setEnabled(true); - } else { - binding.save.setEnabled(false); - } - - checkWritableFolder(); - } - - private void checkWritableFolder() { - if (!mSyncedFolder.isEnabled()) { - binding.settingInstantBehaviourContainer.setEnabled(false); - binding.settingInstantBehaviourContainer.setAlpha(alphaDisabled); - return; - } - - if (mSyncedFolder.getLocalPath() != null && new File(mSyncedFolder.getLocalPath()).canWrite()) { - binding.settingInstantBehaviourContainer.setEnabled(true); - binding.settingInstantBehaviourContainer.setAlpha(alphaEnabled); - mUploadBehaviorSummary.setText(mUploadBehaviorItemStrings[mSyncedFolder.getUploadActionInteger()]); - } else { - binding.settingInstantBehaviourContainer.setEnabled(false); - binding.settingInstantBehaviourContainer.setAlpha(alphaDisabled); - - mSyncedFolder.setUploadAction( - getResources().getTextArray(R.array.pref_behaviour_entryValues)[0].toString()); - - mUploadBehaviorSummary.setText(R.string.auto_upload_file_behaviour_kept_in_folder); - } - } - - private void setupViews(SyncedFoldersSettingsLayoutBinding binding, boolean enable) { - float alpha; - if (enable) { - alpha = alphaEnabled; - } else { - alpha = alphaDisabled; - } - binding.settingInstantUploadOnWifiContainer.setEnabled(enable); - binding.settingInstantUploadOnWifiContainer.setAlpha(alpha); - - binding.settingInstantUploadOnChargingContainer.setEnabled(enable); - binding.settingInstantUploadOnChargingContainer.setAlpha(alpha); - - binding.settingInstantUploadExistingContainer.setEnabled(enable); - binding.settingInstantUploadExistingContainer.setAlpha(alpha); - - binding.settingInstantUploadPathUseSubfoldersContainer.setEnabled(enable); - binding.settingInstantUploadPathUseSubfoldersContainer.setAlpha(alpha); - - binding.remoteFolderContainer.setEnabled(enable); - binding.remoteFolderContainer.setAlpha(alpha); - - binding.localFolderContainer.setEnabled(enable); - binding.localFolderContainer.setAlpha(alpha); - - binding.settingInstantNameCollisionPolicyContainer.setEnabled(enable); - binding.settingInstantNameCollisionPolicyContainer.setAlpha(alpha); - - mUploadOnWifiCheckbox.setEnabled(enable); - mUploadOnChargingCheckbox.setEnabled(enable); - mUploadExistingCheckbox.setEnabled(enable); - mUploadUseSubfoldersCheckbox.setEnabled(enable); - - checkWritableFolder(); - } - - /** - * setup all listeners. - * - * @param binding the parent binding - */ - private void setupListeners(SyncedFoldersSettingsLayoutBinding binding) { - mSave.setOnClickListener(new OnSyncedFolderSaveClickListener()); - mCancel.setOnClickListener(new OnSyncedFolderCancelClickListener()); - binding.delete.setOnClickListener(new OnSyncedFolderDeleteClickListener()); - - binding.settingInstantUploadOnWifiContainer.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - mSyncedFolder.setWifiOnly(!mSyncedFolder.isWifiOnly()); - mUploadOnWifiCheckbox.toggle(); - } - }); - - binding.settingInstantUploadOnChargingContainer.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - mSyncedFolder.setChargingOnly(!mSyncedFolder.isChargingOnly()); - mUploadOnChargingCheckbox.toggle(); - } - }); - - binding.settingInstantUploadExistingContainer.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - mSyncedFolder.setExisting(!mSyncedFolder.isExisting()); - mUploadExistingCheckbox.toggle(); - } - }); - - binding.settingInstantUploadPathUseSubfoldersContainer.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - mSyncedFolder.setSubfolderByDate(!mSyncedFolder.isSubfolderByDate()); - mUploadUseSubfoldersCheckbox.toggle(); - // Only allow setting subfolder rule if subfolder is allowed - if (mUploadUseSubfoldersCheckbox.isChecked()) { - mUploadSubfolderRulesContainer.setVisibility(View.VISIBLE); - } else { - mUploadSubfolderRulesContainer.setVisibility(View.GONE); - } - } - }); - - binding.settingInstantUploadSubfolderRuleSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - mSyncedFolder.setSubFolderRule(SubFolderRule.values()[i]); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - mSyncedFolder.setSubFolderRule(SubFolderRule.YEAR_MONTH); - } - }); - - binding.remoteFolderContainer.setOnClickListener(v -> { - Intent action = new Intent(getActivity(), FolderPickerActivity.class); - getActivity().startActivityForResult(action, REQUEST_CODE__SELECT_REMOTE_FOLDER); - }); - - binding.localFolderContainer.setOnClickListener(v -> { - Intent action = new Intent(getActivity(), UploadFilesActivity.class); - action.putExtra(UploadFilesActivity.KEY_LOCAL_FOLDER_PICKER_MODE, true); - action.putExtra(REQUEST_CODE_KEY, REQUEST_CODE__SELECT_LOCAL_FOLDER); - getActivity().startActivityForResult(action, REQUEST_CODE__SELECT_LOCAL_FOLDER); - }); - - binding.syncEnabled.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - setEnabled(!mSyncedFolder.isEnabled()); - } - }); - - binding.settingInstantBehaviourContainer.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - showBehaviourDialog(); - } - }); - - binding.settingInstantNameCollisionPolicyContainer.setOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - showNameCollisionPolicyDialog(); - } - }); - } - - private void showBehaviourDialog() { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity()); - builder.setTitle(R.string.prefs_instant_behaviour_dialogTitle) - .setSingleChoiceItems(getResources().getTextArray(R.array.pref_behaviour_entries), - mSyncedFolder.getUploadActionInteger(), - new - DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - mSyncedFolder.setUploadAction( - getResources().getTextArray( - R.array.pref_behaviour_entryValues)[which].toString()); - mUploadBehaviorSummary.setText(SyncedFolderPreferencesDialogFragment - .this.mUploadBehaviorItemStrings[which]); - behaviourDialogShown = false; - dialog.dismiss(); - } - }) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - behaviourDialogShown = false; - } - }); - behaviourDialogShown = true; - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(getActivity(), builder); - - behaviourDialog = builder.create(); - behaviourDialog.show(); - } - - private void showNameCollisionPolicyDialog() { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity()); - - builder.setTitle(R.string.pref_instant_name_collision_policy_dialogTitle) - .setSingleChoiceItems(getResources().getTextArray(R.array.pref_name_collision_policy_entries), - getSelectionIndexForNameCollisionPolicy(mSyncedFolder.getNameCollisionPolicy()), - new OnNameCollisionDialogClickListener()) - .setOnCancelListener(dialog -> nameCollisionPolicyDialogShown = false); - - nameCollisionPolicyDialogShown = true; - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(getActivity(), builder); - - behaviourDialog = builder.create(); - behaviourDialog.show(); - } - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - Log_OC.d(TAG, "onCreateView, savedInstanceState is " + savedInstanceState); - - binding = SyncedFoldersSettingsLayoutBinding.inflate(requireActivity().getLayoutInflater(), null, false); - - setupDialogElements(binding); - setupListeners(binding); - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(binding.getRoot().getContext()); - builder.setView(binding.getRoot()); - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.getRoot().getContext(), builder); - - return builder.create(); - } - - @Override - public void onDestroyView() { - Log_OC.d(TAG, "destroy SyncedFolderPreferencesDialogFragment view"); - if (getDialog() != null && getRetainInstance()) { - getDialog().setDismissMessage(null); - } - - if (behaviourDialog != null && behaviourDialog.isShowing()) { - behaviourDialog.dismiss(); - } - - super.onDestroyView(); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(BEHAVIOUR_DIALOG_STATE, behaviourDialogShown); - outState.putBoolean(NAME_COLLISION_POLICY_DIALOG_STATE, nameCollisionPolicyDialogShown); - - super.onSaveInstanceState(outState); - } - - @Override - public void onViewStateRestored(@Nullable Bundle savedInstanceState) { - behaviourDialogShown = savedInstanceState != null && - savedInstanceState.getBoolean(BEHAVIOUR_DIALOG_STATE, false); - nameCollisionPolicyDialogShown = savedInstanceState != null && - savedInstanceState.getBoolean(NAME_COLLISION_POLICY_DIALOG_STATE, false); - - if (behaviourDialogShown) { - showBehaviourDialog(); - } - if (nameCollisionPolicyDialogShown) { - showNameCollisionPolicyDialog(); - } - - super.onViewStateRestored(savedInstanceState); - } - - public interface OnSyncedFolderPreferenceListener { - void onSaveSyncedFolderPreference(SyncedFolderParcelable syncedFolder); - - void onCancelSyncedFolderPreference(); - - void onDeleteSyncedFolderPreference(SyncedFolderParcelable syncedFolder); - } - - private class OnSyncedFolderSaveClickListener implements OnClickListener { - @Override - public void onClick(View v) { - dismiss(); - ((OnSyncedFolderPreferenceListener) getActivity()).onSaveSyncedFolderPreference(mSyncedFolder); - } - } - - private class OnSyncedFolderCancelClickListener implements OnClickListener { - @Override - public void onClick(View v) { - dismiss(); - ((OnSyncedFolderPreferenceListener) getActivity()).onCancelSyncedFolderPreference(); - } - } - - private class OnSyncedFolderDeleteClickListener implements OnClickListener { - @Override - public void onClick(View v) { - dismiss(); - ((OnSyncedFolderPreferenceListener) getActivity()).onDeleteSyncedFolderPreference(mSyncedFolder); - } - } - - private class OnNameCollisionDialogClickListener implements DialogInterface.OnClickListener { - @Override - public void onClick(DialogInterface dialog, int which) { - mSyncedFolder.setNameCollisionPolicy(getNameCollisionPolicyForSelectionIndex(which)); - - mNameCollisionPolicySummary.setText( - SyncedFolderPreferencesDialogFragment.this.mNameCollisionPolicyItemStrings[which]); - nameCollisionPolicyDialogShown = false; - dialog.dismiss(); - } - } - - /** - * Get index for name collision selection dialog. - * - * @return 0 if ASK_USER, 1 if OVERWRITE, 2 if RENAME, 3 if SKIP, Otherwise: 0 - */ - static private Integer getSelectionIndexForNameCollisionPolicy(NameCollisionPolicy nameCollisionPolicy) { - switch (nameCollisionPolicy) { - case OVERWRITE: - return 1; - case RENAME: - return 2; - case CANCEL: - return 3; - case ASK_USER: - default: - return 0; - } - } - - /** - * Get index for name collision selection dialog. Inverse of getSelectionIndexForNameCollisionPolicy. - * - * @return ASK_USER if 0, OVERWRITE if 1, RENAME if 2, SKIP if 3. Otherwise: ASK_USER - */ - static private NameCollisionPolicy getNameCollisionPolicyForSelectionIndex(int index) { - switch (index) { - case 1: - return NameCollisionPolicy.OVERWRITE; - case 2: - return NameCollisionPolicy.RENAME; - case 3: - return NameCollisionPolicy.CANCEL; - case 0: - default: - return NameCollisionPolicy.ASK_USER; - } - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt new file mode 100644 index 000000000000..01672938757d --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt @@ -0,0 +1,566 @@ +/* + * Nextcloud Android client application + * + * @author Andy Scherzinger + * Copyright (C) 2016 Andy Scherzinger + * Copyright (C) 2016 Nextcloud + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * 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 AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this program. If not, see . + */ +package com.owncloud.android.ui.dialog + +import android.app.Activity +import android.app.Dialog +import android.content.DialogInterface +import android.content.Intent +import android.graphics.Typeface +import android.os.Build +import android.os.Bundle +import android.text.TextUtils +import android.text.style.StyleSpan +import android.view.View +import android.widget.AdapterView +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.preferences.SubFolderRule +import com.owncloud.android.R +import com.owncloud.android.databinding.SyncedFoldersSettingsLayoutBinding +import com.owncloud.android.datamodel.MediaFolderType +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.FolderPickerActivity +import com.owncloud.android.ui.activity.UploadFilesActivity +import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.io.File +import javax.inject.Inject + +/** + * Dialog to show the preferences/configuration of a synced folder allowing the user to change the different + * parameters. + */ +class SyncedFolderPreferencesDialogFragment : DialogFragment(), Injectable { + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + private lateinit var uploadBehaviorItemStrings: Array + private lateinit var nameCollisionPolicyItemStrings: Array + + private var syncedFolder: SyncedFolderParcelable? = null + private var behaviourDialogShown = false + private var nameCollisionPolicyDialogShown = false + private var behaviourDialog: AlertDialog? = null + private var binding: SyncedFoldersSettingsLayoutBinding? = null + private var isNeutralButtonActive = true + + @Deprecated("Deprecated in Java") + override fun onAttach(activity: Activity) { + super.onAttach(activity) + require(activity is OnSyncedFolderPreferenceListener) { + ( + "The host activity must implement " + + OnSyncedFolderPreferenceListener::class.java.canonicalName + ) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // keep the state of the fragment on configuration changes + retainInstance = true + binding = null + + val arguments = arguments + if (arguments != null) { + syncedFolder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arguments.getParcelable(SYNCED_FOLDER_PARCELABLE, SyncedFolderParcelable::class.java) + } else { + @Suppress("DEPRECATION") + arguments.getParcelable(SYNCED_FOLDER_PARCELABLE) + } + } + + uploadBehaviorItemStrings = resources.getTextArray(R.array.pref_behaviour_entries) + nameCollisionPolicyItemStrings = resources.getTextArray(R.array.pref_name_collision_policy_entries) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + Log_OC.d(TAG, "onCreateView, savedInstanceState is $savedInstanceState") + binding = SyncedFoldersSettingsLayoutBinding.inflate(requireActivity().layoutInflater, null, false) + + setupDialogElements(binding!!) + setupListeners(binding!!) + + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setView(binding!!.getRoot()) + + viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireContext(), builder) + + return builder.create() + } + + /** + * find all relevant UI elements and set their values. + * + * @param binding the parent binding + */ + private fun setupDialogElements(binding: SyncedFoldersSettingsLayoutBinding) { + setupLayout(binding) + applyUserColor(binding) + setButtonOrder(binding) + setValuesViaSyncedFolder(binding) + } + + private fun setupLayout(binding: SyncedFoldersSettingsLayoutBinding) { + if (syncedFolder!!.type.id > MediaFolderType.CUSTOM.id) { + // hide local folder chooser and delete for non-custom folders + binding.localFolderContainer.visibility = View.GONE + isNeutralButtonActive = false + } else if (syncedFolder!!.id <= SyncedFolder.UNPERSISTED_ID) { + isNeutralButtonActive = false + + // Hide delete/enabled for unpersisted custom folders + binding.syncEnabled.visibility = View.GONE + + // auto set custom folder to enabled + syncedFolder?.isEnabled = true + + // switch text to create headline + binding.syncedFoldersSettingsTitle.setText(R.string.autoupload_create_new_custom_folder) + + // disable save button + binding.btnPositive.isEnabled = false + } else { + binding.localFolderContainer.visibility = View.GONE + } + } + + private fun applyUserColor(binding: SyncedFoldersSettingsLayoutBinding) { + viewThemeUtils?.androidx?.colorSwitchCompat(binding.syncEnabled) + + viewThemeUtils?.platform?.themeCheckbox( + binding.settingInstantUploadOnWifiCheckbox, + binding.settingInstantUploadOnChargingCheckbox, + binding.settingInstantUploadExistingCheckbox, + binding.settingInstantUploadPathUseSubfoldersCheckbox + ) + + viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(binding.btnPositive) + viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(binding.btnNegative) + viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(binding.btnNeutral) + } + + private fun setButtonOrder(binding: SyncedFoldersSettingsLayoutBinding) { + // btnNeutral btnNegative btnPositive + if (isNeutralButtonActive) { + // Cancel Delete Save + binding.btnNeutral.setText(R.string.common_cancel) + binding.btnNegative.setText(R.string.common_delete) + } else { + // Cancel Save + binding.btnNeutral.visibility = View.GONE + binding.btnNegative.setText(R.string.common_cancel) + } + } + + private fun setValuesViaSyncedFolder(binding: SyncedFoldersSettingsLayoutBinding) { + syncedFolder?.let { + setEnabled(it.isEnabled) + + if (!TextUtils.isEmpty(it.localPath)) { + binding.syncedFoldersSettingsLocalFolderPath.text = DisplayUtils.createTextWithSpan( + String.format( + getString(R.string.synced_folders_preferences_folder_path), + it.localPath + ), + it.folderName, + StyleSpan(Typeface.BOLD) + ) + binding.localFolderSummary.text = FileStorageUtils.pathToUserFriendlyDisplay( + it.localPath, + activity, + resources + ) + } else { + binding.localFolderSummary.setText(R.string.choose_local_folder) + } + + if (!TextUtils.isEmpty(it.localPath)) { + binding.remoteFolderSummary.text = it.remotePath + } else { + binding.remoteFolderSummary.setText(R.string.choose_remote_folder) + } + + binding.settingInstantUploadOnWifiCheckbox.isChecked = it.isWifiOnly + binding.settingInstantUploadOnChargingCheckbox.isChecked = it.isChargingOnly + binding.settingInstantUploadExistingCheckbox.isChecked = it.isExisting + binding.settingInstantUploadPathUseSubfoldersCheckbox.isChecked = it.isSubfolderByDate + + binding.settingInstantUploadSubfolderRuleSpinner.setSelection(it.subFolderRule.ordinal) + + binding.settingInstantUploadSubfolderRuleContainer.visibility = + if (binding.settingInstantUploadPathUseSubfoldersCheckbox.isChecked) View.VISIBLE else View.GONE + + binding.settingInstantBehaviourSummary.text = uploadBehaviorItemStrings[it.uploadActionInteger] + val nameCollisionPolicyIndex = getSelectionIndexForNameCollisionPolicy( + it.nameCollisionPolicy + ) + binding.settingInstantNameCollisionPolicySummary.text = + nameCollisionPolicyItemStrings[nameCollisionPolicyIndex] + } + } + + /** + * set correct icon/flag. + * + * @param enabled if enabled or disabled + */ + private fun setEnabled(enabled: Boolean) { + syncedFolder?.isEnabled = enabled + binding?.syncEnabled?.isChecked = enabled + setupViews(binding, enabled) + } + + /** + * set (new) remote path on activity result of the folder picker activity. The result gets originally propagated to + * the underlying activity since the picker is an activity and the result can't get passed to the dialog fragment + * directly. + * + * @param path the remote path to be set + */ + fun setRemoteFolderSummary(path: String?) { + syncedFolder?.remotePath = path + binding?.remoteFolderSummary?.text = path + checkAndUpdateSaveButtonState() + } + + /** + * set (new) local path on activity result of the folder picker activity. The result gets originally propagated to + * the underlying activity since the picker is an activity and the result can't get passed to the dialog fragment + * directly. + * + * @param path the local path to be set + */ + fun setLocalFolderSummary(path: String?) { + syncedFolder?.localPath = path + binding?.localFolderSummary?.text = FileStorageUtils.pathToUserFriendlyDisplay(path, activity, resources) + binding?.syncedFoldersSettingsLocalFolderPath?.text = DisplayUtils.createTextWithSpan( + String.format( + getString(R.string.synced_folders_preferences_folder_path), + syncedFolder!!.localPath + ), + File(syncedFolder!!.localPath).name, + StyleSpan(Typeface.BOLD) + ) + checkAndUpdateSaveButtonState() + } + + private fun checkAndUpdateSaveButtonState() { + binding?.btnPositive?.isEnabled = syncedFolder!!.localPath != null && syncedFolder!!.remotePath != null + checkWritableFolder() + } + + private fun checkWritableFolder() { + if (!syncedFolder!!.isEnabled) { + binding?.settingInstantBehaviourContainer?.isEnabled = false + binding?.settingInstantBehaviourContainer?.alpha = alphaDisabled + return + } + if (syncedFolder!!.localPath != null && File(syncedFolder!!.localPath).canWrite()) { + binding?.settingInstantBehaviourContainer?.isEnabled = true + binding?.settingInstantBehaviourContainer?.alpha = alphaEnabled + binding?.settingInstantBehaviourSummary?.text = + uploadBehaviorItemStrings[syncedFolder!!.uploadActionInteger] + } else { + binding?.settingInstantBehaviourContainer?.isEnabled = false + binding?.settingInstantBehaviourContainer?.alpha = alphaDisabled + syncedFolder?.setUploadAction( + resources.getTextArray(R.array.pref_behaviour_entryValues)[0].toString() + ) + binding?.settingInstantBehaviourSummary?.setText(R.string.auto_upload_file_behaviour_kept_in_folder) + } + } + + private fun setupViews(optionalBinding: SyncedFoldersSettingsLayoutBinding?, enable: Boolean) { + val alpha: Float = if (enable) { + alphaEnabled + } else { + alphaDisabled + } + + optionalBinding?.let { binding -> + binding.settingInstantUploadOnWifiContainer.isEnabled = enable + binding.settingInstantUploadOnWifiContainer.alpha = alpha + binding.settingInstantUploadOnChargingContainer.isEnabled = enable + binding.settingInstantUploadOnChargingContainer.alpha = alpha + binding.settingInstantUploadExistingContainer.isEnabled = enable + binding.settingInstantUploadExistingContainer.alpha = alpha + binding.settingInstantUploadPathUseSubfoldersContainer.isEnabled = enable + binding.settingInstantUploadPathUseSubfoldersContainer.alpha = alpha + binding.remoteFolderContainer.isEnabled = enable + binding.remoteFolderContainer.alpha = alpha + binding.localFolderContainer.isEnabled = enable + binding.localFolderContainer.alpha = alpha + binding.settingInstantNameCollisionPolicyContainer.isEnabled = enable + binding.settingInstantNameCollisionPolicyContainer.alpha = alpha + binding.settingInstantUploadOnWifiCheckbox.isEnabled = enable + binding.settingInstantUploadOnChargingCheckbox.isEnabled = enable + binding.settingInstantUploadExistingCheckbox.isEnabled = enable + binding.settingInstantUploadPathUseSubfoldersCheckbox.isEnabled = enable + } + + checkWritableFolder() + } + + /** + * setup all listeners. + * + * @param binding the parent binding + */ + private fun setupListeners(binding: SyncedFoldersSettingsLayoutBinding) { + binding.btnPositive.setOnClickListener(OnSyncedFolderSaveClickListener()) + if (isNeutralButtonActive) { + binding.btnNeutral.setOnClickListener(OnSyncedFolderCancelClickListener()) + binding.btnNegative.setOnClickListener(OnSyncedFolderDeleteClickListener()) + } else { + binding.btnNegative.setOnClickListener(OnSyncedFolderCancelClickListener()) + } + + syncedFolder?.let { syncedFolder -> + binding.settingInstantUploadOnWifiContainer.setOnClickListener { + syncedFolder.isWifiOnly = !syncedFolder.isWifiOnly + binding.settingInstantUploadOnWifiCheckbox.toggle() + } + binding.settingInstantUploadOnChargingContainer.setOnClickListener { + syncedFolder.isChargingOnly = !syncedFolder.isChargingOnly + binding.settingInstantUploadOnChargingCheckbox.toggle() + } + binding.settingInstantUploadExistingContainer.setOnClickListener { + syncedFolder.isExisting = !syncedFolder.isExisting + binding.settingInstantUploadExistingCheckbox.toggle() + } + binding.settingInstantUploadPathUseSubfoldersContainer.setOnClickListener { + syncedFolder.isSubfolderByDate = !syncedFolder.isSubfolderByDate + binding.settingInstantUploadPathUseSubfoldersCheckbox.toggle() + + // Only allow setting subfolder rule if subfolder is allowed + if (binding.settingInstantUploadPathUseSubfoldersCheckbox.isChecked) { + binding.settingInstantUploadSubfolderRuleContainer.visibility = View.VISIBLE + } else { + binding.settingInstantUploadSubfolderRuleContainer.visibility = View.GONE + } + } + binding.settingInstantUploadSubfolderRuleSpinner.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) { + syncedFolder.subFolderRule = SubFolderRule.values()[i] + } + + override fun onNothingSelected(adapterView: AdapterView<*>?) { + syncedFolder.subFolderRule = SubFolderRule.YEAR_MONTH + } + } + + binding.syncEnabled.setOnClickListener { setEnabled(!syncedFolder.isEnabled) } + } + + binding.remoteFolderContainer.setOnClickListener { + val action = Intent(activity, FolderPickerActivity::class.java) + requireActivity().startActivityForResult(action, REQUEST_CODE__SELECT_REMOTE_FOLDER) + } + binding.localFolderContainer.setOnClickListener { + val action = Intent(activity, UploadFilesActivity::class.java) + action.putExtra(UploadFilesActivity.KEY_LOCAL_FOLDER_PICKER_MODE, true) + action.putExtra(UploadFilesActivity.REQUEST_CODE_KEY, REQUEST_CODE__SELECT_LOCAL_FOLDER) + requireActivity().startActivityForResult(action, REQUEST_CODE__SELECT_LOCAL_FOLDER) + } + + binding.settingInstantBehaviourContainer.setOnClickListener { showBehaviourDialog() } + binding.settingInstantNameCollisionPolicyContainer.setOnClickListener { showNameCollisionPolicyDialog() } + } + + private fun showBehaviourDialog() { + val builder = MaterialAlertDialogBuilder(requireActivity()) + + syncedFolder?.let { + val behaviourEntries = resources.getTextArray(R.array.pref_behaviour_entries) + val behaviourEntryValues = resources.getTextArray(R.array.pref_behaviour_entryValues) + builder.setTitle(R.string.prefs_instant_behaviour_dialogTitle) + .setSingleChoiceItems(behaviourEntries, it.uploadActionInteger) { dialog: DialogInterface, which: Int -> + it.setUploadAction(behaviourEntryValues[which].toString()) + binding?.settingInstantBehaviourSummary?.text = uploadBehaviorItemStrings[which] + behaviourDialogShown = false + dialog.dismiss() + } + .setOnCancelListener { behaviourDialogShown = false } + } + + behaviourDialogShown = true + viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireActivity(), builder) + + behaviourDialog = builder.create() + behaviourDialog?.show() + } + + private fun showNameCollisionPolicyDialog() { + syncedFolder?.let { + val builder = MaterialAlertDialogBuilder(requireActivity()) + builder.setTitle(R.string.pref_instant_name_collision_policy_dialogTitle) + .setSingleChoiceItems( + resources.getTextArray(R.array.pref_name_collision_policy_entries), + getSelectionIndexForNameCollisionPolicy(it.nameCollisionPolicy), + OnNameCollisionDialogClickListener() + ) + .setOnCancelListener { nameCollisionPolicyDialogShown = false } + + nameCollisionPolicyDialogShown = true + + viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireActivity(), builder) + behaviourDialog = builder.create() + behaviourDialog?.show() + } + } + + override fun onDestroyView() { + Log_OC.d(TAG, "destroy SyncedFolderPreferencesDialogFragment view") + if (dialog != null && retainInstance) { + dialog?.setDismissMessage(null) + } + if (behaviourDialog != null && behaviourDialog!!.isShowing) { + behaviourDialog?.dismiss() + } + + super.onDestroyView() + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(BEHAVIOUR_DIALOG_STATE, behaviourDialogShown) + outState.putBoolean(NAME_COLLISION_POLICY_DIALOG_STATE, nameCollisionPolicyDialogShown) + super.onSaveInstanceState(outState) + } + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + behaviourDialogShown = savedInstanceState != null && + savedInstanceState.getBoolean(BEHAVIOUR_DIALOG_STATE, false) + nameCollisionPolicyDialogShown = savedInstanceState != null && + savedInstanceState.getBoolean(NAME_COLLISION_POLICY_DIALOG_STATE, false) + if (behaviourDialogShown) { + showBehaviourDialog() + } + if (nameCollisionPolicyDialogShown) { + showNameCollisionPolicyDialog() + } + + super.onViewStateRestored(savedInstanceState) + } + + interface OnSyncedFolderPreferenceListener { + fun onSaveSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) + fun onCancelSyncedFolderPreference() + fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) + } + + private inner class OnSyncedFolderSaveClickListener : View.OnClickListener { + override fun onClick(v: View) { + dismiss() + (activity as OnSyncedFolderPreferenceListener?)?.onSaveSyncedFolderPreference(syncedFolder) + } + } + + private inner class OnSyncedFolderCancelClickListener : View.OnClickListener { + override fun onClick(v: View) { + dismiss() + (activity as OnSyncedFolderPreferenceListener?)?.onCancelSyncedFolderPreference() + } + } + + private inner class OnSyncedFolderDeleteClickListener : View.OnClickListener { + override fun onClick(v: View) { + dismiss() + (activity as OnSyncedFolderPreferenceListener?)?.onDeleteSyncedFolderPreference(syncedFolder) + } + } + + private inner class OnNameCollisionDialogClickListener : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface, which: Int) { + syncedFolder!!.nameCollisionPolicy = + getNameCollisionPolicyForSelectionIndex(which) + binding?.settingInstantNameCollisionPolicySummary?.text = + nameCollisionPolicyItemStrings[which] + nameCollisionPolicyDialogShown = false + dialog.dismiss() + } + } + + companion object { + const val SYNCED_FOLDER_PARCELABLE = "SyncedFolderParcelable" + const val REQUEST_CODE__SELECT_REMOTE_FOLDER = 0 + const val REQUEST_CODE__SELECT_LOCAL_FOLDER = 1 + private val TAG = SyncedFolderPreferencesDialogFragment::class.java.simpleName + private const val BEHAVIOUR_DIALOG_STATE = "BEHAVIOUR_DIALOG_STATE" + private const val NAME_COLLISION_POLICY_DIALOG_STATE = "NAME_COLLISION_POLICY_DIALOG_STATE" + private const val alphaEnabled = 1.0f + private const val alphaDisabled = 0.7f + + @JvmStatic + fun newInstance(syncedFolder: SyncedFolderDisplayItem?, section: Int): SyncedFolderPreferencesDialogFragment { + requireNotNull(syncedFolder) { "SyncedFolder is mandatory but NULL!" } + val args = Bundle() + args.putParcelable(SYNCED_FOLDER_PARCELABLE, SyncedFolderParcelable(syncedFolder, section)) + val dialogFragment = SyncedFolderPreferencesDialogFragment() + dialogFragment.arguments = args + dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog) + return dialogFragment + } + + /** + * Get index for name collision selection dialog. + * + * @return 0 if ASK_USER, 1 if OVERWRITE, 2 if RENAME, 3 if SKIP, Otherwise: 0 + */ + @Suppress("MagicNumber") + private fun getSelectionIndexForNameCollisionPolicy(nameCollisionPolicy: NameCollisionPolicy): Int { + return when (nameCollisionPolicy) { + NameCollisionPolicy.OVERWRITE -> 1 + NameCollisionPolicy.RENAME -> 2 + NameCollisionPolicy.CANCEL -> 3 + NameCollisionPolicy.ASK_USER -> 0 + } + } + + /** + * Get index for name collision selection dialog. Inverse of getSelectionIndexForNameCollisionPolicy. + * + * @return ASK_USER if 0, OVERWRITE if 1, RENAME if 2, SKIP if 3. Otherwise: ASK_USER + */ + @Suppress("MagicNumber") + private fun getNameCollisionPolicyForSelectionIndex(index: Int): NameCollisionPolicy { + return when (index) { + 1 -> NameCollisionPolicy.OVERWRITE + 2 -> NameCollisionPolicy.RENAME + 3 -> NameCollisionPolicy.CANCEL + 0 -> NameCollisionPolicy.ASK_USER + else -> NameCollisionPolicy.ASK_USER + } + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 76b5393ecf4c..97f777d4c492 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -278,8 +278,7 @@ private void onOverflowIconClicked() { R.id.action_favorite, R.id.action_unset_favorite, R.id.action_see_details, - R.id.action_move, - R.id.action_copy, + R.id.action_move_or_copy, R.id.action_stream_media, R.id.action_send_share_file, R.id.action_pin_to_homescreen @@ -694,7 +693,7 @@ public void listenForTransferProgress() { if (progressListener != null) { if (containerActivity.getFileDownloaderBinder() != null) { containerActivity.getFileDownloaderBinder(). - addDatatransferProgressListener(progressListener, getFile()); + addDataTransferProgressListener(progressListener, getFile()); } if (containerActivity.getFileUploaderBinder() != null) { containerActivity.getFileUploaderBinder(). @@ -709,7 +708,7 @@ private void leaveTransferProgress() { if (progressListener != null) { if (containerActivity.getFileDownloaderBinder() != null) { containerActivity.getFileDownloaderBinder(). - removeDatatransferProgressListener(progressListener, getFile()); + removeDataTransferProgressListener(progressListener, getFile()); } if (containerActivity.getFileUploaderBinder() != null) { containerActivity.getFileUploaderBinder(). diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index e7745f3ba368..26f2ac3ecdce 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -1235,11 +1235,8 @@ public boolean onFileActionChosen(@IdRes final int itemId, Set checkedFi } else if (itemId == R.id.action_unset_favorite) { mContainerActivity.getFileOperationsHelper().toggleFavoriteFiles(checkedFiles, false); return true; - } else if (itemId == R.id.action_move) { - pickFolderForMoveOrCopy(FolderPickerActivity.MOVE, checkedFiles); - return true; - } else if (itemId == R.id.action_copy) { - pickFolderForMoveOrCopy(FolderPickerActivity.COPY, checkedFiles); + } else if (itemId == R.id.action_move_or_copy) { + pickFolderForMoveOrCopy(checkedFiles); return true; } else if (itemId == R.id.action_select_all_action_menu) { selectAllFiles(true); @@ -1257,18 +1254,9 @@ public boolean onFileActionChosen(@IdRes final int itemId, Set checkedFi return false; } - private void pickFolderForMoveOrCopy(final String extraAction, final Set checkedFiles) { - int requestCode; - switch (extraAction) { - case FolderPickerActivity.MOVE: - requestCode = FileDisplayActivity.REQUEST_CODE__MOVE_FILES; - break; - case FolderPickerActivity.COPY: - requestCode = FileDisplayActivity.REQUEST_CODE__COPY_FILES; - break; - default: - throw new IllegalArgumentException("Unknown extra action: " + extraAction); - } + private void pickFolderForMoveOrCopy(final Set checkedFiles) { + int requestCode = FileDisplayActivity.REQUEST_CODE__MOVE_OR_COPY_FILES; + String extraAction = FolderPickerActivity.MOVE_OR_COPY; final Intent action = new Intent(getActivity(), FolderPickerActivity.class); final ArrayList paths = new ArrayList<>(checkedFiles.size()); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt index 9af639d82e63..d14e37204f08 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt @@ -30,7 +30,6 @@ import android.view.ViewGroup import android.widget.ImageView import androidx.annotation.VisibleForTesting import androidx.appcompat.widget.SearchView -import androidx.core.view.MenuItemCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider @@ -89,7 +88,7 @@ class UnifiedSearchFragment : Fragment(), Injectable, UnifiedSearchListInterface override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - vm = ViewModelProvider(this, vmFactory).get(UnifiedSearchViewModel::class.java) + vm = ViewModelProvider(this, vmFactory)[UnifiedSearchViewModel::class.java] setUpViewModel() val query = savedInstanceState?.getString(ARG_QUERY) ?: arguments?.getString(ARG_QUERY) @@ -125,7 +124,7 @@ class UnifiedSearchFragment : Fragment(), Injectable, UnifiedSearchListInterface binding.emptyList.emptyListViewText.text = requireContext().getString(R.string.file_list_empty_unified_search_no_results) binding.emptyList.emptyListIcon.setImageDrawable( - viewThemeUtils.platform.tintPrimaryDrawable(requireContext(), R.drawable.ic_search_grey) + viewThemeUtils.platform.tintDrawable(requireContext(), R.drawable.ic_search_grey) ) } } @@ -151,10 +150,12 @@ class UnifiedSearchFragment : Fragment(), Injectable, UnifiedSearchListInterface } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + @Suppress("DEPRECATION") + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = ListFragmentBinding.inflate(inflater, container, false) binding.listRoot.updatePadding(top = resources.getDimension(R.dimen.standard_half_padding).toInt()) setUpBinding() + setHasOptionsMenu(true) return binding.root } @@ -223,8 +224,14 @@ class UnifiedSearchFragment : Fragment(), Injectable, UnifiedSearchListInterface @Deprecated("Deprecated in Java") override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { val item = menu.findItem(R.id.action_search) - searchView = MenuItemCompat.getActionView(item) as SearchView + searchView = item.actionView as SearchView? + + // Required to align with TextView width. + // Because this fragment is opened with TextView onClick on the previous screen + searchView?.maxWidth = Integer.MAX_VALUE + viewThemeUtils.androidx.themeToolbarSearchView(searchView!!) + searchView?.setQuery(vm.query.value, false) searchView?.setOnQueryTextListener(this) searchView?.isIconified = false diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index f7465a27d291..4fa13c39fdc5 100755 --- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -1007,27 +1007,7 @@ public void cancelTransference(OCFile file) { } } - /** - * Start operations to move one or several files - * - * @param filePaths Remote paths of files to move - * @param targetFolder Folder where the files while be moved into - */ - public void moveFiles(final List filePaths, final OCFile targetFolder) { - copyOrMoveFiles(OperationsService.ACTION_MOVE_FILE, filePaths, targetFolder); - } - - /** - * Start operations to copy one or several files - * - * @param filePaths Remote paths of files to move - * @param targetFolder Folder where the files while be copied into - */ - public void copyFiles(final List filePaths, final OCFile targetFolder) { - copyOrMoveFiles(OperationsService.ACTION_COPY_FILE, filePaths, targetFolder); - } - - private void copyOrMoveFiles(final String action, final List filePaths, final OCFile targetFolder) { + public void moveOrCopyFiles(String action, final List filePaths, final OCFile targetFolder) { for (String path : filePaths) { Intent service = new Intent(fileActivity, OperationsService.class); service.setAction(action); @@ -1039,7 +1019,6 @@ private void copyOrMoveFiles(final String action, final List filePaths, fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); } - public void exportFiles(Collection files, Context context, View view, diff --git a/app/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.java index ffca7c685a83..62a098e37d6e 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.java @@ -261,7 +261,7 @@ private void setButtonsForRemote() { public void listenForTransferProgress() { if (mProgressListener != null && !mListening && containerActivity.getFileDownloaderBinder() != null) { - containerActivity.getFileDownloaderBinder().addDatatransferProgressListener(mProgressListener, getFile()); + containerActivity.getFileDownloaderBinder().addDataTransferProgressListener(mProgressListener, getFile()); mListening = true; setButtonsForTransferring(); } @@ -271,7 +271,7 @@ public void listenForTransferProgress() { public void leaveTransferProgress() { if (mProgressListener != null && containerActivity.getFileDownloaderBinder() != null) { containerActivity.getFileDownloaderBinder() - .removeDatatransferProgressListener(mProgressListener, getFile()); + .removeDataTransferProgressListener(mProgressListener, getFile()); mListening = false; } } diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.java index 09e03ab9b62f..b0ec4ee1b400 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.java @@ -375,8 +375,7 @@ private void showFileActions(OCFile file) { Arrays.asList( R.id.action_rename_file, R.id.action_sync_file, - R.id.action_move, - R.id.action_copy, + R.id.action_move_or_copy, R.id.action_favorite, R.id.action_unset_favorite, R.id.action_pin_to_homescreen diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java index 18ee93690fe2..20d82add5a4b 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java @@ -424,8 +424,7 @@ private void showFileActions(OCFile file) { Arrays.asList( R.id.action_rename_file, R.id.action_sync_file, - R.id.action_move, - R.id.action_copy, + R.id.action_move_or_copy, R.id.action_favorite, R.id.action_unset_favorite, R.id.action_pin_to_homescreen diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java index e34031a51a71..16648c631c71 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java @@ -300,8 +300,7 @@ private void showFileActions(OCFile file) { Arrays.asList( R.id.action_rename_file, R.id.action_sync_file, - R.id.action_move, - R.id.action_copy, + R.id.action_move_or_copy, R.id.action_favorite, R.id.action_unset_favorite, R.id.action_pin_to_homescreen diff --git a/app/src/main/java/com/owncloud/android/utils/WebViewUtil.kt b/app/src/main/java/com/owncloud/android/utils/WebViewUtil.kt new file mode 100644 index 000000000000..93dde6c9d3db --- /dev/null +++ b/app/src/main/java/com/owncloud/android/utils/WebViewUtil.kt @@ -0,0 +1,110 @@ +/* + * Nextcloud Android client application + * + * @author Alper Ozturk + * Copyright (C) 2023 Alper Ozturk + * Copyright (C) 2023 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.utils + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.owncloud.android.R + +class WebViewUtil(private val context: Context) { + + private val packageName = "com.google.android.webview" + + fun checkWebViewVersion() { + if (!isWebViewVersionValid()) { + showUpdateDialog() + } + } + + private fun isWebViewVersionValid(): Boolean { + val currentWebViewVersion = getCurrentWebViewMajorVersion() ?: return true + val minSupportedWebViewVersion: String = getMinimumSupportedMajorWebViewVersion() + return currentWebViewVersion.toInt() >= minSupportedWebViewVersion.toInt() + } + + private fun showUpdateDialog() { + val builder = MaterialAlertDialogBuilder(context) + .setTitle(context.getString(R.string.webview_version_check_alert_dialog_title)) + .setMessage(context.getString(R.string.webview_version_check_alert_dialog_message)) + .setCancelable(false) + .setPositiveButton( + context.getString(R.string.webview_version_check_alert_dialog_positive_button_title) + ) { _, _ -> + redirectToAndroidSystemWebViewStorePage() + } + + val dialog = builder.create() + dialog.show() + } + + private fun redirectToAndroidSystemWebViewStorePage() { + val uri = Uri.parse("market://details?id=$packageName") + val intent = Intent(Intent.ACTION_VIEW, uri) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + + try { + context.startActivity(intent) + } catch (e: android.content.ActivityNotFoundException) { + redirectToPlayStoreWebsiteForAndroidSystemWebView() + } + } + + private fun redirectToPlayStoreWebsiteForAndroidSystemWebView() { + val playStoreWebUri = Uri.parse("https://play.google.com/store/apps/details?id=$packageName") + val webIntent = Intent(Intent.ACTION_VIEW, playStoreWebUri) + context.startActivity(webIntent) + } + + private fun getCurrentWebViewMajorVersion(): String? { + val pm: PackageManager = context.packageManager + + return try { + val pi = pm.getPackageInfo("com.google.android.webview", 0) + val fullVersion = pi.versionName + + // Split the version string by "." and get the first part + val versionParts = fullVersion.split("\\.".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + + if (versionParts.isNotEmpty()) { + versionParts[0] + } else { + null + } + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + /** + * Ideally we should fetch from database, reading actual value + * from PlayStore not feasible due to frequently api changes made by + * Google + * + */ + private fun getMinimumSupportedMajorWebViewVersion(): String { + return "118" + } +} diff --git a/app/src/main/res/drawable/ic_move.xml b/app/src/main/res/drawable/ic_move.xml deleted file mode 100644 index bea61c0c3fe6..000000000000 --- a/app/src/main/res/drawable/ic_move.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/layout/files_folder_picker.xml b/app/src/main/res/layout/files_folder_picker.xml index a9b85539fad4..c4d77b680856 100644 --- a/app/src/main/res/layout/files_folder_picker.xml +++ b/app/src/main/res/layout/files_folder_picker.xml @@ -46,27 +46,39 @@ + + + + diff --git a/app/src/main/res/layout/loading_dialog.xml b/app/src/main/res/layout/loading_dialog.xml index 9625b7954e35..cdef05385fcd 100644 --- a/app/src/main/res/layout/loading_dialog.xml +++ b/app/src/main/res/layout/loading_dialog.xml @@ -16,7 +16,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . --> - - . --> - + android:layout_height="wrap_content"> - - - + android:orientation="vertical"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" + android:gravity="center|start" + android:layout_marginBottom="@dimen/standard_margin" + android:text="@string/sort_by"/> - + - + - + - + - + - + - + diff --git a/app/src/main/res/layout/storage_path_item.xml b/app/src/main/res/layout/storage_path_item.xml index dfcbf39ae9e7..5a7cfeba0b28 100644 --- a/app/src/main/res/layout/storage_path_item.xml +++ b/app/src/main/res/layout/storage_path_item.xml @@ -1,5 +1,4 @@ - - - - - - - - + android:text="@string/menu_item_sort_by_name_z_a" + app:icon="@drawable/ic_user" + app:iconPadding="@dimen/standard_padding" + tools:text="DCIM" /> diff --git a/app/src/main/res/layout/synced_folders_settings_layout.xml b/app/src/main/res/layout/synced_folders_settings_layout.xml index 153684a0e39e..14dc2f0d9687 100644 --- a/app/src/main/res/layout/synced_folders_settings_layout.xml +++ b/app/src/main/res/layout/synced_folders_settings_layout.xml @@ -61,9 +61,9 @@ android:layout_width="@dimen/synced_folders_control_width" android:layout_height="match_parent" android:gravity="center" - android:padding="@dimen/standard_padding"> + android:padding="@dimen/standard_half_padding"> - - + android:text="@string/common_cancel" + android:layout_weight="1"/> - + + - - + android:text="@string/common_delete" + android:layout_weight="1"/> - - - + + diff --git a/app/src/main/res/values-es-rCR/strings.xml b/app/src/main/res/values-es-rCR/strings.xml index 6881255cf44f..9652d8bf0e11 100644 --- a/app/src/main/res/values-es-rCR/strings.xml +++ b/app/src/main/res/values-es-rCR/strings.xml @@ -240,6 +240,7 @@ Crear No hay carpetas aquí Seleccionar + Mover No se te permite %s para copiar este archivo para crear este archivo diff --git a/app/src/main/res/values-es-rDO/strings.xml b/app/src/main/res/values-es-rDO/strings.xml index ca2e385c934e..e8ae500158c7 100644 --- a/app/src/main/res/values-es-rDO/strings.xml +++ b/app/src/main/res/values-es-rDO/strings.xml @@ -255,6 +255,7 @@ Crear No hay carpetas aquí Seleccionar + Mover No se te permite %s para copiar este archivo para crear este archivo diff --git a/app/src/main/res/values-es-rEC/strings.xml b/app/src/main/res/values-es-rEC/strings.xml index 76819a2725fa..a78a292b441d 100644 --- a/app/src/main/res/values-es-rEC/strings.xml +++ b/app/src/main/res/values-es-rEC/strings.xml @@ -387,6 +387,8 @@ Crear No hay carpetas aquí Seleccionar + Selecciona la carpeta de destino. + Mover No se te permite %s para copiar este archivo para crear este archivo diff --git a/app/src/main/res/values-es-rGT/strings.xml b/app/src/main/res/values-es-rGT/strings.xml index c335c3b518ca..2a4400a0c654 100644 --- a/app/src/main/res/values-es-rGT/strings.xml +++ b/app/src/main/res/values-es-rGT/strings.xml @@ -240,6 +240,7 @@ Crear No hay carpetas aquí Seleccionar + Mover No se te permite %s para copiar este archivo para crear este archivo diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml index f94a9d7fbb94..c9969fa3638c 100644 --- a/app/src/main/res/values-es-rMX/strings.xml +++ b/app/src/main/res/values-es-rMX/strings.xml @@ -329,6 +329,8 @@ Crear No hay carpetas aquí Seleccionar + Elegir carpeta destino + Mover No se te permite %s para copiar este archivo para crear este archivo diff --git a/app/src/main/res/values-es-rSV/strings.xml b/app/src/main/res/values-es-rSV/strings.xml index 6881255cf44f..9652d8bf0e11 100644 --- a/app/src/main/res/values-es-rSV/strings.xml +++ b/app/src/main/res/values-es-rSV/strings.xml @@ -240,6 +240,7 @@ Crear No hay carpetas aquí Seleccionar + Mover No se te permite %s para copiar este archivo para crear este archivo diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ed1bb858abc6..449c6147c232 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -253,7 +253,7 @@ No es posible sin conexión a internet Más Notas - Coloquio + Talk Más apps de Nextcloud Nextcloud Notas Nextcloud Talk diff --git a/app/src/main/res/values-et-rEE/strings.xml b/app/src/main/res/values-et-rEE/strings.xml index 6dea94f883c5..845e7deb5e81 100644 --- a/app/src/main/res/values-et-rEE/strings.xml +++ b/app/src/main/res/values-et-rEE/strings.xml @@ -263,6 +263,8 @@ Loo Siin ei ole kaustu Vali + Vali sihtkaust + Liiguta Sul ei ole %s õigusi et kopeerida seda faili selle faili loomiseks @@ -436,6 +438,7 @@ Staatuse teade Vaikeväärtus Allalaadimised + Aasta \"%1$s\" on sinuga jagatud %1$s jagas sinuga \"%2$s\" Leite konflikte diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index bf0a2aa802ac..d88f97b2deb7 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -392,6 +392,8 @@ Sortu Ez dago karpetarik hemen Aukeratu + Aukeratu helburuko karpeta + Mugitu Ez daukazu baimenik %s fitxategi hau kopiatzeko fitxategi hau sortzeko diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 6e739ade6904..bf89788c8ad4 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -392,6 +392,8 @@ ایجاد کردن هیچ پوشه ای اینجا وجود ندارد انتخاب کردن + پوشهٔ هدف را انتخاب کنید + انتقال شما مجاز نیستید%s کپی این فایل برای ساختن این پرونده diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index b7e81f464eef..0352263ba128 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -380,6 +380,8 @@ Luo Ei kansioita täällä Valitse + Valitse kohdekansio + Siirrä Sinulla ei ole oikeutta %s kopioida tämä tiedosto luoda tätä tiedostoa diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index aff2b3fb4a29..066438b9f495 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -394,6 +394,9 @@ Attention, la suppression est irréversible. Créer Aucun dossier Valider + Sélectionner le dossier cible + Copier + Déplacer Vous n\'avez pas la permission %s de copier ce fichier de créer ce fichier diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index e12eebe7f3d6..e5feb7e13238 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -336,6 +336,8 @@ Cruthaich Chan eil pasgan an-seo Tagh + Dèan lethbhreac + Gluais Chan fhad thu %s lethbhreac dhen fhaidhle seo a dhèanamh am faidhle seo a chruthachadh diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 15ddb5798876..83a632ebc77c 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -392,6 +392,9 @@ Crear Aquí non hai cartafoles Escoller + Escoller o cartafol de destino + Copiar + Mover Non ten permiso %s copiar este ficheiro para crear este ficheiro diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 6be9e609e82a..36cb7c17eb4f 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -369,6 +369,9 @@ Stvori Ovdje nema mapa Odaberite + Odaberi ciljnu mapu + Kopirajte + Premjesti Nemate dopuštenje %s za kopiranje ove datoteke za izradu ove datoteke @@ -877,6 +880,7 @@ Pričekajte… U tijeku je provjera spremljenih vjerodajnica Kopiranje datoteke iz osobnog podatkovnog prostora + Ažuriraj Što je nova slika Preskoči Novo u %1$s diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index 7f89e32a81fd..2e77e1d75e17 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -392,6 +392,9 @@ Létrehozás Itt nincsenek mappák Válasszon + Válasszon célmappát + Másolás + Áthelyezés Nem engedélyezett %s a fájl másolása fájl létrehozása @@ -959,6 +962,7 @@ A Nextcloud itt érhető el: https://nextcloud.com Egy pillanat… Tárolt hitelesítő adatok ellenőrzése Fájl másolása a privát tárolóról + Frissítés Újdonságok kép Kihagyás Új itt: %1$s diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index d10a78189142..ce04c1f25c75 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -329,6 +329,8 @@ Otomatis unggah hanya bekerja dengan baik apabila Anda mengeluarkan aplikasi ini Buat Tidak ada folder Pilih + Pilih folder target + Pindah Anda tidak memiliki izin %s untuk menyalin berkas ini untuk membuat berkas ini @@ -668,6 +670,7 @@ Otomatis unggah hanya bekerja dengan baik apabila Anda mengeluarkan aplikasi ini Tunggu sebentar… Mengecek kredensial yang tersimpan Menyalin berkas dari penyimpanan pribadi + Perbarui Lewat Apa yang baru di %1$s Kirim surel diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 1d850a399041..00b618ac3c8d 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -340,6 +340,8 @@ Búa til Engar möppur hér Velja + Veldu úttaksmöppu + Færa Þú hefur ekki heimild %s til að afrita þessa skrá til að búa til þessa skrá @@ -793,6 +795,7 @@ Bíddu augnablik… Athuga geymd auðkenni Afrita skrá úr einkageymslu + Uppfæra Mynd fyrir \'Hvað er nýtt\' Sleppa (Nýtt í %s) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 97c0acb7d21b..853538454e40 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -392,6 +392,9 @@ Crea Qui non c\'è alcuna cartella Scegli + Scegli la cartella di destinazione + Copia + Sposta Non ti è consentito %s per copiare questo file per creare questo file @@ -941,6 +944,7 @@ Attendi… Controllo delle credenziali memorizzate Copia file dall\'archiviazione privata + Aggiorna Immagine Cosa c\'è di nuovo Salta Prima volta su %1$s diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index c52880031c86..7c8b9e26a98b 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -348,6 +348,9 @@ יצירה אין כאן תיקיות בחירה + נא לבחור תיקיית יעד + העתקה + העברה אין לך הרשאה %s להעתיק את הקובץ הזה ליצור את הקובץ הזה @@ -807,6 +810,7 @@ נא להמתין רגע… בודק אישורים שמורים מעתיק קובץ מאחסון פרטי + עדכון תמונת מה חדש דלג חדש ב־%1$s diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 8712d6f4355c..3e40a180b51c 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -388,6 +388,9 @@ 作成 フォルダーがありません 選択 + ターゲットフォルダーを選択 + コピー + 移動 %sが許可されていません。 このファイルをコピー このファイルを作成する @@ -922,6 +925,7 @@ 少々お待ちください… 保存された資格情報をチェック プライベートストレージからファイルをコピー中 + 更新 新しいイメージとは スキップ %1$sの新機能 diff --git a/app/src/main/res/values-ka-rGE/strings.xml b/app/src/main/res/values-ka-rGE/strings.xml index f849a3320722..f243aecc967e 100644 --- a/app/src/main/res/values-ka-rGE/strings.xml +++ b/app/src/main/res/values-ka-rGE/strings.xml @@ -219,6 +219,7 @@ შექმნა აქ დირექტორიები არაა არჩევა + გადატანა თქვენ არ გაქვთ უფლება, %s რომ დააკოპიროთ ეს ფაილი შექმნათ ეს ფაილი @@ -505,6 +506,7 @@ ჩამოტვირთვა მოწმდება შენახული უფლებამოსილებანი ფაილის კოპირება პირადი საცავიდან + განახლება რა არის ახალი სურათი გამოტოვება ახალი %1$s-ში diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 4fe95c0c284d..aa76b63429c6 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -389,6 +389,8 @@ 생성 폴더가 없습니다 선택 + 폴더 선택 + 이동 %s 권한이 없음 이 파일을 복사할 파일을 생성할 @@ -941,6 +943,7 @@ Nextcloud를 여기서 확인하십시오: https://nextcloud.com 잠시 기다려 주십시오… 저장된 인증 정보 확인 중 개인 저장소에서 파일 복사 + 업데이트 새로운 기능 사진 건너뛰기 %1$s의 새로운 것 diff --git a/app/src/main/res/values-lo/strings.xml b/app/src/main/res/values-lo/strings.xml index cc130232f389..ce605d3f413e 100644 --- a/app/src/main/res/values-lo/strings.xml +++ b/app/src/main/res/values-lo/strings.xml @@ -342,6 +342,8 @@ ສ້າງ ບໍ່ມີໂຟນເດີ ເລືອກ + ສຳເນົາ + ຍ້າຍ ບໍ່ໄດ້ຮັບອານຸຍາດ%s ສໍາເນົາຟາຍນີ້ ສ້າງຟາຍນີ້ diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml index 2d63918af878..87fdde43f23b 100644 --- a/app/src/main/res/values-lt-rLT/strings.xml +++ b/app/src/main/res/values-lt-rLT/strings.xml @@ -363,6 +363,9 @@ Sukurti Čia aplankų nėra Pasirinkti + Pasirinkite paskirties aplanką + Kopija + Perkelti Neturite leidimo %s kopijuoti failo Sukurti failą @@ -851,6 +854,7 @@ Palaukite… Dekriptuojami prisijungimo duomenys Kopijuojamas failas iš privačios saugyklos + Atnaujinti Koks naujas vaizdas Praleisti Naujas %1$s diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 773cf0affdf4..aff3996614f8 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -253,6 +253,7 @@ Izveidot Šeit nav mapju Izvēlieties + Pārvietot Jums nav atļaujas %s lai kopētu šo datni lai izveidotu šo datni @@ -543,6 +544,7 @@ Uzgaidiet brītiņu… Pārbauda saglabātos akredācijas datus Kopē datni no privātās krātuves + Atjaunināt Kas jauns attēls Izlaist Jaunums %1$s diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 07e5e22fca3e..f015c210a0ca 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -344,6 +344,8 @@ Креирај Тука нема папки Избери + Избери папка + Премести Вие не сте овластени %s за да ја копирате оваа датотека да креирате датотека @@ -793,6 +795,7 @@ Почекајте малку… Проверка на зачуваните акредитиви Копирам датотека од приватното складиште + Ажурирај Слика од тоа што е ново Прескокни Ново во %1$s diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 14cc430980b7..e33b3fbc7606 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -392,6 +392,9 @@ Opprett Ingen mapper her Velg + Velg målmappe + Kopi + Flytt Du har ikke tillatelse til %s å kopiere denne filen å opprette filen @@ -929,6 +932,7 @@ Vent et øyeblikk… Sjekker lagrede påloggingsdetaljer Kopierer fil fra privat lager + Oppdater Hva er nytt-bilde Hopp over Nytt i %1$s diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 6329f0587aca..7cfd4257ef78 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -372,6 +372,9 @@ Aanmaken Hier zijn geen bestanden Kiezen + Kies doelmap… + Kopiëren + Verplaatsen Je mist autorisatie %s om dit bestand te kopiëren om dit bestand te creëren @@ -886,6 +889,7 @@ Wacht even… Opgeslagen inloggegevens nakijken Bestand vanaf privéopslag kopiëren + Update Wat is nieuw afbeelding Overslaan Nieuw in %1$s diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index ebb36dd77b4e..e6198a5a30bf 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -392,6 +392,9 @@ Utwórz Brak katalogów Wybierz + Wybierz katalog docelowy + Skopiuj + Przenieś Nie masz uprawnień %s do kopiowania tego pliku do utworzenia tego pliku @@ -941,6 +944,7 @@ Proszę czekać… Sprawdzanie danych Kopiowanie pliku z prywatnego magazynu + Aktualizuj Jaki jest nowy obraz Pomiń Co nowego w %1$s diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index f349e72750dc..00a8328d9320 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -392,6 +392,9 @@ Criar Sem pastas aqui Escolher + Escolher pasta destino + Copiar + Mover Você não tem permissão %s para copiar este arquivo para criar este arquivo @@ -941,6 +944,7 @@ Aguarde um momento… Verificando credenciais salvas Copiando o arquivo da armazenagem privada + Atualizar Imagem nova Saltar Novo em %1$s diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 1c45a1ddbc51..af909d1bc6c9 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -381,6 +381,9 @@ Criar Sem pastas aqui Escolher + Escolher pasta de destino + copiar + Mover Não tem permissão %s para copiar este ficheiro para criar este ficheiro @@ -921,6 +924,7 @@ Aproveite o novo e melhorado envio automático. Aguarde um momento… A verificar as credenciais guardadas A copiar o ficheiro do armazenamento privado + Atualizar Imagem de novidades Passar à frente Novo em %1$s diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index fe2d34b6695e..ad4ea64a1137 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -386,6 +386,9 @@ Creați Niciun dosar aici Alege + Alege directorul destinație + Copiază + Mută Nu ai permisiunea %s să copiați acest fișier pentru a crea fisierul @@ -910,6 +913,7 @@ Așteaptă un moment… Se verifică datele de autentificare stocate Copiere fișier din stocare privată + Actualizare Ce imagine este nouă Sari peste Nou în %1$s diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 940c6eeeef0b..de75e897a0c5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -393,6 +393,9 @@ Создать Здесь нет каталогов Выбрать + Выбор папки назначения + Копировать + Переместить У вас нет разрешений %s для копирования этого файла для создания файла @@ -942,6 +945,7 @@ Подождите немного… Проверка сохранённых реквизитов учётных данных Копирование файла из частного хранилища + Изменение Изображение «что нового» Пропустить Новое в %1$s diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index e83542f790b5..481deb0d2c27 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -346,6 +346,9 @@ Crea Peruna cartella inoghe Sèbera + Sèbera cartella de destinatzione + Còpia + Tràmuda Non tenes su permissu%s de copiare custu documentu de creare custu archìviu @@ -819,6 +822,7 @@ Abeta pagu pagu... Controllende is credentziales sarvadas Copiende s\'archìviu dae s\'archiviatzione privada + Agiorna Immàgine de ite ddoe est de nou Brinca Prima borta in %1$s diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index 6949ae517a7c..377e6a28fa7a 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -376,6 +376,8 @@ Vytvoriť Nie sú tu žiadne priečinky Vybrať + Vyberte cieľový priečinok + Presunúť Nemáte oprávnenie %s kopírovať súbor vytvoriť súbor @@ -896,6 +898,7 @@ Počkajte chvíľu… Overujem uložené prihlasovacie údaje Kopírovanie súboru z privátneho úložiska + Aktualizovať Obrázok čo je nové Preskočiť Nové v %1$s diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 54ffc676dd2d..abc7c38411ef 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -389,6 +389,9 @@ Ustvari Ni map Izbor + Izbor ciljne mape + Kopiraj + Premakni Ni ustreznega dovoljenja %s za kopiranje te datoteke za ustvarjanje datoteke @@ -925,6 +928,7 @@ Počakajte trenutek … Poteka preverjanje shranjenih poveril Kopiranje datoteke iz zasebne shrambe + Posodobi Kaj je nova slika Preskoči Novo v %1$s diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index c879badd5b8c..72ccedae22f2 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -312,6 +312,8 @@ Krijo Këtu nuk ka dosje Zgjidhni + Kopjoni + Zhvendos Nuk keni leje %s për kopjim të kësaj kartele për krijim kartele @@ -732,6 +734,7 @@ Prisni një moment… Po kontrollohen kredenciale të depozituara Po kopjohet skedar nga depo private + Përditëso Imazhi me çfarë është e re Kalo I ri në %1$s diff --git a/app/src/main/res/values-sr-rSP/strings.xml b/app/src/main/res/values-sr-rSP/strings.xml index 9a30b0358719..e7d600837b31 100644 --- a/app/src/main/res/values-sr-rSP/strings.xml +++ b/app/src/main/res/values-sr-rSP/strings.xml @@ -286,6 +286,8 @@ Napravi Bez fascikli Odaberi + Kopiraj + Premesti Nije vam dozvoljeno da %s da kopirate ovaj fajl da napravite fajl diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 4d3e9ca96442..e40a4b282f64 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -392,6 +392,9 @@ Направи Нема фасцикли овде Одабери + Одаберите одредишну фасциклу + Копирај + Помери Није вам дозвољено %s да копирате овај фајл да направите фајл @@ -941,6 +944,7 @@ Сачекајте мало… Проверавам сачуване акредитиве Копирам фајл из личног складишта + Ажурирај Слика шта је ново Прескочи Ново у %1$s diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 5b608555a95d..f8394b873cb4 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -392,6 +392,9 @@ Skapa Inga mappar här Välj + Välj målmapp + Kopiera + Flytta Du har inte tillstånd %s att kopiera denna fil att skapa denna fil @@ -941,6 +944,7 @@ Vänligen vänta… Kontrollerar lagrade inloggningsuppgifter Kopierar fil från privat lagring + Uppdatera Vad är nytt-bild Hoppa över Ny i %1$s diff --git a/app/src/main/res/values-th-rTH/strings.xml b/app/src/main/res/values-th-rTH/strings.xml index 0f284c4bc34f..764ebe348367 100644 --- a/app/src/main/res/values-th-rTH/strings.xml +++ b/app/src/main/res/values-th-rTH/strings.xml @@ -334,6 +334,9 @@ สร้าง ไม่มีโฟลเดอร์ที่นี่ เลือก + เลือกโฟลเดอร์เป้าหมาย + คัดลอก + ย้าย คุณไม่ได้รับอนุญาต%s ให้คัดลอกไฟล์นี้ ให้สร้างไฟล์นี้ @@ -630,6 +633,7 @@ ดาวน์โหลด รอสักครู่… กำลังคัดลอกไฟล์จากพื้นที่จัดเก็บส่วนตัว + อัปเดต รูปภาพมีอะไรใหม่ ข้าม มีอะไรใหม่ใน %1$s diff --git a/app/src/main/res/values-tk/strings.xml b/app/src/main/res/values-tk/strings.xml index 962bfb591000..d2eb9098cf29 100644 --- a/app/src/main/res/values-tk/strings.xml +++ b/app/src/main/res/values-tk/strings.xml @@ -330,6 +330,8 @@ Dörediň Bu ýerde bukjalar ýok Saýlaň + Göçüriň + Göçüriň %sSize rugsat berilmeýär bu faýly göçürmek bu faýly döretmek diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 47e5571f75b4..123bfa207c7f 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -392,6 +392,9 @@ Ekle Burada herhangi bir klasör yok Seçin + Hedef klasörü seçin + Kopyala + Taşı %s izniniz yok bu dosyayı kopyalayın bu dosyayı oluşturamazsınız @@ -941,6 +944,7 @@ Lütfen bekleyin … Kayıtlı kimlik bilgileri denetleniyor Dosya kişisel depolamadan kopyalanıyor + Güncelle Yenilikler görseli Atla %1$s yenilikleri diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index b5491249045a..9c66b218e5c0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -314,7 +314,7 @@ Додати або завантажити Не вдалося передати файл до менеджера звантажень Не вдалося роздрукувати файл - Не вдалося запустити редактор + Не вдалося відкрити редактор Неможливо оновити інтерфейс Додати зірочку Із зірочкою @@ -390,6 +390,9 @@ Створити Тут відсутні каталоги Вибрати + Виберіть каталог призначення + Копія + Перемістити У вас відсутні повноваження %s скопіювати цей файл щоб створити цей файл @@ -931,6 +934,7 @@ Зачекайте трохи… Перевірка збережених даних авторизації Копіювання файлу з приватного сховища + Оновлення Зображення про нові функції Пропустити Нове у %1$s diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index b78e6fd7ba7f..ba2e5d6ee42f 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -376,6 +376,8 @@ Tạo Không có thư mục nào Chọn + ‎Chọn thư‎‎ ‎‎mục đích‎ + Dịch chuyển Bạn không được cho phép %s sao chép tập tin này tạo tập tin này @@ -883,6 +885,7 @@ Đợi một chút… Kiểm tra thông tin đăng nhập đã lưu trữ Sao chép tệp từ bộ nhớ riêng + Cập nhật Hình ảnh mới là gì Bỏ qua Mới trong %1$s diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 83f3a23314ee..32f36352e970 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -392,6 +392,9 @@ 创建 这里没有文件夹 选择 + 选择目标文件夹 + 复制 + 移动 您没有 %s 的权限 复制这个文件 创建这个文件 @@ -944,6 +947,7 @@ 请稍等… 正在检查保存的证书 正在从私有存储中复制文件 + 更新 有什么新图片 跳过 新建%1$s diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 0fc553cb404d..c603bbdf982b 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -392,6 +392,9 @@ 建立 這裡沒有資料夾 選擇 + 選擇目標資料夾 + 複製 + 移動 您沒有%s的權限 複製這個檔案 建立此檔案 @@ -941,6 +944,7 @@ 請稍候… 檢查儲存的身分驗證 從私有儲存空間複製檔案中 + 更新 有什麼新圖像? 略過 新增到 %1$s diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 7a74434ec14e..944b5fda4342 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -392,6 +392,9 @@ 建立 這裡沒有資料夾 選擇 + 選擇目標資料夾 + 複製 + 移動 您沒有 %s 的權限 複製這個檔案 建立此檔案 @@ -941,6 +944,7 @@ 稍候…… 正在檢查儲存的憑證 從私有儲存空間複製檔案中 + 更新 有什麼新圖片? 略過 新增到 %1$s diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 60e85c4e8b1c..113e681bbd2b 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -30,8 +30,7 @@ - - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 75112b21250e..d552f8c69bc1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -162,6 +162,7 @@ Fetching server version… Waiting to upload %1$s (%2$d) + Downloading \u0020 Downloading… %1$d%% Downloading %2$s Downloaded @@ -428,10 +429,12 @@ No app for sending logs found. Please install an email client. %1$s Android app logs - Move - Copy + Move or Copy Nothing in here. You can add a folder. - Choose + + Copy + Move + Choose target folder Unable to move file. Please check whether it exists. It is not possible to move a folder into one of its own underlying folders @@ -439,7 +442,6 @@ An error occurred while trying to move this file or folder to move this file - Unable to copy. Please check whether the file exists. It is not possible to copy a folder into one of its own underlying folders The file is already present in the destination folder @@ -545,8 +547,6 @@ Get release candidate from F-Droid app Get development release from F-Droid app Download development release directly - Move to… - Copy to… Choose remote folder… Choose local folder… %1$s/%2$s @@ -656,12 +656,6 @@ shared via link shared More menu - A - Z - Newest first - Biggest first - Smallest first - Z - A - Oldest first Type Sync status button Settings button @@ -976,6 +970,10 @@ Close Login with %1$s to %2$s Login via direct link failed! + Update Android System WebView + Please update the Android System WebView app for a login + Update + The link to your %1$s web interface when you open it in the browser. Delayed due to too many wrong attempts Create @@ -1035,8 +1033,6 @@ Show videos Videos only Set media folder - Choose location - Select Lock file Unlock file Error changing file lock status