diff --git a/.editorconfig b/.editorconfig index 3ba9e9360807..67e5fa3c2eae 100644 --- a/.editorconfig +++ b/.editorconfig @@ -42,7 +42,10 @@ trim_trailing_whitespace=false indent_size=2 [*.{kt,kts}] +ktlint_code_style = android_studio # IDE does not follow this Ktlint rule strictly, but the default ordering is pretty good anyway, so let's ditch it ktlint_standard_import-ordering = disabled +ktlint_standard_no-consecutive-comments = disabled +ktlint_function_naming_ignore_when_annotated_with = Composable ij_kotlin_allow_trailing_comma = false ij_kotlin_allow_trailing_comma_on_call_site = false diff --git a/.github/workflows/autoApproveRenovate.yml b/.github/workflows/autoApproveRenovate.yml new file mode 100644 index 000000000000..6aaeb1e318db --- /dev/null +++ b/.github/workflows/autoApproveRenovate.yml @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2023 Álvaro Brey +# SPDX-FileCopyrightText: 2023 Andy Scherzinger +# SPDX-License-Identifier: GPL-3.0-or-later + +name: Auto approve renovate PRs + +on: + pull_request_target: + branches: + - main + - master + - stable* + +permissions: + contents: read + +concurrency: + group: renovate-approve-merge-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + auto-approve-merge: + if: github.actor == 'renovate[bot]' + runs-on: ubuntu-latest + permissions: + # for hmarr/auto-approve-action to approve PRs + pull-requests: write + # for alexwilson/enable-github-automerge-action to approve PRs + contents: write + + steps: + - uses: mdecoleman/pr-branch-name@bab4c71506bcd299fb350af63bb8e53f2940a599 # v2.0.0 + id: branchname + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + # GitHub actions bot approve + - uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363 # v4.0.0 + if: startsWith(steps.branchname.outputs.branch, 'renovate/') + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + # Enable GitHub auto merge + - name: Auto merge + uses: alexwilson/enable-github-automerge-action@main + if: startsWith(steps.branchname.outputs.branch, 'renovate/') + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 326bd8d6cfc9..50695ca84077 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -39,7 +39,7 @@ jobs: with: swap-size-gb: 10 - name: Initialize CodeQL - uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/init@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 with: languages: ${{ matrix.language }} - name: Set up JDK 17 @@ -53,4 +53,4 @@ jobs: echo "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties" ./gradlew assembleDebug - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 8b9eea8b7a8a..edae2f673e96 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -26,4 +26,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: gradle/wrapper-validation-action@88425854a36845f9c881450d9660b5fd46bee142 # v3.4.2 + - uses: gradle/wrapper-validation-action@f9c9c575b8b21b6485636a91ffecd10e558c62f6 # v3.5.0 diff --git a/.github/workflows/pr-feedback.yml b/.github/workflows/pr-feedback.yml index be0d028f95cf..7b68226f89d5 100644 --- a/.github/workflows/pr-feedback.yml +++ b/.github/workflows/pr-feedback.yml @@ -35,7 +35,7 @@ jobs: with: feedback-message: | Hello there, - Thank you so much for taking the time and effort to create a pull request to our Nextcloud project. + Thank you so much for taking the time and effort to create a pull request to our Nextcloud project. We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process. @@ -45,6 +45,6 @@ jobs: (If you believe you should not receive this message, you can add yourself to the [blocklist](https://github.com/nextcloud/.github/blob/master/non-community-usernames.txt).) days-before-feedback: 14 - start-date: "2024-04-30" - exempt-authors: "${{ steps.blocklist.outputs.blocklist }},${{ steps.scrape.outputs.users }},nextcloud-command,nextcloud-android-bot" + start-date: '2024-04-30' + exempt-authors: '${{ steps.blocklist.outputs.blocklist }},${{ steps.scrape.outputs.users }},nextcloud-command,nextcloud-android-bot' exempt-bots: true diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index e1d5e67b23a7..a031b5ded615 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check if secrets are available - run: echo "::set-output name=ok::${{ secrets.KS_PASS != '' }}" + run: echo "ok=${{ secrets.KS_PASS != '' }}" >> "$GITHUB_OUTPUT" id: check-secrets - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 if: ${{ steps.check-secrets.outputs.ok == 'true' }} diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml index 3d095278e436..031e80a83555 100644 --- a/.github/workflows/reuse.yml +++ b/.github/workflows/reuse.yml @@ -9,12 +9,14 @@ name: REUSE Compliance Check -on: pull_request +on: [pull_request] jobs: reuse-compliance-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: REUSE Compliance Check - uses: fsfe/reuse-action@a46482ca367aef4454a87620aa37c2be4b2f8106 # v3.0.0 + uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 70e087f6ee57..0e63bf75a362 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/upload-sarif@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 with: sarif_file: results.sarif diff --git a/.github/workflows/screenShotTest.yml b/.github/workflows/screenShotTest.yml index a2b8e633b0f3..c633ab678216 100644 --- a/.github/workflows/screenShotTest.yml +++ b/.github/workflows/screenShotTest.yml @@ -56,7 +56,7 @@ jobs: - name: create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@77986be26589807b8ebab3fde7bbf5c60dabec32 # v2.31.0 + uses: reactivecircus/android-emulator-runner@f0d1ed2dcad93c7479e8b2f2226c83af54494915 # v2.32.0 with: api-level: ${{ matrix.api-level }} force-avd-creation: false @@ -84,7 +84,7 @@ jobs: - name: Run screenshot tests env: SHOT_TEST: "true" - uses: reactivecircus/android-emulator-runner@77986be26589807b8ebab3fde7bbf5c60dabec32 # v2.31.0 + uses: reactivecircus/android-emulator-runner@f0d1ed2dcad93c7479e8b2f2226c83af54494915 # v2.32.0 with: api-level: ${{ matrix.api-level }} force-avd-creation: false @@ -99,7 +99,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: scripts/uploadReport.sh "${{ secrets.LOG_USERNAME }}" "${{ secrets.LOG_PASSWORD }}" ${{github.event.number}} "${{ matrix.color }}-${{ matrix.scheme }}" "Screenshot" ${{github.event.number}} - name: Archive Espresso results - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 if: ${{ always() }} with: name: Report-${{ matrix.color }}-${{ matrix.scheme }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 47f4f4c4a886..3e602090c1fc 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -33,7 +33,7 @@ jobs: if: ${{ always() }} run: scripts/deleteOldComments.sh "test" "Unit" ${{github.event.number}} - name: Run unit tests with coverage - uses: gradle/gradle-build-action@66535aaf56f831b35e3a8481c9c99b665b84dd45 # v3.4.2 + uses: gradle/gradle-build-action@ac2d340dc04d9e1113182899e983b5400c17cda1 # v3.5.0 with: arguments: jacocoTestGplayDebugUnitTest - name: Upload failing results @@ -49,7 +49,7 @@ jobs: fail_ci_if_error: true - name: Upload jacoco artifacts if: ${{ failure() }} - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: test-results path: app/build/reports/tests/testGplayDebugUnitTest/ diff --git a/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index acdbfdc008a4..000000000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,28 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: Nextcloud Android -Upstream-Contact: Nextcloud Android team -Source: https://github.com/nextcloud/android - -Files: gradle/wrapper/gradle-wrapper.jar -Copyright: 2015-2021 the original authors -License: Apache-2.0 - -Files: user_manual/images/android-1.png user_manual/images/android-2.png user_manual/images/android-3.png user_manual/images/android-4.png user_manual/images/android-10.png user_manual/images/davdroid-1-button-in-nextcloud-app.png user_manual/images/davdroid-2-install-davdroid.png user_manual/images/davdroid-3-enter-password.png user_manual/images/davdroid-4-specify-owner-email.png -Copyright: 2016-2024 Nextcloud GmbH and Nextcloud contributors -License: AGPL-3.0-or-later - -Files: user_manual/conf.py user_manual/android_app.rst user_manual/index.rst user_manual/conf.py user_manual/Makefile -Copyright: 2015-2016 ownCloud Inc., 2016-2024 Nextcloud GmbH -License: GPL-2.0-only - -Files: user_manual/images/android-11.png user_manual/images/android-12.png user_manual/images/android-13.png user_manual/images/android-14.png user_manual/images/android-15.png user_manual/images/android-5.png user_manual/images/android-6.png user_manual/images/android-8.png user_manual/images/android-9.png -Copyright: 2015-2016 ownCloud Inc. -License: GPL-2.0-only - -Files: app/src/*/res/mipmap-*dpi/ic_launcher.png app/src/*/ic_launcher-web.png src/generic/fastlane/metadata/android/en-US/images/icon.png src/versionDev/fastlane/metadata/android/en-US/images/icon.png app/src/main/ic_launcher-web-round.png -Copyright: 2017-2024 Nextcloud GmbH -License: LicenseRef-NextcloudTrademarks - -Files: .idea/* app/schemas/com.nextcloud.client.database.NextcloudDatabase/*.json app/screenshots/gplay/debug/*.png app/src/main/res/values-*/strings.xml src/*/fastlane/metadata/android/*/*.txt src/versionDev/fastlane/metadata/android/*/changelogs/*.txt app/src/androidTest/assets/* app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker app/src/*/google-services.json app/src/main/res/drawable-*dpi/checker_16_16.png app/src/main/res/raw/encryption_key_words.txt app/src/main/resources/ical4j.properties app/src/main/res/drawable-*dpi/apk.png app/src/main/res/drawable-*dpi/fdroid.png app/src/main/res/drawable-*dpi/playstore.png app/src/main/res/drawable-*dpi/background.png app/src/main/res/drawable-*dpi/background_nc18.png -Copyright: 2016-2024 Nextcloud GmbH and Nextcloud contributors -License: AGPL-3.0-or-later diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dfc2e760c77..70d1944d18f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 3.29.1 (June 27, 2024) + +- Bugfixes + +Minimum: NC 16 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/93 + maybe needs password 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 5a36009fb8d8..b9105cb833f9 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 @@ -21,7 +21,6 @@ import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; @@ -45,6 +44,7 @@ import android.view.WindowManager; import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.nextcloud.appReview.InAppReviewHelper; import com.nextcloud.client.account.User; @@ -66,6 +66,7 @@ import com.nextcloud.model.WorkerStateLiveData; import com.nextcloud.utils.extensions.ActivityExtensionsKt; import com.nextcloud.utils.extensions.BundleExtensionsKt; +import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.extensions.IntentExtensionsKt; import com.nextcloud.utils.view.FastScrollUtils; import com.owncloud.android.MainApp; @@ -327,10 +328,15 @@ private void checkStoragePath() { MainApp.setStoragePath(newStorage); try { - AlertDialog alertDialog = new AlertDialog.Builder(this, R.style.Theme_ownCloud_Dialog).setTitle(R.string.wrong_storage_path).setMessage(R.string.wrong_storage_path_desc).setPositiveButton(R.string.dialog_close, (dialog, which) -> dialog.dismiss()).setIcon(R.drawable.ic_settings).create(); + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog) + .setTitle(R.string.wrong_storage_path) + .setMessage(R.string.wrong_storage_path_desc) + .setPositiveButton(R.string.dialog_close, (dialog, which) -> dialog.dismiss()) + .setIcon(R.drawable.ic_settings); - alertDialog.show(); - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)); + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(getApplicationContext(), builder); + + builder.create().show(); } catch (WindowManager.BadTokenException e) { Log_OC.e(TAG, "Error showing wrong storage info, so skipping it: " + e.getMessage()); } @@ -404,18 +410,12 @@ private void upgradeNotificationForInstantUpload() { if (preferences.instantPictureUploadEnabled() || preferences.instantVideoUploadEnabled()) { preferences.removeLegacyPreferences(); // show info pop-up - new AlertDialog.Builder(this, R.style.Theme_ownCloud_Dialog).setTitle(R.string.drawer_synced_folders).setMessage(R.string.synced_folders_new_info).setPositiveButton(R.string.drawer_open, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // show instant upload - Intent syncedFoldersIntent = new Intent(getApplicationContext(), SyncedFoldersActivity.class); - dialog.dismiss(); - startActivity(syncedFoldersIntent); - } - }).setNegativeButton(R.string.drawer_close, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }).setIcon(R.drawable.nav_synced_folders).show(); + new MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog).setTitle(R.string.drawer_synced_folders).setMessage(R.string.synced_folders_new_info).setPositiveButton(R.string.drawer_open, (dialog, which) -> { + // show instant upload + Intent syncedFoldersIntent = new Intent(getApplicationContext(), SyncedFoldersActivity.class); + dialog.dismiss(); + startActivity(syncedFoldersIntent); + }).setNegativeButton(R.string.drawer_close, (dialog, which) -> dialog.dismiss()).setIcon(R.drawable.nav_synced_folders).show(); } } @@ -1067,8 +1067,8 @@ private void popBack() { protected void onSaveInstanceState(@NonNull Bundle outState) { // responsibility of restore is preferred in onCreate() before than in // onRestoreInstanceState when there are Fragments involved - Log_OC.v(TAG, "onSaveInstanceState() start"); super.onSaveInstanceState(outState); + FileExtensionsKt.logFileSize(mWaitingToPreview, TAG); outState.putParcelable(FileDisplayActivity.KEY_WAITING_TO_PREVIEW, mWaitingToPreview); outState.putBoolean(FileDisplayActivity.KEY_SYNC_IN_PROGRESS, mSyncInProgress); // outState.putBoolean(FileDisplayActivity.KEY_REFRESH_SHARES_IN_PROGRESS, @@ -2399,12 +2399,15 @@ private void selectUserAndOpenFile(List users, String fileId) { for (int i = 0; i < userNames.length; i++) { userNames[i] = users.get(i).getAccountName(); } - final AlertDialog.Builder builder = new AlertDialog.Builder(this); + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(R.string.common_choose_account).setItems(userNames, (dialog, which) -> { User user = users.get(which); openFile(user, fileId); showLoadingDialog(getString(R.string.retrieving_file)); }); + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(getApplicationContext(), builder); + final AlertDialog dialog = builder.create(); dismissLoadingDialog(); dialog.show(); 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 f8fbc5c1efe4..ed9574f1fb1d 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 @@ -475,10 +475,7 @@ open class FolderPickerActivity : * @param operation Creation operation performed. * @param result Result of the creation. */ - private fun onCreateFolderOperationFinish( - operation: CreateFolderOperation, - result: RemoteOperationResult<*> - ) { + private fun onCreateFolderOperationFinish(operation: CreateFolderOperation, result: RemoteOperationResult<*>) { if (result.isSuccess) { val fileListFragment = listOfFilesFragment fileListFragment?.onItemClicked(storageManager.getFileByPath(operation.remotePath)) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt index 471390356e49..671efe8492b0 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt @@ -343,11 +343,7 @@ class NotificationsActivity : DrawerActivity(), NotificationsContract.View { } } - override fun onActionCallback( - isSuccess: Boolean, - notification: Notification, - holder: NotificationViewHolder - ) { + override fun onActionCallback(isSuccess: Boolean, notification: Notification, holder: NotificationViewHolder) { if (isSuccess) { adapter?.removeNotification(holder) } else { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java index 0370aadb3b39..96119519fd40 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java @@ -46,12 +46,14 @@ import android.widget.Toast; import com.google.android.material.button.MaterialButton; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.nextcloud.client.account.User; import com.nextcloud.client.di.Injectable; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.utils.extensions.BundleExtensionsKt; +import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.extensions.IntentExtensionsKt; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -103,7 +105,6 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog.Builder; import androidx.appcompat.widget.SearchView; import androidx.core.view.MenuItemCompat; @@ -134,6 +135,7 @@ public class ReceiveExternalFilesActivity extends FileActivity @Inject AppPreferences preferences; @Inject LocalBroadcastManager localBroadcastManager; @Inject SyncedFolderProvider syncedFolderProvider; + private AccountManager mAccountManager; private Stack mParents = new Stack<>(); private List mStreamsToUpload; @@ -204,7 +206,7 @@ protected void setAccount(Account account, boolean savedAccount) { Account[] accounts = mAccountManager.getAccountsByType(MainApp.getAccountType(this)); if (accounts.length == 0) { Log_OC.i(TAG, "No ownCloud account is available"); - DialogNoAccount dialog = new DialogNoAccount(); + DialogNoAccount dialog = new DialogNoAccount(viewThemeUtils); dialog.show(getSupportFragmentManager(), null); } @@ -264,7 +266,7 @@ private void browseToFolderIfItExists() { @Override protected void onSaveInstanceState(@NonNull Bundle outState) { - Log_OC.d(TAG, "onSaveInstanceState() start"); + FileExtensionsKt.logFileSize(mFile, TAG); super.onSaveInstanceState(outState); outState.putString(KEY_PARENTS, generatePath(mParents)); outState.putParcelable(KEY_FILE, mFile); @@ -307,10 +309,16 @@ public void selectFile(OCFile file) { } public static class DialogNoAccount extends DialogFragment { + private final ViewThemeUtils viewThemeUtils; + + public DialogNoAccount(ViewThemeUtils viewThemeUtils) { + this.viewThemeUtils = viewThemeUtils; + } + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new Builder(getActivity()); + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); builder.setIcon(R.drawable.ic_warning); builder.setTitle(R.string.uploader_wrn_no_account_title); builder.setMessage(String.format(getString(R.string.uploader_wrn_no_account_text), @@ -327,7 +335,8 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { startActivityForResult(intent, REQUEST_CODE__SETUP_ACCOUNT); }); builder.setNeutralButton(R.string.uploader_wrn_no_account_quit_btn_text, - (dialog, which) -> getActivity().finish()); + (dialog, which) -> requireActivity().finish()); + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(requireContext(), builder); return builder.create(); } } @@ -680,7 +689,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } Account[] accounts = mAccountManager.getAccountsByType(MainApp.getAuthTokenType()); if (accounts.length == 0) { - DialogNoAccount dialog = new DialogNoAccount(); + DialogNoAccount dialog = new DialogNoAccount(viewThemeUtils); dialog.show(getSupportFragmentManager(), null); } else { // there is no need for checking for is there more then one diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java index 5e68f2900edc..a5272adc86a1 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java @@ -38,6 +38,7 @@ import android.view.ViewGroup; import android.webkit.URLUtil; +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; @@ -82,7 +83,6 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; @@ -493,32 +493,34 @@ private void removeE2E(PreferenceCategory preferenceCategoryMore) { preferenceCategoryMore.removePreference(preference); } else { preference.setOnPreferenceClickListener(p -> { - AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.FallbackTheming_Dialog); - AlertDialog alertDialog = builder.setTitle(R.string.prefs_e2e_mnemonic) - .setMessage(getString(R.string.remove_e2e_message)) - .setCancelable(true) - .setNegativeButton(R.string.common_cancel, ((dialog, i) -> dialog.dismiss())) - .setPositiveButton(R.string.confirm_removal, (dialog, which) -> { - EncryptionUtils.removeE2E(arbitraryDataProvider, user); - preferenceCategoryMore.removePreference(preference); - - Preference pMnemonic = findPreference("mnemonic"); - if (pMnemonic != null) { - preferenceCategoryMore.removePreference(pMnemonic); - } - - dialog.dismiss(); - }) - .create(); - - alertDialog.show(); - + showRemoveE2EAlertDialog(preferenceCategoryMore, preference); return true; }); } } } + private void showRemoveE2EAlertDialog(PreferenceCategory preferenceCategoryMore, Preference preference) { + new MaterialAlertDialogBuilder(this, R.style.FallbackTheming_Dialog) + .setTitle(R.string.prefs_e2e_mnemonic) + .setMessage(getString(R.string.remove_e2e_message)) + .setCancelable(true) + .setNegativeButton(R.string.common_cancel, ((dialog, i) -> dialog.dismiss())) + .setPositiveButton(R.string.confirm_removal, (dialog, which) -> { + EncryptionUtils.removeE2E(arbitraryDataProvider, user); + preferenceCategoryMore.removePreference(preference); + + Preference pMnemonic = findPreference("mnemonic"); + if (pMnemonic != null) { + preferenceCategoryMore.removePreference(pMnemonic); + } + + dialog.dismiss(); + }) + .create() + .show(); + } + private void setupHelpPreference(PreferenceCategory preferenceCategoryMore) { boolean helpEnabled = getResources().getBoolean(R.bool.help_enabled); Preference pHelp = findPreference("help"); @@ -791,7 +793,7 @@ private void setupGeneralCategory() { if (storagePath.equals(newPath)) { return true; } - StorageMigration storageMigration = new StorageMigration(this, user, storagePath, newPath); + StorageMigration storageMigration = new StorageMigration(this, user, storagePath, newPath, viewThemeUtils); storageMigration.setStorageMigrationProgressListener(this); storageMigration.migrate(); @@ -979,22 +981,23 @@ public void handleMnemonicRequest(Intent data) { ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(this); String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC).trim(); - - AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.FallbackTheming_Dialog); - AlertDialog alertDialog = builder.setTitle(R.string.prefs_e2e_mnemonic) - .setMessage(mnemonic) - .setNegativeButton(R.string.common_cancel, (dialog, i) -> dialog.dismiss()) - .setNeutralButton(R.string.common_copy, (dialog, i) -> - ClipboardUtil.copyToClipboard(this, mnemonic, false)) - .setPositiveButton(R.string.common_ok, (dialog, which) -> dialog.dismiss()) - .create(); - - alertDialog.show(); - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)); + showMnemonicAlertDialogDialog(mnemonic); } } } + private void showMnemonicAlertDialogDialog(String mnemonic) { + new MaterialAlertDialogBuilder(this, R.style.FallbackTheming_Dialog) + .setTitle(R.string.prefs_e2e_mnemonic) + .setMessage(mnemonic) + .setPositiveButton(R.string.common_ok, (dialog, which) -> dialog.dismiss()) + .setNegativeButton(R.string.common_cancel, (dialog, i) -> dialog.dismiss()) + .setNeutralButton(R.string.common_copy, (dialog, i) -> + ClipboardUtil.copyToClipboard(this, mnemonic, false)) + .create() + .show(); + } + @Override @NonNull public MenuInflater getMenuInflater() { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/StorageMigration.java b/app/src/main/java/com/owncloud/android/ui/activity/StorageMigration.java index 995894029137..5574e8ffa1d7 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/StorageMigration.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/StorageMigration.java @@ -19,12 +19,14 @@ import android.os.AsyncTask; import android.view.View; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.nextcloud.client.account.User; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.utils.FileStorageUtils; +import com.owncloud.android.utils.theme.ViewThemeUtils; import java.io.File; @@ -37,10 +39,11 @@ public class StorageMigration { private static final String TAG = StorageMigration.class.getName(); - private Context mContext; - private User user; - private String mSourceStoragePath; - private String mTargetStoragePath; + private final Context mContext; + private final User user; + private final String mSourceStoragePath; + private final String mTargetStoragePath; + private final ViewThemeUtils viewThemeUtils; private StorageMigrationProgressListener mListener; @@ -49,11 +52,12 @@ public interface StorageMigrationProgressListener { void onCancelMigration(); } - public StorageMigration(Context context, User user, String sourcePath, String targetPath) { + public StorageMigration(Context context, User user, String sourcePath, String targetPath, ViewThemeUtils viewThemeUtils) { mContext = context; this.user = user; mSourceStoragePath = sourcePath; mTargetStoragePath = targetPath; + this.viewThemeUtils = viewThemeUtils; } public void setStorageMigrationProgressListener(StorageMigrationProgressListener listener) { @@ -72,7 +76,8 @@ public void migrate() { mSourceStoragePath, mTargetStoragePath, progressDialog, - mListener).execute(); + mListener, + viewThemeUtils).execute(); progressDialog.getButton(ProgressDialog.BUTTON_POSITIVE).setVisibility(View.GONE); } @@ -83,62 +88,76 @@ private boolean storageFolderAlreadyExists() { return f.exists() && f.isDirectory(); } - private void askToOverride() { + public static void a(ViewThemeUtils viewThemeUtils, Context context) { + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) + .setMessage(R.string.file_migration_directory_already_exists) + .setCancelable(true) + .setOnCancelListener(dialogInterface -> { + + }) + .setNegativeButton(R.string.common_cancel, (dialogInterface, i) -> { + + }) + .setNeutralButton(R.string.file_migration_use_data_folder, (dialogInterface, i) -> { + + }) + .setPositiveButton(R.string.file_migration_override_data_folder, (dialogInterface, i) -> { - new AlertDialog.Builder(mContext) + }); + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(context, builder); + + AlertDialog alertDialog = builder.create(); + + alertDialog.show(); + } + + private void askToOverride() { + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mContext) .setMessage(R.string.file_migration_directory_already_exists) .setCancelable(true) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialogInterface) { - if (mListener != null) { - mListener.onCancelMigration(); - } + .setOnCancelListener(dialogInterface -> { + if (mListener != null) { + mListener.onCancelMigration(); } }) - .setNegativeButton(R.string.common_cancel, new OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - if (mListener != null) { - mListener.onCancelMigration(); - } + .setNegativeButton(R.string.common_cancel, (dialogInterface, i) -> { + if (mListener != null) { + mListener.onCancelMigration(); } }) - .setNeutralButton(R.string.file_migration_use_data_folder, new OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - ProgressDialog progressDialog = createMigrationProgressDialog(); - progressDialog.show(); - new StoragePathSwitchTask( - mContext, - user, - mSourceStoragePath, - mTargetStoragePath, - progressDialog, - mListener).execute(); - - progressDialog.getButton(ProgressDialog.BUTTON_POSITIVE).setVisibility(View.GONE); + .setNeutralButton(R.string.file_migration_use_data_folder, (dialogInterface, i) -> { + ProgressDialog progressDialog = createMigrationProgressDialog(); + progressDialog.show(); + new StoragePathSwitchTask( + mContext, + user, + mSourceStoragePath, + mTargetStoragePath, + progressDialog, + mListener, + viewThemeUtils).execute(); + + progressDialog.getButton(ProgressDialog.BUTTON_POSITIVE).setVisibility(View.GONE); - } }) - .setPositiveButton(R.string.file_migration_override_data_folder, new OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - ProgressDialog progressDialog = createMigrationProgressDialog(); - progressDialog.show(); - new FileMigrationTask( - mContext, - user, - mSourceStoragePath, - mTargetStoragePath, - progressDialog, - mListener).execute(); - - progressDialog.getButton(ProgressDialog.BUTTON_POSITIVE).setVisibility(View.GONE); - } - }) - .create() - .show(); + .setPositiveButton(R.string.file_migration_override_data_folder, (dialogInterface, i) -> { + ProgressDialog progressDialog = createMigrationProgressDialog(); + progressDialog.show(); + new FileMigrationTask( + mContext, + user, + mSourceStoragePath, + mTargetStoragePath, + progressDialog, + mListener, + viewThemeUtils).execute(); + + progressDialog.getButton(ProgressDialog.BUTTON_POSITIVE).setVisibility(View.GONE); + }); + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(mContext, builder); + builder.create().show(); } private ProgressDialog createMigrationProgressDialog() { @@ -168,20 +187,22 @@ private static abstract class FileMigrationTaskBase extends AsyncTask dialogInterface.dismiss()) + .setPositiveButton(R.string.common_yes, (dialogInterface, i) -> { + if (mListener != null) { + mListener.onStorageMigrationFinished(mStorageTarget, true); } - }) - .create() - .show(); + }); + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(mContext, builder); + builder.create().show(); } protected boolean[] saveAccountsSyncStatus() { @@ -292,8 +306,9 @@ public StoragePathSwitchTask(Context context, String source, String target, ProgressDialog progressDialog, - StorageMigrationProgressListener listener) { - super(context, user, source, target, progressDialog, listener); + StorageMigrationProgressListener listener, + ViewThemeUtils viewThemeUtils) { + super(context, user, source, target, progressDialog, listener, viewThemeUtils); } @Override @@ -340,8 +355,9 @@ public FileMigrationTask(Context context, String source, String target, ProgressDialog progressDialog, - StorageMigrationProgressListener listener) { - super(context, user, source, target, progressDialog, listener); + StorageMigrationProgressListener listener, + ViewThemeUtils viewThemeUtils) { + super(context, user, source, target, progressDialog, listener, viewThemeUtils); } @Override 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 92b17dfb2cbc..23486394a2ca 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 @@ -24,6 +24,7 @@ import androidx.appcompat.app.AlertDialog import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.nextcloud.client.core.Clock import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.di.Injectable @@ -221,13 +222,15 @@ class SyncedFoldersActivity : } private fun showPowerCheckDialog() { - val alertDialog = AlertDialog.Builder(this) - .setView(findViewById(R.id.root_layout)) + val builder = MaterialAlertDialogBuilder(this) + .setView(R.id.root_layout) .setPositiveButton(R.string.common_ok) { dialog, _ -> dialog.dismiss() } .setTitle(R.string.autoupload_disable_power_save_check) .setMessage(getString(R.string.power_save_check_dialog_message)) - .show() - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, builder) + + builder.create().show() } /** @@ -307,7 +310,8 @@ class SyncedFoldersActivity : } val syncFolderItems = sortSyncedFolderItems( mergeFolderData(currentAccountSyncedFoldersList, mediaFolders) - ) + ).filterNotNull() + CoroutineScope(Dispatchers.Main).launch { adapter.setSyncFolderItems(syncFolderItems) adapter.notifyDataSetChanged() @@ -315,7 +319,9 @@ class SyncedFoldersActivity : if (!TextUtils.isEmpty(path)) { val section = adapter.getSectionByLocalPathAndType(path, type) if (section >= 0) { - onSyncFolderSettingsClick(section, adapter[section]) + adapter.get(section)?.let { + onSyncFolderSettingsClick(section, it) + } } } loadJob = null @@ -559,7 +565,9 @@ class SyncedFoldersActivity : return result } - override fun onSyncStatusToggleClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem) { + override fun onSyncStatusToggleClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { + if (syncedFolderDisplayItem == null) return + if (syncedFolderDisplayItem.id > SyncedFolder.UNPERSISTED_ID) { syncedFolderProvider.updateSyncedFolderEnabled( syncedFolderDisplayItem.id, @@ -577,7 +585,7 @@ class SyncedFoldersActivity : } } - override fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem) { + override fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { val fragmentTransaction = supportFragmentManager.beginTransaction().apply { addToBackStack(null) } @@ -596,7 +604,9 @@ class SyncedFoldersActivity : } } - override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem) { + override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem?) { + if (syncedFolder == null) return + syncedFolder.isHidden = !syncedFolder.isHidden saveOrUpdateSyncedFolder(syncedFolder) adapter.setSyncFolderItem(section, syncedFolder) @@ -676,7 +686,7 @@ class SyncedFoldersActivity : saveOrUpdateSyncedFolder(newCustomFolder) adapter.addSyncFolderItem(newCustomFolder) } else { - val item = adapter[syncedFolder.section] + val item = adapter.get(syncedFolder.section) ?: return updateSyncedFolderItem( item, syncedFolder.id, @@ -793,11 +803,7 @@ class SyncedFoldersActivity : item.setExcludeHidden(excludeHidden) } - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { when (requestCode) { PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE -> { // If request is cancelled, result arrays are empty. @@ -815,7 +821,7 @@ class SyncedFoldersActivity : private fun showBatteryOptimizationInfo() { if (powerManagementService.isPowerSavingExclusionAvailable || checkIfBatteryOptimizationEnabled()) { - val alertDialogBuilder = AlertDialog.Builder(this, R.style.Theme_ownCloud_Dialog) + val alertDialogBuilder = MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog) .setTitle(getString(R.string.battery_optimization_title)) .setMessage(getString(R.string.battery_optimization_message)) .setPositiveButton(getString(R.string.battery_optimization_disable)) { _, _ -> 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 4de179ad5edc..da59551317a1 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 @@ -33,6 +33,7 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.utils.extensions.ActivityExtensionsKt; +import com.nextcloud.utils.extensions.FileExtensionsKt; import com.owncloud.android.R; import com.owncloud.android.databinding.UploadFilesLayoutBinding; import com.owncloud.android.lib.common.utils.Log_OC; @@ -410,7 +411,7 @@ public void handleOnBackPressed() { protected void onSaveInstanceState(@NonNull Bundle outState) { // responsibility of restore is preferred in onCreate() before than in // onRestoreInstanceState when there are Fragments involved - Log_OC.d(TAG, "onSaveInstanceState() start"); + FileExtensionsKt.logFileSize(mCurrentDir, TAG); super.onSaveInstanceState(outState); outState.putString(UploadFilesActivity.KEY_DIRECTORY_PATH, mCurrentDir.getAbsolutePath()); if (mOptionsMenu != null && mOptionsMenu.findItem(R.id.action_select_all) != null) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/PrintAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/PrintAdapter.java index 015196aa1583..5cc1f7dbeac5 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/PrintAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/PrintAdapter.java @@ -17,19 +17,17 @@ import com.owncloud.android.lib.common.utils.Log_OC; -import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Objects; public class PrintAdapter extends PrintDocumentAdapter { private static final String TAG = PrintAdapter.class.getSimpleName(); private static final String PDF_NAME = "finalPrint.pdf"; - private String filePath; + private final String filePath; public PrintAdapter(String filePath) { this.filePath = filePath; @@ -58,11 +56,9 @@ public void onWrite(PageRange[] pages, ParcelFileDescriptor destination, CancellationSignal cancellationSignal, WriteResultCallback callback) { - InputStream in = null; - OutputStream out = null; - try { - in = new FileInputStream(new File(filePath)); - out = new FileOutputStream(destination.getFileDescriptor()); + + try (InputStream in = new FileInputStream(filePath); + OutputStream out = new FileOutputStream(destination.getFileDescriptor())) { byte[] buf = new byte[16384]; int size; @@ -79,14 +75,6 @@ public void onWrite(PageRange[] pages, } catch (IOException e) { Log_OC.e(TAG, "Error using temp file", e); - } finally { - try { - Objects.requireNonNull(in).close(); - Objects.requireNonNull(out).close(); - - } catch (IOException | NullPointerException e) { - Log_OC.e(TAG, "Error closing streams", e); - } } } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java deleted file mode 100644 index 026f9e23510c..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java +++ /dev/null @@ -1,454 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2016 Andy Scherzinger - * SPDX-FileCopyrightText: 2016 Nextcloud - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.adapter; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.PopupMenu; - -import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter; -import com.afollestad.sectionedrecyclerview.SectionedViewHolder; -import com.nextcloud.client.core.Clock; -import com.owncloud.android.R; -import com.owncloud.android.databinding.GridSyncItemBinding; -import com.owncloud.android.databinding.SyncedFoldersEmptyBinding; -import com.owncloud.android.databinding.SyncedFoldersFooterBinding; -import com.owncloud.android.databinding.SyncedFoldersItemHeaderBinding; -import com.owncloud.android.datamodel.MediaFolderType; -import com.owncloud.android.datamodel.SyncedFolderDisplayItem; -import com.owncloud.android.datamodel.ThumbnailsCacheManager; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - -/** - * Adapter to display all auto-synced folders and/or instant upload media folders. - */ -public class SyncedFolderAdapter extends SectionedRecyclerViewAdapter { - - private final Context context; - private final Clock clock; - private final int gridWidth; - private final int gridTotal; - private final ClickListener clickListener; - private final List syncFolderItems; - private final List filteredSyncFolderItems; - private final boolean light; - private static final int VIEW_TYPE_EMPTY = Integer.MAX_VALUE; - private static final int VIEW_TYPE_ITEM = 1; - private static final int VIEW_TYPE_HEADER = 2; - private static final int VIEW_TYPE_FOOTER = 3; - private boolean hideItems; - private final ViewThemeUtils viewThemeUtils; - private final Executor thumbnailThreadPool; - - public SyncedFolderAdapter(Context context, - Clock clock, - int gridWidth, - ClickListener listener, - boolean light, - ViewThemeUtils viewThemeUtils) { - this.context = context; - this.clock = clock; - this.gridWidth = gridWidth; - gridTotal = gridWidth * 2; - clickListener = listener; - syncFolderItems = new ArrayList<>(); - filteredSyncFolderItems = new ArrayList<>(); - this.light = light; - this.hideItems = true; - this.viewThemeUtils = viewThemeUtils; - this.thumbnailThreadPool = Executors.newCachedThreadPool(); - - shouldShowHeadersForEmptySections(true); - shouldShowFooters(true); - } - - public void toggleHiddenItemsVisibility() { - hideItems = !hideItems; - filteredSyncFolderItems.clear(); - filteredSyncFolderItems.addAll(filterHiddenItems(syncFolderItems, hideItems)); - notifyDataSetChanged(); - } - - public void setSyncFolderItems(List syncFolderItems) { - this.syncFolderItems.clear(); - this.syncFolderItems.addAll(syncFolderItems); - - this.filteredSyncFolderItems.clear(); - this.filteredSyncFolderItems.addAll(filterHiddenItems(this.syncFolderItems, hideItems)); - } - - public void setSyncFolderItem(int location, SyncedFolderDisplayItem syncFolderItem) { - if (hideItems && syncFolderItem.isHidden() && filteredSyncFolderItems.contains(syncFolderItem)) { - filteredSyncFolderItems.remove(location); - } else { - if (filteredSyncFolderItems.contains(syncFolderItem)) { - filteredSyncFolderItems.set(filteredSyncFolderItems.indexOf(syncFolderItem), syncFolderItem); - } else { - filteredSyncFolderItems.add(syncFolderItem); - } - } - - if (syncFolderItems.contains(syncFolderItem)) { - syncFolderItems.set(syncFolderItems.indexOf(syncFolderItem), syncFolderItem); - } else { - syncFolderItems.add(syncFolderItem); - } - - notifyDataSetChanged(); - } - - public void addSyncFolderItem(SyncedFolderDisplayItem syncFolderItem) { - syncFolderItems.add(syncFolderItem); - - // add item for display when either all items should be shown (!hideItems) - // or if item should be shown (!.isHidden()) - if (!hideItems || !syncFolderItem.isHidden()) { - filteredSyncFolderItems.add(syncFolderItem); - notifyDataSetChanged(); - } - } - - public void removeItem(int section) { - if (filteredSyncFolderItems.contains(syncFolderItems.get(section))) { - filteredSyncFolderItems.remove(syncFolderItems.get(section)); - notifyDataSetChanged(); - } - syncFolderItems.remove(section); - } - - /** - * Filter for hidden items - * - * @param items Collection of items to filter - * @return Non-hidden items - */ - private List filterHiddenItems(List items, boolean hide) { - if (!hide) { - return items; - } else { - List result = new ArrayList<>(); - - for (SyncedFolderDisplayItem item : items) { - if (!item.isHidden() && !result.contains(item)) { - result.add(item); - } - } - - return result; - } - } - - @Override - public int getSectionCount() { - if (filteredSyncFolderItems.size() > 0) { - return filteredSyncFolderItems.size() + 1; - } else { - return 0; - } - } - - @VisibleForTesting - public void clear() { - filteredSyncFolderItems.clear(); - syncFolderItems.clear(); - } - - public int getUnfilteredSectionCount() { - if (syncFolderItems.size() > 0) { - return syncFolderItems.size() + 1; - } else { - return 0; - } - } - - @Override - public int getItemCount(int section) { - if (section < filteredSyncFolderItems.size()) { - List filePaths = filteredSyncFolderItems.get(section).getFilePaths(); - - if (filePaths != null) { - return filteredSyncFolderItems.get(section).getFilePaths().size(); - } else { - return 1; - } - } else { - return 1; - } - } - - public SyncedFolderDisplayItem get(int section) { - return filteredSyncFolderItems.get(section); - } - - @Override - public int getItemViewType(int section, int relativePosition, int absolutePosition) { - if (isLastSection(section)) { - return VIEW_TYPE_EMPTY; - } else { - return VIEW_TYPE_ITEM; - } - } - - @Override - public int getHeaderViewType(int section) { - if (isLastSection(section)) { - return VIEW_TYPE_EMPTY; - } else { - return VIEW_TYPE_HEADER; - } - } - - @Override - public int getFooterViewType(int section) { - if (isLastSection(section) && showFooter()) { - return VIEW_TYPE_FOOTER; - } else { - // only show footer after last item and only if folders have been hidden - return VIEW_TYPE_EMPTY; - } - } - - private boolean showFooter() { - return syncFolderItems.size() > filteredSyncFolderItems.size(); - } - - /** - * returns the section of a synced folder for the given local path and type. - * - * @param localPath the local path of the synced folder - * @param type the of the synced folder - * @return the section index of the looked up synced folder, -1 if not present - */ - public int getSectionByLocalPathAndType(String localPath, int type) { - for (int i = 0; i < filteredSyncFolderItems.size(); i++) { - if (filteredSyncFolderItems.get(i).getLocalPath().equalsIgnoreCase(localPath) && - filteredSyncFolderItems.get(i).getType().id == type) { - return i; - } - } - - return -1; - } - - @Override - public void onBindHeaderViewHolder(SectionedViewHolder commonHolder, final int section, boolean expanded) { - if (section < filteredSyncFolderItems.size()) { - HeaderViewHolder holder = (HeaderViewHolder) commonHolder; - holder.binding.headerContainer.setVisibility(View.VISIBLE); - - holder.binding.title.setText(filteredSyncFolderItems.get(section).getFolderName()); - - if (MediaFolderType.VIDEO == filteredSyncFolderItems.get(section).getType()) { - holder.binding.type.setImageResource(R.drawable.video_32dp); - } else if (MediaFolderType.IMAGE == filteredSyncFolderItems.get(section).getType()) { - holder.binding.type.setImageResource(R.drawable.image_32dp); - } else { - holder.binding.type.setImageResource(R.drawable.folder_star_32dp); - } - - holder.binding.syncStatusButton.setVisibility(View.VISIBLE); - holder.binding.syncStatusButton.setTag(section); - holder.binding.syncStatusButton.setOnClickListener(v -> { - filteredSyncFolderItems.get(section).setEnabled( - !filteredSyncFolderItems.get(section).isEnabled(), - clock.getCurrentTime() - ); - setSyncButtonActiveIcon( - holder.binding.syncStatusButton, - filteredSyncFolderItems.get(section).isEnabled()); - clickListener.onSyncStatusToggleClick(section, filteredSyncFolderItems.get(section)); - }); - setSyncButtonActiveIcon(holder.binding.syncStatusButton, filteredSyncFolderItems.get(section).isEnabled()); - - if (light) { - holder.binding.settingsButton.setVisibility(View.GONE); - } else { - holder.binding.settingsButton.setVisibility(View.VISIBLE); - holder.binding.settingsButton.setTag(section); - holder.binding.settingsButton.setOnClickListener( - v -> onOverflowIconClicked(section, filteredSyncFolderItems.get(section), v)); - } - } - } - - private void onOverflowIconClicked(int section, SyncedFolderDisplayItem item, View view) { - PopupMenu popup = new PopupMenu(context, view); - popup.inflate(R.menu.synced_folders_adapter); - popup.setOnMenuItemClickListener(i -> optionsItemSelected(i, section, item)); - popup.getMenu() - .findItem(R.id.action_auto_upload_folder_toggle_visibility) - .setChecked(item.isHidden()); - - popup.show(); - } - - private boolean optionsItemSelected(MenuItem menuItem, int section, SyncedFolderDisplayItem item) { - if (menuItem.getItemId() == R.id.action_auto_upload_folder_toggle_visibility) { - clickListener.onVisibilityToggleClick(section, item); - } else { - // default: R.id.action_create_custom_folder - clickListener.onSyncFolderSettingsClick(section, item); - } - return true; - } - - @Override - public void onBindFooterViewHolder(SectionedViewHolder holder, int section) { - if (isLastSection(section) && showFooter()) { - FooterViewHolder footerHolder = (FooterViewHolder) holder; - footerHolder.binding.footerText.setOnClickListener(v -> toggleHiddenItemsVisibility()); - footerHolder.binding.footerText.setText( - context.getResources().getQuantityString( - R.plurals.synced_folders_show_hidden_folders, - getHiddenFolderCount(), - getHiddenFolderCount() - ) - ); - } - } - - @Override - public void onBindViewHolder(SectionedViewHolder commonHolder, int section, int relativePosition, - int absolutePosition) { - if (section < filteredSyncFolderItems.size() && filteredSyncFolderItems.get(section).getFilePaths() != null) { - MainViewHolder holder = (MainViewHolder) commonHolder; - - File file = new File(filteredSyncFolderItems.get(section).getFilePaths().get(relativePosition)); - - ThumbnailsCacheManager.MediaThumbnailGenerationTask task = - new ThumbnailsCacheManager.MediaThumbnailGenerationTask(holder.binding.thumbnail, - context, - viewThemeUtils); - - ThumbnailsCacheManager.AsyncMediaThumbnailDrawable asyncDrawable = - new ThumbnailsCacheManager.AsyncMediaThumbnailDrawable( - context.getResources(), - ThumbnailsCacheManager.mDefaultImg - ); - holder.binding.thumbnail.setImageDrawable(asyncDrawable); - - task.executeOnExecutor(thumbnailThreadPool, file); - - // set proper tag - holder.binding.thumbnail.setTag(file.hashCode()); - - holder.itemView.setTag(relativePosition % gridWidth); - - if (filteredSyncFolderItems.get(section).getNumberOfFiles() > gridTotal && - relativePosition >= gridTotal - 1) { - holder.binding.counter.setText( - String.format( - Locale.US, - "%d", - filteredSyncFolderItems.get(section).getNumberOfFiles() - gridTotal)); - holder.binding.counterLayout.setVisibility(View.VISIBLE); - holder.binding.thumbnailDarkener.setVisibility(View.VISIBLE); - } else { - holder.binding.counterLayout.setVisibility(View.GONE); - holder.binding.thumbnailDarkener.setVisibility(View.GONE); - } - } - } - - @NonNull - @Override - public SectionedViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - if (viewType == VIEW_TYPE_HEADER) { - return new HeaderViewHolder( - SyncedFoldersItemHeaderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) - ); - } else if (viewType == VIEW_TYPE_FOOTER) { - return new FooterViewHolder( - SyncedFoldersFooterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) - ); - } else if (viewType == VIEW_TYPE_EMPTY) { - return new EmptyViewHolder( - SyncedFoldersEmptyBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) - ); - } else { - return new MainViewHolder( - GridSyncItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) - ); - } - } - - private boolean isLastSection(int section) { - return section >= getSectionCount() - 1; - } - - public int getHiddenFolderCount() { - if (syncFolderItems != null && filteredSyncFolderItems != null) { - return syncFolderItems.size() - filteredSyncFolderItems.size(); - } else { - return 0; - } - } - - public interface ClickListener { - void onSyncStatusToggleClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem); - void onSyncFolderSettingsClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem); - void onVisibilityToggleClick(int section, SyncedFolderDisplayItem item); - } - - static class HeaderViewHolder extends SectionedViewHolder { - protected SyncedFoldersItemHeaderBinding binding; - - private HeaderViewHolder(SyncedFoldersItemHeaderBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - static class FooterViewHolder extends SectionedViewHolder { - protected SyncedFoldersFooterBinding binding; - - private FooterViewHolder(SyncedFoldersFooterBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - static class EmptyViewHolder extends SectionedViewHolder { - private EmptyViewHolder(SyncedFoldersEmptyBinding binding) { - super(binding.getRoot()); - } - } - - static class MainViewHolder extends SectionedViewHolder { - protected GridSyncItemBinding binding; - - private MainViewHolder(GridSyncItemBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - private void setSyncButtonActiveIcon(ImageButton syncStatusButton, boolean enabled) { - if (enabled) { - syncStatusButton.setImageDrawable( - viewThemeUtils.platform.tintPrimaryDrawable(context, R.drawable.ic_cloud_sync_on) - ); - } else { - syncStatusButton.setImageResource(R.drawable.ic_cloud_sync_off); - } - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt new file mode 100644 index 000000000000..215f23f1f4b3 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt @@ -0,0 +1,456 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2016 Andy Scherzinger + * SPDX-FileCopyrightText: 2016 Nextcloud + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.owncloud.android.ui.adapter + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.PopupMenu +import androidx.annotation.VisibleForTesting +import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter +import com.afollestad.sectionedrecyclerview.SectionedViewHolder +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.client.core.Clock +import com.owncloud.android.R +import com.owncloud.android.databinding.GridSyncItemBinding +import com.owncloud.android.databinding.SyncedFoldersEmptyBinding +import com.owncloud.android.databinding.SyncedFoldersFooterBinding +import com.owncloud.android.databinding.SyncedFoldersItemHeaderBinding +import com.owncloud.android.datamodel.MediaFolderType +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.datamodel.ThumbnailsCacheManager.AsyncMediaThumbnailDrawable +import com.owncloud.android.datamodel.ThumbnailsCacheManager.MediaThumbnailGenerationTask +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.io.File +import java.util.Locale +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +/** + * Adapter to display all auto-synced folders and/or instant upload media folders. + */ +@Suppress("LongParameterList") +class SyncedFolderAdapter( + private val context: Context, + private val clock: Clock, + private val gridWidth: Int, + private val clickListener: ClickListener, + private val light: Boolean, + private val viewThemeUtils: ViewThemeUtils +) : SectionedRecyclerViewAdapter() { + + private val gridTotal = gridWidth * 2 + private val syncFolderItems: MutableList = ArrayList() + private val filteredSyncFolderItems: MutableList = ArrayList() + private var hideItems = true + private val thumbnailThreadPool: Executor = Executors.newCachedThreadPool() + + init { + shouldShowHeadersForEmptySections(true) + shouldShowFooters(true) + } + + @SuppressLint("NotifyDataSetChanged") + fun toggleHiddenItemsVisibility() { + hideItems = !hideItems + + filterHiddenItems(syncFolderItems, hideItems)?.let { + filteredSyncFolderItems.clear() + filteredSyncFolderItems.addAll(it) + notifyDataSetChanged() + } + } + + fun setSyncFolderItems(syncFolderItems: List) { + this.syncFolderItems.clear() + this.syncFolderItems.addAll(syncFolderItems) + + filterHiddenItems(this.syncFolderItems, hideItems)?.let { + filteredSyncFolderItems.clear() + filteredSyncFolderItems.addAll(it) + } + } + + @SuppressLint("NotifyDataSetChanged") + fun setSyncFolderItem(location: Int, syncFolderItem: SyncedFolderDisplayItem) { + if (hideItems && syncFolderItem.isHidden && filteredSyncFolderItems.contains(syncFolderItem)) { + filteredSyncFolderItems.removeAt(location) + } else { + if (filteredSyncFolderItems.contains(syncFolderItem)) { + filteredSyncFolderItems[filteredSyncFolderItems.indexOf(syncFolderItem)] = syncFolderItem + } else { + filteredSyncFolderItems.add(syncFolderItem) + } + } + + if (syncFolderItems.contains(syncFolderItem)) { + syncFolderItems[syncFolderItems.indexOf(syncFolderItem)] = syncFolderItem + } else { + syncFolderItems.add(syncFolderItem) + } + + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun addSyncFolderItem(syncFolderItem: SyncedFolderDisplayItem) { + syncFolderItems.add(syncFolderItem) + + // add item for display when either all items should be shown (!hideItems) + // or if item should be shown (!.isHidden()) + if (!hideItems || !syncFolderItem.isHidden) { + filteredSyncFolderItems.add(syncFolderItem) + notifyDataSetChanged() + } + } + + @SuppressLint("NotifyDataSetChanged") + fun removeItem(section: Int) { + if (filteredSyncFolderItems.contains(syncFolderItems[section])) { + filteredSyncFolderItems.remove(syncFolderItems[section]) + notifyDataSetChanged() + } + + syncFolderItems.removeAt(section) + } + + /** + * Filter for hidden items + * + * @param items Collection of items to filter + * @return Non-hidden items + */ + private fun filterHiddenItems( + items: List?, + hide: Boolean + ): List? { + if (!hide) { + return items + } else { + val result: MutableList = ArrayList() + + for (item in items!!) { + if (!item.isHidden && !result.contains(item)) { + result.add(item) + } + } + + return result + } + } + + override fun getSectionCount(): Int { + return if (filteredSyncFolderItems.size > 0) { + filteredSyncFolderItems.size + 1 + } else { + 0 + } + } + + @VisibleForTesting + fun clear() { + filteredSyncFolderItems.clear() + syncFolderItems.clear() + } + + val unfilteredSectionCount: Int + get() = if (syncFolderItems.size > 0) { + syncFolderItems.size + 1 + } else { + 0 + } + + override fun getItemCount(section: Int): Int { + if (section < filteredSyncFolderItems.size) { + val filePaths = filteredSyncFolderItems[section].filePaths + + return if (filePaths != null) { + filteredSyncFolderItems[section].filePaths.size + } else { + 1 + } + } else { + return 1 + } + } + + fun get(section: Int): SyncedFolderDisplayItem? { + return if (section in filteredSyncFolderItems.indices) { + filteredSyncFolderItems[section] + } else { + null + } + } + + override fun getItemViewType(section: Int, relativePosition: Int, absolutePosition: Int): Int { + return if (isLastSection(section)) { + VIEW_TYPE_EMPTY + } else { + VIEW_TYPE_ITEM + } + } + + override fun getHeaderViewType(section: Int): Int { + return if (isLastSection(section)) { + VIEW_TYPE_EMPTY + } else { + VIEW_TYPE_HEADER + } + } + + override fun getFooterViewType(section: Int): Int { + return if (isLastSection(section) && showFooter()) { + VIEW_TYPE_FOOTER + } else { + // only show footer after last item and only if folders have been hidden + VIEW_TYPE_EMPTY + } + } + + private fun showFooter(): Boolean { + return syncFolderItems.size > filteredSyncFolderItems.size + } + + /** + * returns the section of a synced folder for the given local path and type. + * + * @param localPath the local path of the synced folder + * @param type the of the synced folder + * @return the section index of the looked up synced folder, `-1` if not present + */ + fun getSectionByLocalPathAndType(localPath: String?, type: Int): Int { + for (i in filteredSyncFolderItems.indices) { + if (filteredSyncFolderItems[i].localPath.equals(localPath, ignoreCase = true) && + filteredSyncFolderItems[i].type.id == type + ) { + return i + } + } + + return -1 + } + + override fun onBindHeaderViewHolder(commonHolder: SectionedViewHolder, section: Int, expanded: Boolean) { + if (section < filteredSyncFolderItems.size) { + val holder = commonHolder as HeaderViewHolder + holder.binding.headerContainer.visibility = View.VISIBLE + + holder.binding.title.text = filteredSyncFolderItems[section].folderName + + if (MediaFolderType.VIDEO == filteredSyncFolderItems[section].type) { + holder.binding.type.setImageResource(R.drawable.video_32dp) + } else if (MediaFolderType.IMAGE == filteredSyncFolderItems[section].type) { + holder.binding.type.setImageResource(R.drawable.image_32dp) + } else { + holder.binding.type.setImageResource(R.drawable.folder_star_32dp) + } + + holder.binding.syncStatusButton.visibility = View.VISIBLE + holder.binding.syncStatusButton.tag = section + holder.binding.syncStatusButton.setOnClickListener { + filteredSyncFolderItems[section].setEnabled( + !filteredSyncFolderItems[section].isEnabled, + clock.currentTime + ) + setSyncButtonActiveIcon( + holder.binding.syncStatusButton, + filteredSyncFolderItems[section].isEnabled + ) + clickListener.onSyncStatusToggleClick(section, filteredSyncFolderItems[section]) + } + setSyncButtonActiveIcon(holder.binding.syncStatusButton, filteredSyncFolderItems[section].isEnabled) + + if (light) { + holder.binding.settingsButton.visibility = View.GONE + } else { + holder.binding.settingsButton.visibility = View.VISIBLE + holder.binding.settingsButton.tag = section + holder.binding.settingsButton.setOnClickListener { v: View -> + onOverflowIconClicked( + section, + filteredSyncFolderItems[section], + v + ) + } + } + } + } + + private fun onOverflowIconClicked(section: Int, item: SyncedFolderDisplayItem, view: View) { + val popup = PopupMenu(context, view).apply { + inflate(R.menu.synced_folders_adapter) + setOnMenuItemClickListener { i: MenuItem -> optionsItemSelected(i, section, item) } + menu + .findItem(R.id.action_auto_upload_folder_toggle_visibility) + .setChecked(item.isHidden) + } + + popup.show() + } + + private fun optionsItemSelected(menuItem: MenuItem, section: Int, item: SyncedFolderDisplayItem): Boolean { + if (menuItem.itemId == R.id.action_auto_upload_folder_toggle_visibility) { + clickListener.onVisibilityToggleClick(section, item) + } else { + // default: R.id.action_create_custom_folder + clickListener.onSyncFolderSettingsClick(section, item) + } + return true + } + + override fun onBindFooterViewHolder(holder: SectionedViewHolder, section: Int) { + if (isLastSection(section) && showFooter()) { + val footerHolder = holder as FooterViewHolder + footerHolder.binding.footerText.setOnClickListener { toggleHiddenItemsVisibility() } + footerHolder.binding.footerText.text = context.resources.getQuantityString( + R.plurals.synced_folders_show_hidden_folders, + hiddenFolderCount, + hiddenFolderCount + ) + } + } + + override fun onBindViewHolder( + commonHolder: SectionedViewHolder, + section: Int, + relativePosition: Int, + absolutePosition: Int + ) { + if (section < filteredSyncFolderItems.size && filteredSyncFolderItems[section].filePaths != null) { + val holder = commonHolder as MainViewHolder + + val file = File(filteredSyncFolderItems[section].filePaths[relativePosition]) + + val task = + MediaThumbnailGenerationTask( + holder.binding.thumbnail, + context, + viewThemeUtils + ) + + val asyncDrawable = + AsyncMediaThumbnailDrawable( + context.resources, + ThumbnailsCacheManager.mDefaultImg + ) + holder.binding.thumbnail.setImageDrawable(asyncDrawable) + + task.executeOnExecutor(thumbnailThreadPool, file) + + // set proper tag + holder.binding.thumbnail.tag = file.hashCode() + + holder.itemView.tag = relativePosition % gridWidth + + if (filteredSyncFolderItems[section].numberOfFiles > gridTotal && + relativePosition >= gridTotal - 1 + ) { + holder.binding.counter.text = String.format( + Locale.US, + "%d", + filteredSyncFolderItems[section].numberOfFiles - gridTotal + ) + holder.binding.counterLayout.visibility = View.VISIBLE + holder.binding.thumbnailDarkener.visibility = View.VISIBLE + } else { + holder.binding.counterLayout.visibility = View.GONE + holder.binding.thumbnailDarkener.visibility = View.GONE + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionedViewHolder { + return when (viewType) { + VIEW_TYPE_HEADER -> { + HeaderViewHolder( + SyncedFoldersItemHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + VIEW_TYPE_FOOTER -> { + FooterViewHolder( + SyncedFoldersFooterBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + VIEW_TYPE_EMPTY -> { + EmptyViewHolder( + SyncedFoldersEmptyBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + else -> { + MainViewHolder( + GridSyncItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + } + } + + private fun isLastSection(section: Int): Boolean { + return section >= sectionCount - 1 + } + + val hiddenFolderCount: Int + get() = syncFolderItems.size - filteredSyncFolderItems.size + + interface ClickListener { + fun onSyncStatusToggleClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) + fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) + fun onVisibilityToggleClick(section: Int, item: SyncedFolderDisplayItem?) + } + + internal class HeaderViewHolder(var binding: SyncedFoldersItemHeaderBinding) : SectionedViewHolder( + binding.root + ) + + internal class FooterViewHolder(var binding: SyncedFoldersFooterBinding) : SectionedViewHolder( + binding.root + ) + + internal class EmptyViewHolder(binding: SyncedFoldersEmptyBinding) : SectionedViewHolder(binding.root) + + internal class MainViewHolder(var binding: GridSyncItemBinding) : SectionedViewHolder( + binding.root + ) + + private fun setSyncButtonActiveIcon(syncStatusButton: ImageButton, enabled: Boolean) { + if (enabled) { + syncStatusButton.setImageDrawable( + viewThemeUtils.platform.tintDrawable(context, R.drawable.ic_cloud_sync_on, ColorRole.PRIMARY) + ) + } else { + syncStatusButton.setImageResource(R.drawable.ic_cloud_sync_off) + } + } + + companion object { + private const val VIEW_TYPE_EMPTY = Int.MAX_VALUE + private const val VIEW_TYPE_ITEM = 1 + private const val VIEW_TYPE_HEADER = 2 + private const val VIEW_TYPE_FOOTER = 3 + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/TrashbinListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/TrashbinListAdapter.java index c8a12dae3dec..8d3ebfecd9e8 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/TrashbinListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/TrashbinListAdapter.java @@ -83,7 +83,7 @@ public void setTrashbinFiles(List trashbinFiles, boolean clear) { files.addAll(trashbinFiles); files = preferences.getSortOrderByType(FileSortOrder.Type.trashBinView, - FileSortOrder.sort_new_to_old).sortTrashbinFiles(files); + FileSortOrder.SORT_NEW_TO_OLD).sortTrashbinFiles(files); notifyDataSetChanged(); } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt index 2c674efdc2bd..b6a58f4726ae 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt @@ -78,10 +78,7 @@ class UnifiedSearchItemViewHolder( binding.unifiedSearchItemLayout.setOnClickListener { listInterface.onSearchResultClicked(entry) } } - private fun getPlaceholder( - entry: SearchResultEntry, - mimetype: String? - ): Drawable { + private fun getPlaceholder(entry: SearchResultEntry, mimetype: String?): Drawable { val drawable = with(entry.icon) { when { equals("icon-folder") -> diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.kt index 07f4842422ba..d88e47c73c47 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.kt @@ -103,14 +103,16 @@ class UnifiedSearchListAdapter( } override fun onBindHeaderViewHolder(holder: SectionedViewHolder, section: Int, expanded: Boolean) { - val headerViewHolder = holder as UnifiedSearchHeaderViewHolder - headerViewHolder.bind(sections[section]) + (holder as UnifiedSearchHeaderViewHolder).run { + bind(sections[section]) + } } override fun onBindFooterViewHolder(holder: SectionedViewHolder, section: Int) { if (sections[section].hasMoreResults) { - val footerViewHolder = holder as UnifiedSearchFooterViewHolder - footerViewHolder.bind(sections[section]) + (holder as UnifiedSearchFooterViewHolder).run { + bind(sections[section]) + } } } @@ -126,17 +128,19 @@ class UnifiedSearchListAdapter( absolutePosition: Int ) { // TODO different binding (and also maybe diff UI) for non-file results - val itemViewHolder = holder as UnifiedSearchItemViewHolder - val entry = sections[section].entries[relativePosition] - itemViewHolder.bind(entry) + (holder as UnifiedSearchItemViewHolder).run { + val entry = sections[section].entries[relativePosition] + bind(entry) + } } override fun onViewAttachedToWindow(holder: SectionedViewHolder) { if (holder is UnifiedSearchItemViewHolder) { - val thumbnailShimmer = holder.binding.thumbnailShimmer - if (thumbnailShimmer.visibility == View.VISIBLE) { - thumbnailShimmer.setImageResource(R.drawable.background) - thumbnailShimmer.resetLoader() + holder.binding.thumbnailShimmer.run { + if (visibility == View.VISIBLE) { + setImageResource(R.drawable.background) + resetLoader() + } } } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java index 76b390518690..80fdcb89a5c0 100755 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java @@ -56,6 +56,7 @@ import java.io.File; import java.util.Arrays; +import java.util.List; import java.util.Optional; import androidx.annotation.NonNull; @@ -119,10 +120,12 @@ public void onBindHeaderViewHolder(SectionedViewHolder holder, int section, bool headerViewHolder.binding.uploadListAction.setOnClickListener(v -> { switch (group.type) { case CURRENT -> { - for (OCUpload upload : group.getItems()) { - uploadHelper.cancelFileUpload(upload.getRemotePath(), upload.getAccountName()); - } - loadUploadItemsFromDb(); + new Thread(() -> { + uploadHelper.cancelFileUploads( + Arrays.asList(group.items), + group.getItem(0).getAccountName()); + parentActivity.runOnUiThread(this::loadUploadItemsFromDb); + }).start(); } case FINISHED -> { uploadsStorageManager.clearSuccessfulUploads(); @@ -152,7 +155,7 @@ private void showFailedPopupMenu(HeaderViewHolder headerViewHolder) { // FIXME For e2e resume is not working new Thread(() -> { - FileUploadHelper.Companion.instance().retryFailedUploads( + uploadHelper.retryFailedUploads( uploadsStorageManager, connectivityService, accountManager, @@ -196,7 +199,7 @@ private void clearTempEncryptedFolder() { // FIXME For e2e resume is not working private void retryCancelledUploads() { new Thread(() -> { - boolean showNotExistMessage = FileUploadHelper.Companion.instance().retryCancelledUploads( + boolean showNotExistMessage = uploadHelper.retryCancelledUploads( uploadsStorageManager, connectivityService, accountManager, diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalDialog.kt b/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalDialog.kt index c27f5282ddc1..e42f1b7c626f 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalDialog.kt @@ -40,7 +40,7 @@ class AccountRemovalDialog : DialogFragment(), AvatarGenerationListener, Injecta private var user: User? = null private lateinit var alertDialog: AlertDialog private var _binding: AccountRemovalDialogBinding? = null - private val binding get() = _binding!! + val binding get() = _binding!! override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.java deleted file mode 100644 index f87616c50c93..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.java +++ /dev/null @@ -1,457 +0,0 @@ -/* - * - * Nextcloud Android client application - * - * @author Tobias Kaminsky - * @author TSI-mc - * Copyright (C) 2019 Tobias Kaminsky - * Copyright (C) 2019 Nextcloud GmbH - * Copyright (C) 2023 TSI-mc - * - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ - -package com.owncloud.android.ui.dialog; - -import android.app.Activity; -import android.app.Dialog; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; - -import com.google.android.material.button.MaterialButton; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.common.collect.Sets; -import com.nextcloud.client.account.CurrentAccountProvider; -import com.nextcloud.client.account.User; -import com.nextcloud.client.di.Injectable; -import com.nextcloud.client.network.ClientFactory; -import com.nextcloud.utils.extensions.BundleExtensionsKt; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.databinding.ChooseTemplateBinding; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.Template; -import com.owncloud.android.files.CreateFileFromTemplateOperation; -import com.owncloud.android.files.FetchTemplateOperation; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation; -import com.owncloud.android.lib.resources.files.model.RemoteFile; -import com.owncloud.android.ui.activity.ExternalSiteWebView; -import com.owncloud.android.ui.activity.RichDocumentsEditorWebView; -import com.owncloud.android.ui.adapter.RichDocumentsTemplateAdapter; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.FileStorageUtils; -import com.owncloud.android.utils.KeyboardUtils; -import com.owncloud.android.utils.NextcloudServer; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; -import androidx.recyclerview.widget.GridLayoutManager; - -/** - * Dialog to show templates for new documents/spreadsheets/presentations. - */ -public class ChooseRichDocumentsTemplateDialogFragment extends DialogFragment implements View.OnClickListener, - RichDocumentsTemplateAdapter.ClickListener, Injectable { - - private static final String ARG_PARENT_FOLDER = "PARENT_FOLDER"; - private static final String ARG_TYPE = "TYPE"; - private static final String TAG = ChooseRichDocumentsTemplateDialogFragment.class.getSimpleName(); - private static final String DOT = "."; - public static final int SINGLE_TEMPLATE = 1; - private static final String WAIT_DIALOG_TAG = "WAIT"; - - private Set fileNames; - - @Inject CurrentAccountProvider currentAccount; - @Inject ClientFactory clientFactory; - @Inject ViewThemeUtils viewThemeUtils; - @Inject FileDataStorageManager fileDataStorageManager; - @Inject KeyboardUtils keyboardUtils; - private RichDocumentsTemplateAdapter adapter; - private OCFile parentFolder; - private OwnCloudClient client; - private MaterialButton positiveButton; - private DialogFragment waitDialog; - - public enum Type { - DOCUMENT, - SPREADSHEET, - PRESENTATION - } - - ChooseTemplateBinding binding; - - @NextcloudServer(max = 18) // will be removed in favor of generic direct editing - public static ChooseRichDocumentsTemplateDialogFragment newInstance(OCFile parentFolder, Type type) { - ChooseRichDocumentsTemplateDialogFragment frag = new ChooseRichDocumentsTemplateDialogFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_PARENT_FOLDER, parentFolder); - args.putString(ARG_TYPE, type.name()); - frag.setArguments(args); - return frag; - } - - @Override - public void onStart() { - super.onStart(); - - AlertDialog alertDialog = (AlertDialog) getDialog(); - - if (alertDialog != null) { - positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton); - - MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE); - if (negativeButton != null) { - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton); - } - - positiveButton.setOnClickListener(this); - positiveButton.setEnabled(false); - } - - checkEnablingCreateButton(); - } - - @Override - public void onResume() { - super.onResume(); - keyboardUtils.showKeyboardForEditText(requireDialog().getWindow(), binding.filename); - } - - @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"); - } - - try { - client = clientFactory.create(currentAccount.getUser()); - } catch (ClientFactory.CreationException e) { - throw new RuntimeException(e); // we'll NPE without the client - } - - parentFolder = BundleExtensionsKt.getParcelableArgument(arguments, ARG_PARENT_FOLDER, OCFile.class); - List folderContent = fileDataStorageManager.getFolderContent(parentFolder, false); - fileNames = Sets.newHashSetWithExpectedSize(folderContent.size()); - - for (OCFile file : folderContent) { - fileNames.add(file.getFileName()); - } - - // Inflate the layout for the dialog - LayoutInflater inflater = requireActivity().getLayoutInflater(); - binding = ChooseTemplateBinding.inflate(inflater, null, false); - View view = binding.getRoot(); - - viewThemeUtils.material.colorTextInputLayout(binding.filenameContainer); - - Type type = Type.valueOf(arguments.getString(ARG_TYPE)); - new FetchTemplateTask(this, client).execute(type); - - binding.list.setHasFixedSize(true); - binding.list.setLayoutManager(new GridLayoutManager(activity, 2)); - adapter = new RichDocumentsTemplateAdapter(type, - this, - getContext(), - currentAccount, - clientFactory, - viewThemeUtils); - binding.list.setAdapter(adapter); - - binding.filename.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // not needed - } - - @Override - public void afterTextChanged(Editable s) { - checkEnablingCreateButton(); - } - }); - - int titleTextId = getTitle(type); - - // Build the dialog - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); - builder.setView(view) - .setPositiveButton(R.string.create, null) - .setNegativeButton(R.string.common_cancel, null) - .setTitle(titleTextId); - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(activity, builder); - - return builder.create(); - } - - private int getTitle(Type type) { - if (type == Type.DOCUMENT) { - return R.string.create_new_document; - } else if (type == Type.SPREADSHEET) { - return R.string.create_new_spreadsheet; - } else if (type == Type.PRESENTATION) { - return R.string.create_new_presentation; - } - - return R.string.select_template; - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - - private void createFromTemplate(Template template, String path) { - waitDialog = IndeterminateProgressDialog.newInstance(R.string.wait_a_moment, false); - waitDialog.show(getParentFragmentManager(), WAIT_DIALOG_TAG); - new CreateFileFromTemplateTask(this, client, template, path, currentAccount.getUser()).execute(); - } - - public void setTemplateList(List